diff --git a/README.md b/README.md index fdd164f32..416d08381 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![Matrix-icon](assets/img/readme/header.png) [![license](http://img.shields.io/badge/license-BSD3-brightgreen.svg?style=flat)](https://github.com/Tencent/matrix/blob/master/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Tencent/matrix/pulls) -[![WeChat Approved](https://img.shields.io/badge/Wechat%20Approved-2.0.5-red.svg)](https://github.com/Tencent/matrix/wiki) +[![WeChat Approved](https://img.shields.io/badge/Wechat%20Approved-2.1.0-red.svg)](https://github.com/Tencent/matrix/wiki) [![CircleCI](https://circleci.com/gh/Tencent/matrix.svg?style=shield)](https://app.circleci.com/pipelines/github/Tencent/matrix) (中文版本请参看[这里](#matrix_cn)) @@ -9,17 +9,19 @@ [Matrix for iOS/macOS 中文版](#matrix_ios_cn) [Matrix for android 中文版](#matrix_android_cn) [Matrix for iOS/macOS](#matrix_ios_en) -[Matrix for android ](#matrix_android_en) +[Matrix for android](#matrix_android_en) **Matrix** is an APM (Application Performance Manage) used in Wechat to monitor, locate and analyse performance problems. It is a **plugin style**, **non-invasive** solution and is currently available on iOS, macOS and Android. # Matrix for iOS/macOS -The monitoring scope of the current tool includes: crash, lag, and out-of-memory, which includes the following two plugins: +The monitoring scope of the current tool includes: crash, lag, and memory, which includes the following three plugins: * **WCCrashBlockMonitorPlugin:** Based on [KSCrash](https://github.com/kstenerud/KSCrash) framework, it features cutting-edge lag stack capture capabilities with crash capture. -* **WCMemoryStatPlugin:** A performance-optimized out-of-memory monitoring tool that captures memory allocation and the callstack of an application's out-of-memory event. +* **WCMemoryStatPlugin:** A memory monitoring tool that captures memory allocation and the callstack of an application's memory event. + +* **WCFPSMonitorPlugin:** A fps monitoring tool that captures main thread's callstack while user scrolling. ## Features @@ -32,18 +34,10 @@ The monitoring scope of the current tool includes: crash, lag, and out-of-memory #### WCMemoryStatPlugin * Live recording every object's creating and the corresponding callstack of its creation, and report it when the application out-of-memory is detected. -* Use a balanced binary tree to store living objects and a hash table to store the callstack to optimize performance to the extreme ## Getting Started #### Install -* **Install via Cocoapods** - 1. Install [CocoaPods](https://guides.cocoapods.org/using/getting-started.html); - 2. Run `pod repo update` to make CocoaPods aware of the latest available `matrix` versions; - 3. In your Podfile, add `pod 'matrix-wechat'` to your app target, from the command line, run `pod install`; - 4. Use the .xcworkspace file generated by CocoaPods to work on your project; - 5. Add `#import ` , then you can use the performance probe tool of WeChat. - * **Install with static framework** 1. Get source code of Matrix; 2. Open terminal, execute `make` in the `matrix/matrix-iOS` directory to compile and generate static library. After compiling, the iOS platform library is in the `matrix/matrix-iOS/build_ios` directory, and the macOS platform library is in the `matrix/matrix-iOS/build_macos` directory. @@ -73,15 +67,16 @@ WCCrashBlockMonitorPlugin *crashBlockPlugin = [[WCCrashBlockMonitorPlugin alloc] [curBuilder addPlugin:crashBlockPlugin]; // add lag and crash monitor. WCMemoryStatPlugin *memoryStatPlugin = [[WCMemoryStatPlugin alloc] init]; -[curBuilder addPlugin:memoryStatPlugin]; // add out-of-memory monitor. +[curBuilder addPlugin:memoryStatPlugin]; // add memory monitor. + +WCFPSMonitorPlugin *fpsMonitorPlugin = [[WCFPSMonitorPlugin alloc] init]; +[curBuilder addPlugin:fpsMonitorPlugin]; // add fps monitor. [matrix addMatrixBuilder:curBuilder]; [crashBlockPlugin start]; // start the lag and crash monitor. -// [memoryStatPlugin start]; -// start out-of-memory monitor -// Be careful, WCMemoryStatPlugin has a large performance loss after it is turned on. It is recommended to turn it on as needed. - +[memoryStatPlugin start]; // start memory monitor +[fpsMonitorPlugin start]; // start fps monitor ``` #### Receive callbacks to obtain monitoring data @@ -109,7 +104,7 @@ Each plugin added to `MatrixBuilder` will call back the corresponding event via ## Tutorials -At this point, Matrix has been integrated into the app and is beginning to collect crash, lag, and out-of-memory data. If you still have questions, check out the example: `samples/sample-apple/MatrixDemo`. +At this point, Matrix has been integrated into the app and is beginning to collect crash, lag, and memory data. If you still have questions, check out the example: `samples/sample-iOS/MatrixDemo`. @@ -220,7 +215,7 @@ At this point, Matrix has been integrated into the app and is beginning to colle 1. Configure `MATRIX_VERSION` in gradle.properties. ``` gradle - MATRIX_VERSION=2.0.5 + MATRIX_VERSION=2.1.0 ``` 2. Add `matrix-gradle-plugin` in your build.gradle: @@ -369,11 +364,11 @@ Then other components in Matrix could use Quikcen Backtrace to unwind stacktrace #### APK Checker Usage -APK Checker can run independently in Jar ([matrix-apk-canary-2.0.5.jar](https://repo.maven.apache.org/maven2/com/tencent/matrix/matrix-apk-canary/2.0.5/matrix-apk-canary-2.0.5.jar)) mode, usage: +APK Checker can run independently in Jar ([matrix-apk-canary-2.1.0.jar](https://repo.maven.apache.org/maven2/com/tencent/matrix/matrix-apk-canary/2.1.0/matrix-apk-canary-2.1.0.jar)) mode, usage: ```shell -java -jar matrix-apk-canary-2.0.5.jar +java -jar matrix-apk-canary-2.1.0.jar Usages: --config CONFIG-FILE-PATH or @@ -432,18 +427,20 @@ Matrix is under the BSD license. See the [LICENSE](https://github.com/Tencent/Ma # Matrix ![Matrix-icon](assets/img/readme/header.png) -[![license](http://img.shields.io/badge/license-BSD3-brightgreen.svg?style=flat)](https://github.com/Tencent/matrix/blob/master/LICENSE)[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Tencent/matrix/pulls) [![WeChat Approved](https://img.shields.io/badge/Wechat%20Approved-2.0.5-red.svg)](https://github.com/Tencent/matrix/wiki) +[![license](http://img.shields.io/badge/license-BSD3-brightgreen.svg?style=flat)](https://github.com/Tencent/matrix/blob/master/LICENSE)[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Tencent/matrix/pulls) [![WeChat Approved](https://img.shields.io/badge/Wechat%20Approved-2.1.0-red.svg)](https://github.com/Tencent/matrix/wiki) **Matrix** 是一款微信研发并日常使用的应用性能接入框架,支持iOS, macOS和Android。 Matrix 通过接入各种性能监控方案,对性能监控项的异常数据进行采集和分析,输出相应的问题分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。 # Matrix for iOS/macOS -当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件: +Matrix-iOS 当前工具监控范围包括:崩溃、卡顿和内存,包含以下三款插件: + +* **WCCrashBlockMonitorPlugin:** 基于 [KSCrash](https://github.com/kstenerud/KSCrash) 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力 -* **WCCrashBlockMonitorPlugin:** 基于 [KSCrash](https://github.com/kstenerud/KSCrash) 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。 +* **WCMemoryStatPlugin:** 一款性能极致的内存监控工具,能够全面捕获应用 OOM 时的内存分配以及调用堆栈情况 -* **WCMemoryStatPlugin:** 一款性能优化到极致的爆内存监控工具,能够全面捕获应用爆内存时的内存分配以及调用堆栈情况。 +* **WCFPSMonitorPlugin:** 一款 FPS 监控工具,当用户滑动界面时,记录主线程调用栈 ## 特性 @@ -455,18 +452,11 @@ Matrix 通过接入各种性能监控方案,对性能监控项的异常数据 #### WCMemoryStatPlugin -* 在应用运行期间获取对象存活以及相应的堆栈信息,在检测到应用爆内存时进行上报 -* 使用平衡二叉树存储存活对象,使用 Hash Table 存储堆栈,将性能优化到极致 +* 在应用运行期间获取对象存活以及相应的堆栈信息,在检测到应用 OOM 时进行上报 ## 使用方法 #### 安装 -* **通过 Cocoapods 安装** - 1. 先安装 [CocoaPods](https://guides.cocoapods.org/using/getting-started.html); - 2. 通过 pod repo update 更新 matrix 的 Cocoapods 版本; - 3. 在 Podfile 对应的 target 中,添加 pod 'matrix-wechat',并执行 pod install; - 4. 在项目中使用 Cocoapods 生成的 .xcworkspace运行工程; - 5. 在你的代码文件头引入头文件 #import ,就可以接入微信的性能探针工具了! * **通过静态库安装** 1. 获取 Matrix 源码; @@ -499,13 +489,15 @@ WCCrashBlockMonitorPlugin *crashBlockPlugin = [[WCCrashBlockMonitorPlugin alloc] WCMemoryStatPlugin *memoryStatPlugin = [[WCMemoryStatPlugin alloc] init]; [curBuilder addPlugin:memoryStatPlugin]; // 添加内存监控功能 + +WCFPSMonitorPlugin *fpsMonitorPlugin = [[WCFPSMonitorPlugin alloc] init]; +[curBuilder addPlugin:fpsMonitorPlugin]; // 添加 fps 监控功能 [matrix addMatrixBuilder:curBuilder]; [crashBlockPlugin start]; // 开启卡顿和崩溃监控 -// [memoryStatPlugin start]; -// 开启内存监控,注意 memoryStatPlugin 开启之后对性能损耗较大,建议按需开启 - +[memoryStatPlugin start]; // 开启内存监控 +[fpsMonitorPlugin start]; // 开启 fps 监控 ``` #### 接收回调获得监控数据 @@ -533,7 +525,7 @@ curBuilder.pluginListener = <一个遵循 MatrixPluginListenerDelegate 的对象 ## Demo -至此,Matrix 已经集成到应用中并且开始收集崩溃、ANR、卡顿和爆内存数据,如仍有疑问,请查看示例:`samples/sample-apple/MatrixDemo`。 +至此,Matrix 已经集成到应用中并且开始收集崩溃、卡顿和爆内存数据,如仍有疑问,请查看示例:`samples/sample-iOS/MatrixDemo` # Matrix for Android @@ -636,7 +628,7 @@ Matrix-android 当前监控范围包括:应用安装包大小,帧率变化 1. 在你项目根目录下的 gradle.properties 中配置要依赖的 Matrix 版本号,如: ``` gradle - MATRIX_VERSION=2.0.5 + MATRIX_VERSION=2.1.0 ``` 2. 在你项目根目录下的 build.gradle 文件添加 Matrix 依赖,如: @@ -782,10 +774,10 @@ WeChatBacktrace.instance().configure(getApplicationContext()).commit(); #### APK Checker -APK Check 以独立的 jar 包提供 ([matrix-apk-canary-2.0.5.jar](https://repo.maven.apache.org/maven2/com/tencent/matrix/matrix-apk-canary/2.0.5/matrix-apk-canary-2.0.5.jar)),你可以运行: +APK Check 以独立的 jar 包提供 ([matrix-apk-canary-2.1.0.jar](https://repo.maven.apache.org/maven2/com/tencent/matrix/matrix-apk-canary/2.1.0/matrix-apk-canary-2.1.0.jar)),你可以运行: ```cmd -java -jar matrix-apk-canary-2.0.5.jar +java -jar matrix-apk-canary-2.1.0.jar ``` 查看 Usages 来使用它。 @@ -846,3 +838,12 @@ Options: # License Matrix is under the BSD license. See the [LICENSE](https://github.com/Tencent/Matrix/blob/master/LICENSE) file for details + +# 信息公示 + +- SDK名称:Matrix +- 版本号:2.1.0 +- 开发者:深圳市腾讯计算机系统有限公司 +- 主要功能:Matrix是微信研发并日常使用的应用性能监控工具,支持iOS、macOS和Android。Matrix通过接入闪退、卡顿、耗电、内存等方面的监控方案,对性能监控项的异常数据进行采集和分析,输出相应的问题分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。 +- [Matrix SDK使用说明](https://github.com/Tencent/matrix) +- [Matrix SDK个人信息保护规则](https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/yTezupX6yF028Mpf) diff --git a/matrix/matrix-android/.gitignore b/matrix/matrix-android/.gitignore index 631c8277c..ff0340f47 100644 --- a/matrix/matrix-android/.gitignore +++ b/matrix/matrix-android/.gitignore @@ -8,4 +8,5 @@ .idea/ **/.* **/bin/ -/gradle/oss-android-template.gradle \ No newline at end of file +/gradle/oss-android-template.gradle +local.gradle \ No newline at end of file diff --git a/matrix/matrix-android/build.gradle b/matrix/matrix-android/build.gradle index 944ea24a8..6d26068e3 100644 --- a/matrix/matrix-android/build.gradle +++ b/matrix/matrix-android/build.gradle @@ -8,7 +8,7 @@ buildscript { } - gradle.ext.KOTLIN_VERSION = "1.3.72" + gradle.ext.KOTLIN_VERSION = "1.4.32" dependencies { // classpath @@ -52,7 +52,7 @@ ext { // For android sub-projects minSdkVersion = 19 targetSdkVersion = 29 - compileSdkVersion = 29 + compileSdkVersion = 31 buildToolsVersion = '29.0.2' MIN_SDK_VERSION_FOR_HOOK = 21 @@ -86,6 +86,8 @@ ext { ABI_FILTERS = ['armeabi-v7a', 'arm64-v8a'] LOGGER_VERSION = 1.1 // fixme logger + + LIFECYCLE_VERSION = '2.3.1' } // Build sample project diff --git a/matrix/matrix-android/gradle.properties b/matrix/matrix-android/gradle.properties index d0733879a..55933a473 100644 --- a/matrix/matrix-android/gradle.properties +++ b/matrix/matrix-android/gradle.properties @@ -14,7 +14,7 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro # org.gradle.parallel=true #Tue Jun 20 10:24:33 CST 2017 -VERSION_NAME_PREFIX=2.0.5 +VERSION_NAME_PREFIX=2.1.0 VERSION_NAME_SUFFIX= ## two options: Internal (for wechat), External (for public repo) PUBLISH_CHANNEL=Internal diff --git a/matrix/matrix-android/gradle/android-publish-private.gradle b/matrix/matrix-android/gradle/android-publish-private.gradle index 913642040..fdf3dc885 100644 --- a/matrix/matrix-android/gradle/android-publish-private.gradle +++ b/matrix/matrix-android/gradle/android-publish-private.gradle @@ -206,4 +206,19 @@ task buildAndPublishRepo(type: Copy, dependsOn: ['build', 'publish']) { } } +task publishRepo(type: Copy, dependsOn: ['publish']) { + group = "publishing" + + // save artifacts files to artifacts folder + from configurations.archives.allArtifacts.files + into "${rootProject.buildDir}/outputs/artifacts/" + rename { String fileName -> + fileName.replace("release.aar", "${version}.aar") + } + + doLast { + println "* published to repo: ${project.group}:${project.name}:${project.version}" + } +} + apply from: rootProject.file('gradle/check.gradle') \ No newline at end of file diff --git a/matrix/matrix-android/gradle/java-publish-private.gradle b/matrix/matrix-android/gradle/java-publish-private.gradle index 5a5000fdf..c3d0f8cad 100644 --- a/matrix/matrix-android/gradle/java-publish-private.gradle +++ b/matrix/matrix-android/gradle/java-publish-private.gradle @@ -143,4 +143,19 @@ task buildAndPublishRepo(type: Copy, dependsOn: ['build', 'publish']) { } } +task publishRepo(type: Copy, dependsOn: ['publish']) { + group = "publishing" + + // save artifacts files to artifacts folder + from configurations.archives.allArtifacts.files + into "${rootProject.buildDir}/outputs/artifacts/" + rename { String fileName -> + fileName.replace("release.aar", "${version}.aar") + } + + doLast { + println "* published to repo: ${project.group}:${project.name}:${project.version}" + } +} + apply from: rootProject.file('gradle/check.gradle') \ No newline at end of file diff --git a/matrix/matrix-android/gradle/java-publish.gradle b/matrix/matrix-android/gradle/java-publish.gradle index 65cb10554..7d751f877 100644 --- a/matrix/matrix-android/gradle/java-publish.gradle +++ b/matrix/matrix-android/gradle/java-publish.gradle @@ -141,7 +141,7 @@ task buildAndPublishToLocalMaven(type: Copy, dependsOn: ['build', 'publishToMave } } -task buildAndPublishRepo(type: Copy, dependsOn: ['build', 'publish']) { +task buildAndPublishRepo(type: Copy, dependsOn: ['publish']) { group = "publishing" // save artifacts files to artifacts folder diff --git a/matrix/matrix-android/matrix-android-commons/CMakeLists.txt b/matrix/matrix-android/matrix-android-commons/CMakeLists.txt index a9b8c0b65..1cdd8e69a 100644 --- a/matrix/matrix-android/matrix-android-commons/CMakeLists.txt +++ b/matrix/matrix-android/matrix-android-commons/CMakeLists.txt @@ -1,6 +1,11 @@ cmake_minimum_required(VERSION 3.4.1) -project(android-commons C) +project(android-commons) + +option(EnableLOG "Enable Logs" ON) +if(EnableLOG) + add_definitions(-DEnableLOG) +endif() -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/libsemi_dlfcn) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/libenhance_dlsym) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/libsemi_dlfcn) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/libxhook) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-commons/build.gradle b/matrix/matrix-android/matrix-android-commons/build.gradle index 62aef0853..ea33532cc 100644 --- a/matrix/matrix-android/matrix-android-commons/build.gradle +++ b/matrix/matrix-android/matrix-android-commons/build.gradle @@ -19,6 +19,7 @@ android { externalNativeBuild { cmake { targets = ['xhook', 'semi_dlfcn', 'enhance_dlsym'] + arguments = ["-DEnableLOG=${gradle.enableLog() ? "ON" : "OFF"}" as String] } exportHeaders { from('src/main/cpp/libxhook') { diff --git a/matrix/matrix-android/matrix-android-commons/src/main/AndroidManifest.xml b/matrix/matrix-android/matrix-android-commons/src/main/AndroidManifest.xml index 8d3d02285..44d0b0d6e 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/AndroidManifest.xml +++ b/matrix/matrix-android/matrix-android-commons/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libenhance_dlsym/CMakeLists.txt b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libenhance_dlsym/CMakeLists.txt index 52b7c5b46..0443da00d 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libenhance_dlsym/CMakeLists.txt +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libenhance_dlsym/CMakeLists.txt @@ -3,17 +3,21 @@ project(libenhance_dlsym CXX) set(enhance_dlsym_source_dir ${CMAKE_CURRENT_SOURCE_DIR}) -set(enhance_dlsym_source - ${enhance_dlsym_source_dir}/EnhanceDlsym.cpp) +set( + enhance_dlsym_source + ${enhance_dlsym_source_dir}/EnhanceDlsym.cpp +) add_library(enhance_dlsym STATIC ${enhance_dlsym_source}) +find_library(log-lib log) + target_include_directories( - enhance_dlsym - PUBLIC ${enhance_dlsym_source_dir} + enhance_dlsym + PUBLIC ${enhance_dlsym_source_dir} ) target_link_libraries( - enhance_dlsym - PUBLIC ${log-lib} + enhance_dlsym + PUBLIC ${log-lib} ) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libenhance_dlsym/EnhanceDlsym.cpp b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libenhance_dlsym/EnhanceDlsym.cpp index 6eb5b0cd4..15c4fc7a4 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libenhance_dlsym/EnhanceDlsym.cpp +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libenhance_dlsym/EnhanceDlsym.cpp @@ -32,10 +32,15 @@ #include #include #include "EnhanceDlsym.h" -#include "../../../../../matrix-jectl/src/main/cpp/jectl/JeLog.h" #define TAG "Matrix.EnhanceDl" +#include + +#define LOGD(TAG, FMT, args...) //__android_log_print(ANDROID_LOG_DEBUG, TAG, FMT, ##args) +#define LOGI(TAG, FMT, args...) //__android_log_print(ANDROID_LOG_INFO, TAG, FMT, ##args) +#define LOGE(TAG, FMT, args...) //__android_log_print(ANDROID_LOG_ERROR, TAG, FMT, ##args) + namespace enhance { static std::set m_opened_info; diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/CMakeLists.txt b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/CMakeLists.txt index 893bf07ba..fb352f0d5 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/CMakeLists.txt +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/CMakeLists.txt @@ -3,18 +3,22 @@ project(libsemi_dlfcn C) set(semi_dlfcn_source_dir ${CMAKE_CURRENT_SOURCE_DIR}) -set(semi_dlfcn_source - ${semi_dlfcn_source_dir}/semi_dlfcn.c - ${semi_dlfcn_source_dir}/sd_log.c) +set( + semi_dlfcn_source + ${semi_dlfcn_source_dir}/semi_dlfcn.c + ${semi_dlfcn_source_dir}/sd_log.c +) add_library(semi_dlfcn STATIC ${semi_dlfcn_source}) +find_library(log-lib log) + target_include_directories( - semi_dlfcn - PUBLIC ${semi_dlfcn_source_dir} + semi_dlfcn + PUBLIC ${semi_dlfcn_source_dir} ) target_link_libraries( - semi_dlfcn - PUBLIC ${log-lib} + semi_dlfcn + PUBLIC ${log-lib} ) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/sd_log.c b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/sd_log.c index a94d33faa..3e89448b6 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/sd_log.c +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/sd_log.c @@ -4,5 +4,10 @@ #include "sd_log.h" +#ifdef EnableLOG bool g_semi_dlfcn_log_enabled = true; +#else +bool g_semi_dlfcn_log_enabled = false; +#endif + int g_semi_dlfcn_log_level = ANDROID_LOG_DEBUG; \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/semi_dlfcn.c b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/semi_dlfcn.c index 3ac926db9..abe0a2b6d 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/semi_dlfcn.c +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libsemi_dlfcn/semi_dlfcn.c @@ -1,3 +1,4 @@ +#include // // Created by YinSheng Tang on 2021/7/21. // @@ -27,9 +28,9 @@ typedef struct { uint32_t magic; const char* pathname; - const void* base_addr; + const ElfW(Ehdr)* ehdr; - ElfW(Phdr)* phdrs; + const ElfW(Phdr)* phdrs; ElfW(Word) phdr_count; ElfW(Addr) load_bias; @@ -38,6 +39,9 @@ typedef struct { ElfW(Sym)* syms; ElfW(Word) sym_count; + + ElfW(Sym)* dynsyms; + ElfW(Word) dynsym_count; } semi_dlinfo_t; // Copy from xhook. @@ -82,11 +86,21 @@ static bool validate_elf_header(uintptr_t base_addr) { return true; } +static ElfW(Addr) find_load_bias(ElfW(Addr) base, const ElfW(Phdr*) phdrs, ElfW(Half) phnum) { + for (int i = 0; i < phnum; ++i) { + const ElfW(Phdr*) phdr = phdrs + i; + if (phdr->p_type == PT_LOAD) { + return base - phdr->p_vaddr; + } + } + return 0; +} + static int legacy_iterate_maps(iterate_callback cb, void* data) { int res = 0; - FILE* fp = fopen("/proc/thread-self/maps", "r"); + FILE* fp = fopen("/proc/self/maps", "r"); if (fp == NULL) { - LOGE(LOG_TAG, "Fail to open /proc/thread-self/maps."); + LOGE(LOG_TAG, "Fail to open /proc/self/maps."); return res; } char line[512] = {}; @@ -96,7 +110,8 @@ static int legacy_iterate_maps(iterate_callback cb, void* data) { int offset = 0; int pathname_pos = 0; - if (sscanf(line, "%"PRIxPTR"-%*" PRIxPTR " %4s %x %*x:%*x %*d%n", &base_addr, perm, &offset, &pathname_pos) != 3) { + if (sscanf(line, "%"PRIxPTR"-%*" PRIxPTR " %4s %x %*x:%*x %*d%n", // NOLINT(cert-err34-c) + &base_addr, perm, &offset, &pathname_pos) != 3) { continue; } @@ -147,11 +162,12 @@ static int legacy_iterate_maps(iterate_callback cb, void* data) { continue; } - struct dl_phdr_info info; - info.dlpi_addr = base_addr; + struct dl_phdr_info info = {0}; info.dlpi_name = pathname; - info.dlpi_phdr = (ElfW(Phdr)*) (base_addr + ((ElfW(Ehdr)*) base_addr)->e_phoff); - info.dlpi_phnum = ((ElfW(Ehdr)*) base_addr)->e_phnum; + const ElfW(Ehdr)* ehdr = (const ElfW(Ehdr)*) base_addr; + info.dlpi_phdr = (ElfW(Phdr)*) (base_addr + ehdr->e_phoff); + info.dlpi_phnum = ehdr->e_phnum; + info.dlpi_addr = find_load_bias(base_addr, info.dlpi_phdr, info.dlpi_phnum); res = cb(&info, sizeof(struct dl_phdr_info), data); if (res != 0) { @@ -164,8 +180,12 @@ static int legacy_iterate_maps(iterate_callback cb, void* data) { return res; } +#pragma clang diagnostic push +#pragma ide diagnostic ignored "readability-redundant-declaration" +#pragma ide diagnostic ignored "bugprone-reserved-identifier" // Make compiler happy. extern int dl_iterate_phdr(int (*__callback)(struct dl_phdr_info*, size_t, void*), void* __data) __attribute__((weak)); +#pragma clang diagnostic pop #ifdef __LP64__ #define LINKER_PATHNAME_SUFFIX "/system/bin/linker64" @@ -176,11 +196,11 @@ extern int dl_iterate_phdr(int (*__callback)(struct dl_phdr_info*, size_t, void* #define LINKER_PATHNAME_SUFFIX_LEN (sizeof(LINKER_PATHNAME_SUFFIX) - 1) static pthread_mutex_t s_linker_base_mutex = PTHREAD_MUTEX_INITIALIZER; -static const void* s_linker_base = NULL; +static ElfW(Addr) s_linker_base = 0; static pthread_mutex_t s_dl_mutex_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t* s_dl_mutex_address = NULL; -static int find_linker_base_iter_cb(struct dl_phdr_info* info, size_t info_size, void* data) { +static int find_linker_base_iter_cb(struct dl_phdr_info* info, __unused size_t info_size, void* data) { const char* pathname = info->dlpi_name; size_t pathname_len = sizeof(pathname); if (pathname_len < LINKER_PATHNAME_SUFFIX_LEN) { @@ -194,14 +214,14 @@ static int find_linker_base_iter_cb(struct dl_phdr_info* info, size_t info_size, return 0; } -static const void* get_linker_base_address() { +static ElfW(Addr) get_linker_base_address() { pthread_mutex_lock(&s_linker_base_mutex); - if (s_linker_base != NULL) { + if (s_linker_base != 0) { goto bail; } - s_linker_base = (const void*) getauxval(AT_BASE); - if (s_linker_base == NULL) { + s_linker_base = (ElfW(Addr)) getauxval(AT_BASE); + if (s_linker_base == 0) { legacy_iterate_maps(find_linker_base_iter_cb, &s_linker_base); } @@ -210,6 +230,8 @@ static const void* get_linker_base_address() { return s_linker_base; } +static bool fill_rest_neccessary_data(semi_dlinfo_t* semi_dlinfo); + #define LINKER_DL_MUTEX_SYMNAME "__dl__ZL10g_dl_mutex" static pthread_mutex_t* get_dl_mutex_address() { @@ -218,36 +240,65 @@ static pthread_mutex_t* get_dl_mutex_address() { goto bail; } - const void* linker_base = get_linker_base_address(); - if (linker_base == NULL) { + ElfW(Addr) linker_base = get_linker_base_address(); + if (linker_base == 0) { goto bail; } + semi_dlinfo_t semi_dlinfo = {0}; + semi_dlinfo.magic = SEMI_DLINFO_MAGIC; + semi_dlinfo.pathname = LINKER_PATHNAME_SUFFIX; + semi_dlinfo.ehdr = (ElfW(Ehdr)*) linker_base; + semi_dlinfo.phdrs = (const ElfW(Phdr)*) (((ElfW(Addr)) linker_base) + semi_dlinfo.ehdr->e_phoff); + semi_dlinfo.phdr_count = semi_dlinfo.ehdr->e_phnum; + semi_dlinfo.load_bias = find_load_bias(linker_base, semi_dlinfo.phdrs, semi_dlinfo.phdr_count); + fill_rest_neccessary_data(&semi_dlinfo); - s_dl_mutex_address = semi_dlsym(linker_base, LINKER_DL_MUTEX_SYMNAME); + s_dl_mutex_address = semi_dlsym(&semi_dlinfo, LINKER_DL_MUTEX_SYMNAME); bail: pthread_mutex_unlock(&s_dl_mutex_mutex); return s_dl_mutex_address; } +typedef struct { + void* original_data; + iterate_callback original_cb; +} dl_iter_data_wrapper_t; + +static int dl_iterate_phdr_npe_avoidance_cb(struct dl_phdr_info* info, size_t info_size, void* wrapped_data) { + if (info->dlpi_name == NULL) { + LOGW(LOG_TAG, "Path is null, skip it."); + return 0; + } + dl_iter_data_wrapper_t* casted_wrapped_data = (dl_iter_data_wrapper_t*) wrapped_data; + return casted_wrapped_data->original_cb(info, info_size, casted_wrapped_data->original_data); +} + int semi_dl_iterate_phdr(iterate_callback cb, void* data) { int sdk = android_get_device_api_level(); - if (sdk < 21) { + bool fallback_to_legacy = false; + Dl_info tmp_info = {}; + dladdr(&semi_dl_iterate_phdr, &tmp_info); + if (tmp_info.dli_fname != NULL && tmp_info.dli_fname[0] != '/') { + LOGW(LOG_TAG, "dladdr only tell us relative path of loaded so, fallbacl to legacy iterate mode."); + fallback_to_legacy = true; + } + if (sdk < 21 || fallback_to_legacy) { return legacy_iterate_maps(cb, data); } else if (sdk >= 21 && sdk <= 22) { pthread_mutex_t *dl_mutex = get_dl_mutex_address(); if (dl_mutex != NULL) { pthread_mutex_lock(dl_mutex); } - const void *linker_base = get_linker_base_address(); + ElfW(Addr) linker_base = get_linker_base_address(); int ret = 0; - if (linker_base != NULL) { - struct dl_phdr_info dlinfo; - dlinfo.dlpi_addr = (ElfW(Addr)) linker_base; + if (linker_base != 0) { + struct dl_phdr_info dlinfo = {0}; dlinfo.dlpi_name = LINKER_PATHNAME_SUFFIX; ElfW(Ehdr)* ehdr = (ElfW(Ehdr)*) linker_base; dlinfo.dlpi_phdr = (ElfW(Phdr)*) (((uintptr_t) linker_base) + ehdr->e_phoff); dlinfo.dlpi_phnum = ehdr->e_phnum; + dlinfo.dlpi_addr = find_load_bias(linker_base, dlinfo.dlpi_phdr, dlinfo.dlpi_phnum); ret = cb(&dlinfo, sizeof(struct dl_phdr_info), data); } else { LOGW(LOG_TAG, "Cannot find base of linker."); @@ -256,23 +307,27 @@ int semi_dl_iterate_phdr(iterate_callback cb, void* data) { goto bail_sdk_21_22; } - ret = dl_iterate_phdr(cb, data); + dl_iter_data_wrapper_t wrapped_data = { + .original_cb = cb, + .original_data = data + }; + ret = dl_iterate_phdr(dl_iterate_phdr_npe_avoidance_cb, &wrapped_data); bail_sdk_21_22: if (dl_mutex != NULL) { pthread_mutex_unlock(dl_mutex); } return ret; - } else if (sdk >= 23 && sdk <= 26) { - const void *linker_base = get_linker_base_address(); + } else if (sdk >= 23) { + ElfW(Addr) linker_base = get_linker_base_address(); int ret = 0; - if (linker_base != NULL) { - struct dl_phdr_info dlinfo; - dlinfo.dlpi_addr = (ElfW(Addr)) linker_base; + if (linker_base != 0) { + struct dl_phdr_info dlinfo = {0}; dlinfo.dlpi_name = LINKER_PATHNAME_SUFFIX; ElfW(Ehdr)* ehdr = (ElfW(Ehdr)*) linker_base; dlinfo.dlpi_phdr = (ElfW(Phdr)*) (((uintptr_t) linker_base) + ehdr->e_phoff); dlinfo.dlpi_phnum = ehdr->e_phnum; + dlinfo.dlpi_addr = find_load_bias((ElfW(Addr)) linker_base, dlinfo.dlpi_phdr, dlinfo.dlpi_phnum); ret = cb(&dlinfo, sizeof(struct dl_phdr_info), data); } else { LOGW(LOG_TAG, "Cannot find base of linker."); @@ -281,12 +336,14 @@ int semi_dl_iterate_phdr(iterate_callback cb, void* data) { goto bail_sdk_23_26; } - ret = dl_iterate_phdr(cb, data); + dl_iter_data_wrapper_t wrapped_data = { + .original_cb = cb, + .original_data = data + }; + ret = dl_iterate_phdr(dl_iterate_phdr_npe_avoidance_cb, &wrapped_data); bail_sdk_23_26: return ret; - } else { - return dl_iterate_phdr(cb, data); } } @@ -296,7 +353,7 @@ typedef struct { semi_dlinfo_t* semi_dlinfo; } semi_dlopen_iter_info; -static int dlopen_iter_cb(struct dl_phdr_info* info, size_t info_size, void* data) { +static int dlopen_iter_cb(struct dl_phdr_info* info, __unused size_t info_size, void* data) { semi_dlopen_iter_info* iter_info = (semi_dlopen_iter_info*) data; semi_dlinfo_t* semi_dlinfo = iter_info->semi_dlinfo; @@ -311,14 +368,23 @@ static int dlopen_iter_cb(struct dl_phdr_info* info, size_t info_size, void* dat return 0; } - LOGD(LOG_TAG, "dlopen_iter_cb, pathname: %s, name_suffix: %s, suffix_len: %zu", pathname, name_suffix, name_suffix_len); + LOGD(LOG_TAG, "pathname: %s, suffix_to_find: %s", info->dlpi_name, name_suffix); if (strncmp(pathname + pathname_len - name_suffix_len, name_suffix, name_suffix_len) == 0) { semi_dlinfo->pathname = pathname; - semi_dlinfo->base_addr = (const void*) info->dlpi_addr; - ElfW(Ehdr)* ehdr = (ElfW(Ehdr)*) info->dlpi_addr; - semi_dlinfo->phdrs = (ElfW(Phdr)*) (((uintptr_t) info->dlpi_addr) + ehdr->e_phoff); - semi_dlinfo->phdr_count = ehdr->e_phnum; + semi_dlinfo->phdrs = info->dlpi_phdr; + semi_dlinfo->phdr_count = info->dlpi_phnum; + semi_dlinfo->load_bias = info->dlpi_addr; + for (int phdrIdx = 0; phdrIdx < info->dlpi_phnum; ++phdrIdx) { + const ElfW(Phdr)* phdr = info->dlpi_phdr + phdrIdx; + if (phdr->p_type == PT_LOAD) { + // dlpi_addr is actually load_bias. + semi_dlinfo->ehdr = (ElfW(Ehdr)*) (info->dlpi_addr + phdr->p_vaddr); + break; + } + } + LOGD(LOG_TAG, "dlopen_iter_cb, pathname: %s, name_suffix: %s, suffix_len: %zu, dlpi_addr: %p, ehdr: %p, phdr: %p", + pathname, name_suffix, name_suffix_len, (void*) info->dlpi_addr, semi_dlinfo->ehdr, info->dlpi_phdr); return 1; } @@ -346,7 +412,10 @@ static bool load_section(int fd, off_t file_offset, size_t section_size, void** goto fail; } + #pragma clang diagnostic push + #pragma ide diagnostic ignored "UnreachableCode" goto bail; + #pragma clang diagnostic pop fail: res = false; @@ -359,17 +428,11 @@ static bool load_section(int fd, off_t file_offset, size_t section_size, void** } static bool fill_rest_neccessary_data(semi_dlinfo_t* semi_dlinfo) { - ElfW(Ehdr)* ehdr = (ElfW(Ehdr)*) semi_dlinfo->base_addr; - ElfW(Phdr)* phdrs = semi_dlinfo->phdrs; - for (int i = 0; i < semi_dlinfo->phdr_count; ++i) { - if (phdrs[i].p_type == PT_LOAD) { - if ((ElfW(Addr)) semi_dlinfo->base_addr < phdrs[i].p_vaddr) { - LOGE(LOG_TAG, "Unexpected first program header."); - return false; - } - semi_dlinfo->load_bias = ((uintptr_t) semi_dlinfo->base_addr) - phdrs[i].p_vaddr; - break; - } + const ElfW(Ehdr)* ehdr = semi_dlinfo->ehdr; + + if (semi_dlinfo->phdr_count != semi_dlinfo->ehdr->e_phnum) { + LOGE(LOG_TAG, "Phdr count mismatch in \"%s\".", semi_dlinfo->pathname); + return false; } int fd = open(semi_dlinfo->pathname, O_RDONLY); @@ -387,6 +450,7 @@ static bool fill_rest_neccessary_data(semi_dlinfo_t* semi_dlinfo) { bool strtbl_ok = false; bool symtbl_ok = false; + bool dynsym_ok = false; for (int shdr_idx = ehdr->e_shnum - 1; shdr_idx >= 0; --shdr_idx) { ElfW(Shdr)* shdr = shdrs + shdr_idx; if (shdr->sh_type == SHT_STRTAB && shdr_idx != ehdr->e_shstrndx) { @@ -412,8 +476,20 @@ static bool fill_rest_neccessary_data(semi_dlinfo_t* semi_dlinfo) { semi_dlinfo->sym_count = 0; symtbl_ok = false; } + } else if (shdr->sh_type == SHT_DYNSYM) { + LOGD(LOG_TAG, "load dynsym, sh_off: %" PRIxPTR ", sh_size: %" PRIuPTR, + (uintptr_t) shdr->sh_offset, (uintptr_t) shdr->sh_size); + if (LIKELY(load_section(fd, shdr->sh_offset, shdr->sh_size, (void **) &semi_dlinfo->dynsyms))) { + semi_dlinfo->dynsym_count = shdr->sh_size / shdr->sh_entsize; + dynsym_ok = true; + } else { + LOGE(LOG_TAG, "Fail to map dynamic symbol table of \"%s\"", semi_dlinfo->pathname); + semi_dlinfo->dynsyms = NULL; + semi_dlinfo->dynsym_count = 0; + dynsym_ok = false; + } } - if (strtbl_ok && symtbl_ok) { + if (strtbl_ok && (symtbl_ok || dynsym_ok)) { break; } } @@ -423,10 +499,24 @@ static bool fill_rest_neccessary_data(semi_dlinfo_t* semi_dlinfo) { free(shdrs); } - if (LIKELY(strtbl_ok && symtbl_ok)) { + if (LIKELY(strtbl_ok && (symtbl_ok || dynsym_ok))) { return true; } else { LOGE(LOG_TAG, "Failure in fill_rest_neccessary_data."); + if (semi_dlinfo->strs != NULL) { + free(semi_dlinfo->strs); + semi_dlinfo->strs = NULL; + } + if (semi_dlinfo->syms != NULL) { + free(semi_dlinfo->syms); + semi_dlinfo->syms = NULL; + semi_dlinfo->sym_count = 0; + } + if (semi_dlinfo->dynsyms != NULL) { + free(semi_dlinfo->dynsyms); + semi_dlinfo->dynsyms = NULL; + semi_dlinfo->dynsym_count = 0; + } return false; } } @@ -471,7 +561,7 @@ void* semi_dlopen(const char* pathname) { semi_dl_iterate_phdr(dlopen_iter_cb, &iter_info); - if (result->base_addr != NULL) { + if (result->ehdr != NULL) { if (!fill_rest_neccessary_data(result)) { goto fail; } @@ -499,7 +589,7 @@ void* semi_dlopen(const char* pathname) { void* semi_dlsym(const void* semi_hlib, const char* sym_name) { semi_dlinfo_t* semi_dlinfo = (semi_dlinfo_t*) semi_hlib; if (UNLIKELY(semi_dlinfo->magic != SEMI_DLINFO_MAGIC)) { - LOGE(LOG_TAG, "Invalid semi_hlib, skip doing dlsym."); + LOGE(LOG_TAG, "Invalid semi_hlib, skip doing dlsym. %x", semi_dlinfo->magic); return NULL; } @@ -512,6 +602,15 @@ void* semi_dlsym(const void* semi_hlib, const char* sym_name) { } } + for (int i = 0; i < semi_dlinfo->dynsym_count; ++i) { + ElfW(Sym) *curr_sym = semi_dlinfo->dynsyms + i; + int sym_type = ELF_ST_TYPE(curr_sym->st_info); + const char *curr_sym_name = semi_dlinfo->strs + curr_sym->st_name; + if ((sym_type == STT_FUNC || sym_type == STT_OBJECT) && strcmp(curr_sym_name, sym_name) == 0) { + return (void *) semi_dlinfo->load_bias + curr_sym->st_value; + } + } + LOGE(LOG_TAG, "Cannot find symbol \"%s\" in \"%s\"", sym_name, semi_dlinfo->pathname); return NULL; } @@ -528,9 +627,17 @@ void semi_dlclose(void* semi_hlib) { } if (semi_dlinfo->strs != NULL) { free(semi_dlinfo->strs); + semi_dlinfo->strs = NULL; } if (semi_dlinfo->syms != NULL) { free(semi_dlinfo->syms); + semi_dlinfo->syms = NULL; + semi_dlinfo->sym_count = 0; + } + if (semi_dlinfo->dynsyms != NULL) { + free(semi_dlinfo->dynsyms); + semi_dlinfo->dynsyms = NULL; + semi_dlinfo->dynsym_count = 0; } free(semi_hlib); } \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/CMakeLists.txt b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/CMakeLists.txt index 429117605..33425c5c4 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/CMakeLists.txt +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/CMakeLists.txt @@ -3,31 +3,31 @@ project(libxhook C) set(xhook_source_dir ${CMAKE_CURRENT_SOURCE_DIR}) -set(xhook_source - ${xhook_source_dir}/xh_core.c - ${xhook_source_dir}/xh_elf.c - ${xhook_source_dir}/xh_jni.c - ${xhook_source_dir}/xh_log.c - ${xhook_source_dir}/xh_util.c - ${xhook_source_dir}/xh_version.c - ${xhook_source_dir}/xh_maps.c - ${xhook_source_dir}/xhook.c - ${xhook_source_dir}/xhook_ext.c) +set( + xhook_source + ${xhook_source_dir}/xh_core.c + ${xhook_source_dir}/xh_elf.c + ${xhook_source_dir}/xh_jni.c + ${xhook_source_dir}/xh_log.c + ${xhook_source_dir}/xh_util.c + ${xhook_source_dir}/xh_version.c + ${xhook_source_dir}/xh_maps.c + ${xhook_source_dir}/xhook.c + ${xhook_source_dir}/xhook_ext.c +) add_library(xhook STATIC ${xhook_source}) -find_library( - log-lib - log -) +find_library(log-lib log) target_include_directories( - xhook - PUBLIC ${xhook_source_dir} + xhook + PUBLIC ${xhook_source_dir} + PUBLIC ${xhook_source_dir}/../libsemi_dlfcn ) target_link_libraries( - xhook - ${log-lib} -) - + xhook + PUBLIC ${log-lib} + PUBLIC semi_dlfcn +) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_core.c b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_core.c index 23b645b6c..892b41fe1 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_core.c +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_core.c @@ -32,6 +32,7 @@ #include #include #include +#include #include "queue.h" #include "tree.h" #include "xh_errno.h" @@ -133,9 +134,11 @@ static void xh_core_init_request_group(xh_core_request_group_t* request_group, i //required info from /proc/self/maps typedef struct xh_core_map_info { - char *pathname; - uintptr_t base_addr; - xh_elf_t elf; + char* pathname; + uintptr_t bias_addr; + ElfW(Phdr)* phdrs; + ElfW(Half) phdr_count; + xh_elf_t elf; RB_ENTRY(xh_core_map_info) link; } xh_core_map_info_t; static __inline__ int xh_core_map_info_cmp(xh_core_map_info_t *a, xh_core_map_info_t *b) @@ -387,7 +390,7 @@ static void xh_core_hook_impl_with_spec_info(xh_core_map_info_t *mi, static void xh_core_hook_impl(xh_core_map_info_t *mi) { //init - if(0 != xh_elf_init(&(mi->elf), mi->base_addr, mi->pathname)) return; + if(0 != xh_elf_init(&(mi->elf), mi->bias_addr, mi->phdrs, mi->phdr_count, mi->pathname)) return; xh_core_hook_impl_with_spec_info(mi, &xh_core_hook_info, &xh_core_ignore_info); @@ -463,24 +466,18 @@ static int is_current_pathname_matches_any_requests(xh_core_hook_info_queue_t *h return match; } -static int xh_core_maps_iterate_cb(void* data, uintptr_t start, uintptr_t end, char* perms, int offset, char* pathname) +static int xh_core_maps_iterate_cb(struct dl_phdr_info* info, size_t info_size, void* data) { xh_core_map_info_tree_t* map_info_refreshed = (xh_core_map_info_tree_t*) data; - //check permission - if (perms[0] != 'r') return 0; - if (perms[3] != 'p') return 0; //do not touch the shared memory - - //check offset - // - //We are trying to find ELF header in memory. - //It can only be found at the beginning of a mapped memory regions - //whose offset is 0. - if(0 != offset) return 0; - - if('[' == pathname[0]) return 0; + const char* pathname = info->dlpi_name; //check pathname + if (pathname[0] == '[') { + XH_LOG_DEBUG("'%s' is not a lib, skip it.", pathname); + return 0; + } + //if we need to hook this elf? if (!is_current_pathname_matches_any_requests(&xh_core_hook_info, &xh_core_ignore_info, pathname)) { @@ -504,18 +501,9 @@ static int xh_core_maps_iterate_cb(void* data, uintptr_t start, uintptr_t end, c check_finished: XH_LOG_INFO("'%s' matches hook request, do further checks.", pathname); - if (0 == xh_check_loaded_so((void *) start)) { - XH_LOG_ERROR("%p is not loaded by linker %s", (void *) start, pathname); - return 0; // do not touch the so that not loaded by linker - } - - //check elf header format - //We are trying to do ELF header checking as late as possible. - if(0 != xh_core_check_elf_header(start, pathname)) return 0; - //check existed map item xh_core_map_info_t mi_key; - mi_key.pathname = pathname; + mi_key.pathname = (char*) pathname; xh_core_map_info_t* mi; if(NULL != (mi = RB_FIND(xh_core_map_info_tree, &xh_core_map_info, &mi_key))) { @@ -532,9 +520,11 @@ static int xh_core_maps_iterate_cb(void* data, uintptr_t start, uintptr_t end, c } //re-hook if base_addr changed - if(mi->base_addr != start) + if(mi->bias_addr != info->dlpi_addr) { - mi->base_addr = start; + mi->bias_addr = info->dlpi_addr; + mi->phdrs = (ElfW(Phdr)*) info->dlpi_phdr; + mi->phdr_count = info->dlpi_phnum; xh_core_hook(mi); } } @@ -547,7 +537,9 @@ static int xh_core_maps_iterate_cb(void* data, uintptr_t start, uintptr_t end, c free(mi); return 0; } - mi->base_addr = start; + mi->bias_addr = info->dlpi_addr; + mi->phdrs = (ElfW(Phdr)*) info->dlpi_phdr; + mi->phdr_count = info->dlpi_phnum; //repeated? //We only keep the first one, that is the real base address @@ -568,10 +560,8 @@ static void xh_core_refresh_impl() { pthread_rwlock_rdlock(&xh_core_refresh_blocker); - xh_maps_invalidate(); - xh_core_map_info_tree_t map_info_refreshed = RB_INITIALIZER(&map_info_refreshed); - xh_maps_iterate((xh_maps_iterate_cb_t) xh_core_maps_iterate_cb, &map_info_refreshed); + semi_dl_iterate_phdr((iterate_callback) xh_core_maps_iterate_cb, &map_info_refreshed); xh_core_map_info_t* mi; xh_core_map_info_t* mi_tmp; @@ -801,106 +791,69 @@ void xh_core_enable_sigsegv_protection(int flag) xh_core_sigsegv_enable = (flag ? 1 : 0); } -void* xh_core_elf_open(const char *path_suffix) { - char line[512]; - FILE* fp; - uintptr_t base_addr; - char perm[5]; - unsigned long offset; - int pathname_pos; - char *pathname; - size_t pathname_len; - xh_core_map_info_t *mi; - size_t path_suffix_len; - int found; - - if (path_suffix == NULL) - { - return NULL; - } +typedef struct xh_single_so_iterate_args { + const char* path_suffix; + xh_core_map_info_t* mi; +} xh_single_so_iterate_args_t; - if(NULL == (fp = fopen("/proc/self/maps", "r"))) - { - XH_LOG_ERROR("fopen /proc/self/maps failed"); - return NULL; - } +static int xh_single_so_search_iterate_cb(struct dl_phdr_info* info, size_t info_size, void* data) { + xh_single_so_iterate_args_t* args = (xh_single_so_iterate_args_t*) data; - path_suffix_len = strlen(path_suffix); - if (path_suffix_len == 0) - { - fclose(fp); - return NULL; + const char* pathname = info->dlpi_name; + size_t path_len = strlen(pathname); + size_t path_suffix_len = strlen(args->path_suffix); + if (strncmp(pathname + path_len - path_suffix_len, args->path_suffix, path_suffix_len) != 0) { + // Continue to process next entry. + return 0; } - found = 0; - while(fgets(line, sizeof(line), fp)) - { - if(sscanf(line, "%"PRIxPTR"-%*lx %4s %lx %*x:%*x %*d%n", &base_addr, perm, &offset, &pathname_pos) != 3) continue; - - //check permission - if(perm[0] != 'r') continue; - if(perm[3] != 'p') continue; //do not touch the shared memory - - //check offset - // - //We are trying to find ELF header in memory. - //It can only be found at the beginning of a mapped memory regions - //whose offset is 0. - if(0 != offset) continue; - - //get pathname - while(isspace(line[pathname_pos]) && pathname_pos < (int)(sizeof(line) - 1)) - pathname_pos += 1; - if(pathname_pos >= (int)(sizeof(line) - 1)) continue; - pathname = line + pathname_pos; - pathname_len = strlen(pathname); - if(0 == pathname_len) continue; - if(pathname[pathname_len - 1] == '\n') - { - pathname[pathname_len - 1] = '\0'; - pathname_len -= 1; - } - if(0 == pathname_len) continue; - if('[' == pathname[0]) continue; - if (path_suffix_len > pathname_len) continue; - - if (strncmp(pathname + pathname_len - path_suffix_len, path_suffix, path_suffix_len) != 0) - { - continue; - } - - if (0 != xh_core_check_elf_header(base_addr, pathname)) continue; + int check_elf_ret = xh_core_check_elf_header(info->dlpi_addr, pathname); + if (0 != check_elf_ret) { + XH_LOG_ERROR("Fail to check elf header: %s, ret: %d.", pathname, check_elf_ret); + // Continue to process next entry. + return 0; + } - found = 1; - break; + args->mi->pathname = strdup(pathname); + if (args->mi->pathname == NULL) { + XH_LOG_ERROR("Fail to allocate memory to store path: %s.", pathname); + return -1; } + args->mi->bias_addr = info->dlpi_addr; + args->mi->phdrs = (ElfW(Phdr)*) info->dlpi_phdr; + args->mi->phdr_count = info->dlpi_phnum; + return 1; +} - if (found != 1) - { - fclose(fp); +void* xh_core_elf_open(const char *path_suffix) { + if (path_suffix == NULL) { + XH_LOG_ERROR("path_suffix is null."); return NULL; } - - mi = malloc(sizeof(xh_core_map_info_t)); - if (mi == NULL) - { - fclose(fp); + xh_core_map_info_t* mi = malloc(sizeof(xh_core_map_info_t)); + if (mi == NULL) { + XH_LOG_ERROR("Fail to allocate memory."); return NULL; } memset(mi, 0, sizeof(xh_core_map_info_t)); - if ((mi->pathname = strdup(pathname)) == NULL) - { - fclose(fp); + xh_single_so_iterate_args_t iter_args = { + .path_suffix = path_suffix, + .mi = mi + }; + int iter_ret = semi_dl_iterate_phdr(xh_single_so_search_iterate_cb, &iter_args); + if (iter_ret > 0) { + XH_LOG_INFO("Open so with path suffix %s successfully, realpath: %s.", path_suffix, mi->pathname); + return mi; + } else { + if (mi->pathname != NULL) { + free(mi->pathname); + mi->pathname = NULL; + } free(mi); - mi = NULL; + XH_LOG_ERROR("Fail to open %s", path_suffix); return NULL; } - - mi->base_addr = base_addr; - fclose(fp); - - return mi; } static int xh_core_hook_single_sym_impl(xh_core_map_info_t *mi, const char *symbol, void *new_func, @@ -914,7 +867,7 @@ static int xh_core_hook_single_sym_impl(xh_core_map_info_t *mi, const char *symb } //init - ret = xh_elf_init(&(mi->elf), mi->base_addr, mi->pathname); + ret = xh_elf_init(&(mi->elf), mi->bias_addr, mi->phdrs, mi->phdr_count, mi->pathname); if (ret != 0) { return ret; diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_elf.c b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_elf.c index 85cbf8af5..bce5ce804 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_elf.c +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_elf.c @@ -771,20 +771,27 @@ static void xh_elf_dump(xh_elf_t *self) #endif -int xh_elf_init(xh_elf_t *self, uintptr_t base_addr, const char *pathname) +int xh_elf_init(xh_elf_t *self, uintptr_t bias_addr, ElfW(Phdr)* phdrs, ElfW(Half) phdr_count, const char *pathname) { - if(0 == base_addr || NULL == pathname) return XH_ERRNO_INVAL; + if(0 == bias_addr || NULL == pathname) return XH_ERRNO_INVAL; //always reset memset(self, 0, sizeof(xh_elf_t)); self->pathname = pathname; - self->base_addr = (ElfW(Addr))base_addr; - self->ehdr = (ElfW(Ehdr) *)base_addr; - self->phdr = (ElfW(Phdr) *)(base_addr + self->ehdr->e_phoff); //segmentation fault sometimes + self->bias_addr = (ElfW(Addr)) bias_addr; + self->phdr = phdrs; - //find the first load-segment with offset 0 - ElfW(Phdr) *phdr0 = xh_elf_get_first_segment_by_type_offset(self, PT_LOAD, 0); + XH_LOG_DEBUG("xh_elf_init: pathname: %s, phdr: %p, phdr_count: %u", pathname, phdrs, phdr_count); + + ElfW(Phdr)* phdr0 = NULL; + for (int i = 0; i < phdr_count; ++i) { + ElfW(Phdr)* phdr = phdrs + i; + if (phdr->p_type == PT_LOAD) { + phdr0 = phdr; + break; + } + } if(NULL == phdr0) { XH_LOG_ERROR("Can NOT found the first load segment. %s", pathname); @@ -797,9 +804,9 @@ int xh_elf_init(xh_elf_t *self, uintptr_t base_addr, const char *pathname) (void *)(phdr0->p_vaddr), pathname); #endif - //save load bias addr + self->base_addr = self->bias_addr + phdr0->p_vaddr; if(self->base_addr < phdr0->p_vaddr) return XH_ERRNO_FORMAT; - self->bias_addr = self->base_addr - phdr0->p_vaddr; + self->ehdr = (ElfW(Ehdr) *) self->base_addr; //find dynamic-segment ElfW(Phdr) *dhdr = xh_elf_get_first_segment_by_type(self, PT_DYNAMIC); diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_elf.h b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_elf.h index b73312089..9670afe22 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_elf.h +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_elf.h @@ -73,7 +73,7 @@ typedef struct int is_use_gnu_hash; } xh_elf_t; -int xh_elf_init(xh_elf_t *self, uintptr_t base_addr, const char *pathname); +int xh_elf_init(xh_elf_t *self, uintptr_t bias_addr, ElfW(Phdr)* phdrs, ElfW(Half) phdr_count, const char *pathname); int xh_elf_hook(xh_elf_t *self, const char *symbol, void *new_func, void **old_func); int xh_elf_check_elfheader(uintptr_t base_addr); diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_log.c b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_log.c index ee7ce70bb..0359417fe 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_log.c +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_log.c @@ -24,5 +24,10 @@ #include #include "xh_log.h" -android_LogPriority xh_log_priority = ANDROID_LOG_WARN; +#ifdef EnableLOG +int enable_log = 1; +#else int enable_log = 0; +#endif + +android_LogPriority xh_log_priority = ANDROID_LOG_DEBUG; diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_log.h b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_log.h index 06ce20abc..b8a8ee615 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_log.h +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_log.h @@ -35,15 +35,15 @@ extern android_LogPriority xh_log_priority; extern int enable_log; #define XH_LOG_TAG "xhook" -//#define XH_LOG_DEBUG(fmt, ...) do{if(xh_log_priority <= ANDROID_LOG_DEBUG) __android_log_print(ANDROID_LOG_DEBUG, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) -//#define XH_LOG_INFO(fmt, ...) do{if(xh_log_priority <= ANDROID_LOG_INFO) __android_log_print(ANDROID_LOG_INFO, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) -//#define XH_LOG_WARN(fmt, ...) do{if(xh_log_priority <= ANDROID_LOG_WARN) __android_log_print(ANDROID_LOG_WARN, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) -//#define XH_LOG_ERROR(fmt, ...) do{if(xh_log_priority <= ANDROID_LOG_ERROR) __android_log_print(ANDROID_LOG_ERROR, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) - -#define XH_LOG_DEBUG(fmt, ...) do{if(enable_log) __android_log_print(ANDROID_LOG_DEBUG, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) -#define XH_LOG_INFO(fmt, ...) do{if(enable_log) __android_log_print(ANDROID_LOG_INFO, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) -#define XH_LOG_WARN(fmt, ...) do{if(enable_log) __android_log_print(ANDROID_LOG_WARN, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) -#define XH_LOG_ERROR(fmt, ...) do{if(enable_log) __android_log_print(ANDROID_LOG_ERROR, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +#define XH_LOG_DEBUG(fmt, ...) do{if(enable_log && (xh_log_priority <= ANDROID_LOG_DEBUG)) __android_log_print(ANDROID_LOG_DEBUG, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +#define XH_LOG_INFO(fmt, ...) do{if(enable_log && (xh_log_priority <= ANDROID_LOG_INFO)) __android_log_print(ANDROID_LOG_INFO, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +#define XH_LOG_WARN(fmt, ...) do{if(enable_log && (xh_log_priority <= ANDROID_LOG_WARN)) __android_log_print(ANDROID_LOG_WARN, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +#define XH_LOG_ERROR(fmt, ...) do{if(enable_log && (xh_log_priority <= ANDROID_LOG_ERROR)) __android_log_print(ANDROID_LOG_ERROR, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) + +// #define XH_LOG_DEBUG(fmt, ...) do{if(enable_log) __android_log_print(ANDROID_LOG_DEBUG, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +// #define XH_LOG_INFO(fmt, ...) do{if(enable_log) __android_log_print(ANDROID_LOG_INFO, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +// #define XH_LOG_WARN(fmt, ...) do{if(enable_log) __android_log_print(ANDROID_LOG_WARN, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +// #define XH_LOG_ERROR(fmt, ...) do{if(enable_log) __android_log_print(ANDROID_LOG_ERROR, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) #define XH_LOG_ABORT(fmt, ...) do{ __android_log_assert(NULL, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_util.c b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_util.c index 97fe11c69..21192f0cb 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_util.c +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xh_util.c @@ -52,7 +52,10 @@ static int xh_util_get_mem_protect_maps_iter_cb(void* data, uintptr_t start, uin int* load0 = (int*) ((void**) data)[4]; int* found_all = (int*) ((void**) data)[5]; - if (pathname != NULL && strcmp(expected_pathname, pathname) != 0) return 0; + // Use addr to locate map entry is enough. + // Use pathname to locate map entry may fail if expected map entry was mapped directly from apk. In this case + // map entry name is always the apk path while our expect name is the so path. + // if (pathname != NULL && strcmp(expected_pathname, pathname) != 0) return 0; if(perms[3] != 'p') return 0; diff --git a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xhook_ext.c b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xhook_ext.c index eb909b4e5..aeeb89610 100644 --- a/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xhook_ext.c +++ b/matrix/matrix-android/matrix-android-commons/src/main/cpp/libxhook/xhook_ext.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "xhook_ext.h" #include "xh_core.h" #include "xh_elf.h" @@ -62,13 +63,20 @@ void xhook_elf_close(void *h_lib) xh_core_elf_close(h_lib); } -static int xh_export_symtable_hook(const char* pathname, const void* base_addr, const char* symbol_name, void* handler, - void** original_address) { +static int xh_export_symtable_hook( + const char* pathname, + const void* bias_addr, + ElfW(Phdr)* phdrs, + ElfW(Half) phdr_count, + const char* symbol_name, + void* handler, + void** original_address +) { if(NULL == symbol_name || NULL == handler) return XH_ERRNO_INVAL; xh_elf_t self = {}; { - int error = xh_elf_init(&self, (uintptr_t) base_addr, pathname); + int error = xh_elf_init(&self, (uintptr_t) bias_addr, phdrs, phdr_count, pathname); if (error != 0) return error; } XH_LOG_INFO("hooking %s in %s using export table hook.\n", symbol_name, pathname); @@ -107,97 +115,79 @@ static int xh_export_symtable_hook(const char* pathname, const void* base_addr, return 0; } -static int xhook_find_library_base_addr(const char* owner_lib_name, char path_name_out[PATH_MAX + 1], const void** base_addr_out) { - FILE* fp = fopen("/proc/self/maps", "r"); - if(fp == NULL) { - XH_LOG_ERROR("fopen /proc/self/maps failed"); - return XH_ERRNO_ELFINIT; +typedef struct found_lib_info { + struct { + const char* owner_lib_name; + } args_in; + + struct { + char path_name[PATH_MAX + 1]; + const void* bias_addr; + ElfW(Phdr)* phdrs; + ElfW(Half) phdr_count; + } args_out; +} found_lib_info_t; + +#define MAPS_ITER_RET_NOTFOUND 0 +#define MAPS_ITER_RET_FOUND 1 + +static int find_owner_library_cb(struct dl_phdr_info* info, size_t info_size, void* data) { + found_lib_info_t* found_lib_info = (found_lib_info_t*) data; + + const char* owner_lib_name = found_lib_info->args_in.owner_lib_name; + size_t owner_name_len = strlen(owner_lib_name); + if (owner_name_len == 0) return MAPS_ITER_RET_NOTFOUND; + + char real_suffix[PATH_MAX + 1]; + if (owner_lib_name[0] != '/') { + real_suffix[0] = '/'; + strncpy(real_suffix + 1, owner_lib_name, PATH_MAX); + ++owner_name_len; + } else { + strncpy(real_suffix, owner_lib_name, PATH_MAX); } - - char line[512] = {}; - while(fgets(line, sizeof(line), fp)) { - uintptr_t base_addr = 0; - char perm[5] = {}; - unsigned long offset = 0; - int pathname_pos = 0; - if (sscanf(line, "%"PRIxPTR"-%*lx %4s %lx %*x:%*x %*d%n", &base_addr, perm, &offset, &pathname_pos) != 3) { - continue; - } - - //check permission - if (perm[0] != 'r') continue; - if (perm[3] != 'p') continue; //do not touch the shared memory - - //check offset - // - //We are trying to find ELF header in memory. - //It can only be found at the beginning of a mapped memory regions - //whose offset is 0. - if (0 != offset) continue; - - // Skip normal mmapped region. - // Some libraries may mmap specific elf file into memory for - // accessing as a normal file. - { - Dl_info info; - if (dladdr((void*) base_addr, &info) == 0) { - continue; - } - } - - //get pathname - while (isspace(line[pathname_pos]) && pathname_pos < (int) (sizeof(line) - 1)) { - pathname_pos += 1; - } - if (pathname_pos >= (int) (sizeof(line) - 1)) continue; - char* pathname = line + pathname_pos; - size_t pathname_len = strlen(pathname); - if (0 == pathname_len) continue; - if (pathname[pathname_len - 1] == '\n') { - pathname[pathname_len - 1] = '\0'; - pathname_len -= 1; - } - if (0 == pathname_len) continue; - if ('[' == pathname[0]) continue; - - //check pathname - //if we need to hook this elf? - char real_suffix[PATH_MAX + 1] = {}; - size_t real_suffix_len = snprintf(real_suffix, sizeof(real_suffix), "/%s", owner_lib_name); - if (pathname_len < real_suffix_len) { - continue; - } - if (strncmp(pathname + pathname_len - real_suffix_len, real_suffix, real_suffix_len) != 0) { - continue; - } - - //check elf header format - //We are trying to do ELF header checking as late as possible. - if (0 != xh_elf_check_elfheader(base_addr)) continue; - - XH_LOG_DEBUG("found library, owner_lib_name: %s, path: %s, base: %" PRIxPTR, - owner_lib_name, pathname, base_addr); - - if (path_name_out != NULL) { - strncpy(path_name_out, pathname, PATH_MAX); - } - if (base_addr_out != NULL) { - *base_addr_out = (const void*) base_addr; - } - fclose(fp); - return 0; + if (owner_name_len > PATH_MAX) owner_name_len = PATH_MAX; + real_suffix[owner_name_len] = '\0'; + + XH_LOG_DEBUG("find_owner_library_cb: curr_pathname: %s, real_suffix: %s", info->dlpi_name, real_suffix); + + size_t curr_pathname_len = strlen(info->dlpi_name); + if (strncmp(info->dlpi_name + curr_pathname_len - owner_name_len, real_suffix, owner_name_len) == 0) { + strcpy(found_lib_info->args_out.path_name, info->dlpi_name); + found_lib_info->args_out.bias_addr = (const void*) info->dlpi_addr; + found_lib_info->args_out.phdrs = (ElfW(Phdr)*) info->dlpi_phdr; + found_lib_info->args_out.phdr_count = info->dlpi_phnum; + XH_LOG_INFO("Found owner lib '%s' by suffix '%s'.", info->dlpi_name, real_suffix); + return MAPS_ITER_RET_FOUND; + } else { + return MAPS_ITER_RET_NOTFOUND; } - fclose(fp); - return XH_ERRNO_NOTFND; } int xhook_export_symtable_hook(const char* owner_lib_name, const char* symbol_name, void* handler, void** original_address) { - char path_name[PATH_MAX + 1] = {}; - const void* base_addr = NULL; - if (xhook_find_library_base_addr(owner_lib_name, path_name, &base_addr) == 0) { - return xh_export_symtable_hook(path_name, base_addr, symbol_name, handler, original_address); - } else { - return XH_ERRNO_NOTFND; + found_lib_info_t found_lib_info = {}; + found_lib_info.args_in.owner_lib_name = owner_lib_name; + switch (semi_dl_iterate_phdr(find_owner_library_cb, &found_lib_info)) { + case MAPS_ITER_RET_FOUND: { + return xh_export_symtable_hook( + found_lib_info.args_out.path_name, + found_lib_info.args_out.bias_addr, + found_lib_info.args_out.phdrs, + found_lib_info.args_out.phdr_count, + symbol_name, + handler, + original_address + ); + } + case MAPS_ITER_RET_NOTFOUND: { + return XH_ERRNO_NOTFND; + } + case XH_ERRNO_NOMEM: { + return XH_ERRNO_NOMEM; + } + default: { + return XH_ERRNO_UNKNOWN; + } } } \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/build.gradle b/matrix/matrix-android/matrix-android-lib/build.gradle index 63d453515..176b6b377 100644 --- a/matrix/matrix-android/matrix-android-lib/build.gradle +++ b/matrix/matrix-android/matrix-android-lib/build.gradle @@ -1,14 +1,18 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion + useLibrary 'android.test.base' + defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName rootProject.ext.VERSION_NAME + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } buildTypes { release { @@ -20,11 +24,14 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + api 'androidx.lifecycle:lifecycle-common:2.3.1' testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.annotation:annotation:1.0.0' - api 'androidx.annotation:annotation:1.0.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + androidTestImplementation "org.mockito:mockito-core:2.8.9" + androidTestImplementation "org.mockito:mockito-android:2.8.9" } version = rootProject.ext.VERSION_NAME @@ -36,10 +43,8 @@ if (rootProject.file('gradle/WeChatPublish.gradle').exists()) { }else { //uploading to WeChat maven repo apply from: rootProject.file('gradle/WeChatPublish.gradle') - apply from: rootProject.file('gradle/WeChatNativeDepend.gradle') wechatPublish { artifactId=POM_ARTIFACT_ID } } } - diff --git a/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ExampleInstrumentedTest.java b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ExampleInstrumentedTest.java index d8617eb9a..fe3003b81 100644 --- a/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ExampleInstrumentedTest.java +++ b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ExampleInstrumentedTest.java @@ -35,7 +35,7 @@ public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.tencent.matrix.test", appContext.getPackageName()); } diff --git a/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/FibonacciTest.kt b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/FibonacciTest.kt new file mode 100644 index 000000000..590782f72 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/FibonacciTest.kt @@ -0,0 +1,36 @@ +package com.tencent.matrix + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.math.min + +/** + * Created by Yves on 2021/11/25 + */ +@RunWith(AndroidJUnit4::class) +class FibonacciTest { + + companion object { + private const val TAG = "FibonacciTest" + } + + class FibonacciInterval(private val maxVal: Long) { + private var n = 34L // 10th + private var m = 55L // 11th + + fun next(): Long { + return min((n + m).also { n = m; m = it }, maxVal) + } + } + + @Test + fun test() { + val f = FibonacciInterval(60 * 1000L) + + for (i in 0..25) { + Log.d(TAG, "${f.next()}") + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ShadowStatefulOwnerTest.kt b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ShadowStatefulOwnerTest.kt new file mode 100644 index 000000000..0889603be --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ShadowStatefulOwnerTest.kt @@ -0,0 +1,82 @@ +package com.tencent.matrix + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.tencent.matrix.lifecycle.IStateObserver +import com.tencent.matrix.lifecycle.StatefulOwner +import com.tencent.matrix.lifecycle.reverse +import com.tencent.matrix.lifecycle.shadow +import org.junit.runner.RunWith +import org.junit.Test + +/** + * Created by Yves on 2022/1/7 + */ +@RunWith(AndroidJUnit4::class) +class ShadowStatefulOwnerTest { + + companion object { + private const val TAG = "ShadowStatefulOwnerTest" + } + + @Test + fun test() { + val origin = object : StatefulOwner() { + fun on() = turnOn() + fun off() = turnOff() + } + + + val shadow = origin.shadow() + val reverse = origin.reverse() + + origin.observeForever(object :IStateObserver { + override fun on() { + Log.d(TAG, "origin on") +// origin.removeObserver(this) + } + + override fun off() { + Log.d(TAG, "origin off") +// origin.removeObserver(this) + } + }) + + shadow.observeForever(object :IStateObserver { + override fun on() { + Log.d(TAG, "shadow on") +// shadow.removeObserver(this) + } + + override fun off() { + Log.d(TAG, "shadow off") +// shadow.removeObserver(this) + } + }) + + + reverse.observeForever(object :IStateObserver { + override fun on() { + Log.d(TAG, "reverse on") + reverse.removeObserver(this) + } + + override fun off() { + Log.d(TAG, "reverse off") + reverse.removeObserver(this) + } + }) + + origin.on() + origin.off() + + Log.d(TAG, "============") + + origin.on() + origin.off() + + Log.d(TAG, "done") + + + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/StatefulOwnerTest.kt b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/StatefulOwnerTest.kt new file mode 100644 index 000000000..db2d09551 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/StatefulOwnerTest.kt @@ -0,0 +1,131 @@ +package com.tencent.matrix + +import android.os.SystemClock +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.tencent.matrix.lifecycle.* +import com.tencent.matrix.util.MatrixLog +import junit.framework.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Created by Yves on 2021/11/11 + */ +@RunWith(AndroidJUnit4::class) +class StatefulOwnerTest { + + companion object { + private const val TAG = "Matrix.test.StatefulOwnerTest" + } + + @Test + fun testRemoveSource() { + + class TestMsOwner: MultiSourceStatefulOwner(ReduceOperators.OR) + + class TestStatefulOwner: StatefulOwner() { + fun handleOn() = turnOn() + fun handleOff() = turnOff() + } + + val msOwner = TestMsOwner() + msOwner.observeForever(object : IStateObserver { + override fun on() { + MatrixLog.d(TAG, "test ON") + } + + override fun off() { + MatrixLog.d(TAG, "test OFF") + } + + }) + assertEquals(msOwner.active(), false) + + val s1 = TestStatefulOwner().apply { + handleOn() + MatrixLog.d(TAG, "add s1") + msOwner.addSourceOwner(this) + } + + assertEquals(msOwner.active(), true) + + val s2 = TestStatefulOwner().apply { + handleOff() + MatrixLog.d(TAG, "add s2") + msOwner.addSourceOwner(this) + } + + assertEquals(msOwner.active(), true) + + MatrixLog.d(TAG, "remove s1") + msOwner.removeSourceOwner(s1) + + assertEquals(msOwner.active(), false) + + MatrixLog.d(TAG, "turn off s2") + s2.handleOff() + + } + + @Test + fun scheduleTest() { + val o1 = object : StatefulOwner(true) { + fun handleOn() = turnOn() + fun handleOff() = turnOff() + } + + o1.observeForever(object : IStateObserver { + override fun on() { + Log.d(TAG, "on: normal observe at ${Thread.currentThread().name}") + } + + override fun off() { + Log.d(TAG, "off: normal observe at ${Thread.currentThread().name}") + } + }) + + o1.observeForever(object : ISerialObserver { + override fun on() { + Log.d(TAG, "on: serial observe at ${Thread.currentThread().name} pool size = ${MatrixLifecycleThread.executor.poolSize}") + } + + override fun off() { + Log.d(TAG, "off: serial observe at ${Thread.currentThread().name} pool size = ${MatrixLifecycleThread.executor.poolSize}") + } + }) + + o1.handleOn() + o1.handleOff() + SystemClock.sleep(100) + o1.handleOn() + o1.handleOff() + SystemClock.sleep(100) + o1.handleOn() + o1.handleOff() + + val m1 = MultiSourceStatefulOwner(ReduceOperators.AND, o1) + m1.observeForever(object : IStateObserver { + override fun on() { + Log.d(TAG, "Multi on: normal observe at ${Thread.currentThread().name}") + } + + override fun off() { + Log.d(TAG, "Multi off: normal observe at ${Thread.currentThread().name}") + } + }) + m1.observeForever(object : ISerialObserver { + override fun on() { + Log.d(TAG, "Multi on: serial observe at ${Thread.currentThread().name} pool size = ${MatrixLifecycleThread.executor.poolSize}") + } + + override fun off() { + Log.d(TAG, "Multi off: serial observe at ${Thread.currentThread().name} pool size = ${MatrixLifecycleThread.executor.poolSize}") + } + }) + + o1.handleOn() + o1.handleOff() + } + +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ThreadTest.kt b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ThreadTest.kt new file mode 100644 index 000000000..14a89ee8d --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/androidTest/java/com/tencent/matrix/ThreadTest.kt @@ -0,0 +1,193 @@ +package com.tencent.matrix + +import android.os.SystemClock +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.tencent.matrix.lifecycle.MatrixLifecycleThread +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.* + +/** + * Created by Yves on 2022/1/17 + */ +@RunWith(AndroidJUnit4::class) +class ThreadTest { + companion object { + private const val TAG = "ThreadTest" + } + + @Test + fun executorTest() { + Log.d(TAG, "executorTest: - 1") + MatrixLifecycleThread.executor.execute { +// SystemClock.sleep(1000) + Log.d(TAG, "executorTest: 1 ${Thread.currentThread().name}") + while (true) {} + } + + SystemClock.sleep(1000) + + Log.d(TAG, "executorTest: - 2") + MatrixLifecycleThread.executor.execute { +// SystemClock.sleep(1000) + Log.d(TAG, "executorTest: 2 ${Thread.currentThread().name}") + while (true) {} + } + + SystemClock.sleep(1000) + + Log.d(TAG, "executorTest: - 3") + MatrixLifecycleThread.executor.execute { +// SystemClock.sleep(1000) + Log.d(TAG, "executorTest: 3 ${Thread.currentThread().name}") + while (true) {} + } + + SystemClock.sleep(1000) + + Log.d(TAG, "executorTest: - 4") + MatrixLifecycleThread.executor.execute { +// SystemClock.sleep(1000) + Log.d(TAG, "executorTest: 4 ${Thread.currentThread().name}") + while (true) {} + } + + SystemClock.sleep(1000) + + Log.d(TAG, "executorTest: - 5 ${Thread.currentThread().name}") + MatrixLifecycleThread.executor.execute { +// SystemClock.sleep(1000) + Log.d(TAG, "executorTest: 5 ${Thread.currentThread().name}") + while (true) {} + } + + SystemClock.sleep(1000) + + Log.d(TAG, "executorTest: - 6") + MatrixLifecycleThread.executor.execute { +// SystemClock.sleep(1000) + Log.d(TAG, "executorTest: 6 ${Thread.currentThread().name}") + while (true) {} + } + + + SystemClock.sleep(1000) + + Log.d(TAG, "executorTest: - 7") + MatrixLifecycleThread.executor.execute { +// SystemClock.sleep(1000) + Log.d(TAG, "executorTest: 7 ${Thread.currentThread().name}") + while (true) {} + } + + SystemClock.sleep(1000) + + Log.d(TAG, "executorTest: - 8") + MatrixLifecycleThread.executor.execute { +// SystemClock.sleep(1000) + Log.d(TAG, "executorTest: 8 ${Thread.currentThread().name}") + while (true) {} + } + + + + SystemClock.sleep(15000) + } + + @Test + fun forkJoinPoolTest() { + val pool = ForkJoinPool( + Runtime.getRuntime().availableProcessors(), + { pool -> + val thread = object : ForkJoinWorkerThread(pool) {} + thread.name = "matrix_di_${(pool?.poolSize ?: 0) + 1}" + Log.d(TAG, "forkJoinPoolTest: create ${thread.name}") + thread + }, + null, + true + ) + + for (i in 0..10) { + pool.execute { + SystemClock.sleep(10) + Log.d(TAG, "forkJoinPoolTest: task done") + } + } + + SystemClock.sleep(2) + + Log.d(TAG, "forkJoinPoolTest: ${pool.poolSize}") + + SystemClock.sleep(1000) + + Log.d(TAG, "forkJoinPoolTest: ${pool.poolSize}") + } + + @Test + fun executorTest2() { + val c = MatrixLifecycleThread.executor + + c.execute { + Log.d(TAG, "cachedThreadPoolTest: costly task") + SystemClock.sleep(1000 * 100L) + Log.d(TAG, "cachedThreadPoolTest: costly task done") + } + + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 2, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 3, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 4, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 5, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 6, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 7, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 8, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 9, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 10, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(510); Log.d(TAG, "cachedThreadPoolTest: task 11, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(20000); Log.d(TAG, "cachedThreadPoolTest: task 12, size = ${c.poolSize}, $this") } }) + + Log.d(TAG, "cachedThreadPoolTest: size 1 = ${c.poolSize}") + +// SystemClock.sleep(35 * 1000L) + + Log.d(TAG, "cachedThreadPoolTest: size 2 = ${c.poolSize}") + + SystemClock.sleep(15 * 1000L) + + Log.d(TAG, "cachedThreadPoolTest: size 3 = ${c.poolSize}") + + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 13, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 14, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 15, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 16, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 17, size = ${c.poolSize}, $this") } }) + c.execute (object : Runnable { override fun run() { SystemClock.sleep(10); Log.d(TAG, "cachedThreadPoolTest: task 18, size = ${c.poolSize}, $this") } }) + + SystemClock.sleep(60 * 1000L) + } + + @Test + fun executorTest3() { + val c = MatrixLifecycleThread.executor + for (i in 0..10) { + + SystemClock.sleep(1000L) + + c.execute { + SystemClock.sleep(1 * 1000L) + } + } + + SystemClock.sleep(30 * 1000L) + Log.d(TAG, "=======") + + for (i in 0..10) { + c.execute { + SystemClock.sleep(10); + } + } + + SystemClock.sleep(1000 * 1000L) + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/AndroidManifest.xml b/matrix/matrix-android/matrix-android-lib/src/main/AndroidManifest.xml index c8c224592..98508a696 100644 --- a/matrix/matrix-android/matrix-android-lib/src/main/AndroidManifest.xml +++ b/matrix/matrix-android/matrix-android-lib/src/main/AndroidManifest.xml @@ -2,7 +2,20 @@ xmlns:android="http://schemas.android.com/apk/res/android" > + + + + + + + + + + + diff --git a/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ISubordinateProxy.aidl b/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ISubordinateProxy.aidl new file mode 100644 index 000000000..74e3fbd5b --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ISubordinateProxy.aidl @@ -0,0 +1,13 @@ +// ISubordinateProxy.aidl +package com.tencent.matrix.lifecycle.supervisor; + +// Declare any non-default types here with import statements +import com.tencent.matrix.util.MemInfo; + +interface ISubordinateProxy { + void dispatchState(in String scene, in String stateName, in boolean state); + void dispatchKill(in String scene, in String targetProcess, in int targetPid); + void dispatchDeath(in String scene, in String targetProcess, in int targetPid, in boolean isLruKill); + + MemInfo getMemInfo(); +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ISupervisorProxy.aidl b/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ISupervisorProxy.aidl new file mode 100644 index 000000000..42120bda7 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ISupervisorProxy.aidl @@ -0,0 +1,21 @@ +// ISupervisorProxy.aidl +package com.tencent.matrix.lifecycle.supervisor; + +// Declare any non-default types here with import statements + +import com.tencent.matrix.lifecycle.supervisor.ProcessToken; +import com.tencent.matrix.lifecycle.supervisor.ISubordinateProxy; + +interface ISupervisorProxy { + void registerSubordinate(in ProcessToken[] tokens, in ISubordinateProxy subordinateProxy); + + void onStateChanged(in ProcessToken token); + + void onSceneChanged(in String scene); + + void onProcessKilled(in ProcessToken token); + void onProcessRescuedFromKill(in ProcessToken token); + void onProcessKillCanceled(in ProcessToken token); + + String getRecentScene(); +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ProcessToken.aidl b/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ProcessToken.aidl new file mode 100644 index 000000000..5e5190ff2 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/lifecycle/supervisor/ProcessToken.aidl @@ -0,0 +1,6 @@ +// ProcessToken.aidl +package com.tencent.matrix.lifecycle.supervisor; + +// Declare any non-default types here with import statements + +parcelable ProcessToken; \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/util/MemInfo.aidl b/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/util/MemInfo.aidl new file mode 100644 index 000000000..653e6eb45 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/aidl/com/tencent/matrix/util/MemInfo.aidl @@ -0,0 +1,6 @@ +// MemInfo.aidl +package com.tencent.matrix.util; + +// Declare any non-default types here with import statements + +parcelable MemInfo; \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/AppActiveMatrixDelegate.java b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/AppActiveMatrixDelegate.java index 2527a7b44..be9435aa2 100644 --- a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/AppActiveMatrixDelegate.java +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/AppActiveMatrixDelegate.java @@ -2,53 +2,26 @@ import android.app.Activity; import android.app.Application; -import android.content.ComponentCallbacks2; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.text.TextUtils; -import android.util.ArrayMap; import com.tencent.matrix.listeners.IAppForeground; -import com.tencent.matrix.util.MatrixHandlerThread; -import com.tencent.matrix.util.MatrixLog; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner; +import com.tencent.matrix.util.MatrixUtil; +/** + * use {@link ProcessUILifecycleOwner} instead + */ +@Deprecated public enum AppActiveMatrixDelegate { INSTANCE; private static final String TAG = "Matrix.AppActiveDelegate"; - private final Set listeners = new HashSet(); - private boolean isAppForeground = false; - private String visibleScene = "default"; - private Controller controller = new Controller(); - private boolean isInit = false; - private String currentFragmentName; - private Handler handler; public void init(Application application) { - if (isInit) { - MatrixLog.e(TAG, "has inited!"); - return; - } - this.isInit = true; - if (null != MatrixHandlerThread.getDefaultHandlerThread()) { - this.handler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper()); - } - application.registerComponentCallbacks(controller); - application.registerActivityLifecycleCallbacks(controller); } public String getCurrentFragmentName() { - return currentFragmentName; + return ProcessUILifecycleOwner.INSTANCE.getCurrentFragmentName(); } /** @@ -57,180 +30,38 @@ public String getCurrentFragmentName() { * @param fragmentName */ public void setCurrentFragmentName(String fragmentName) { - MatrixLog.i(TAG, "[setCurrentFragmentName] fragmentName:%s", fragmentName); - this.currentFragmentName = fragmentName; - updateScene(fragmentName); + ProcessUILifecycleOwner.INSTANCE.setCurrentFragmentName(fragmentName); } public String getVisibleScene() { - return visibleScene; - } - - private void onDispatchForeground(String visibleScene) { - if (isAppForeground || !isInit) { - return; - } - - MatrixLog.i(TAG, "onForeground... visibleScene[%s]", visibleScene); - handler.post(new Runnable() { - @Override - public void run() { - isAppForeground = true; - synchronized (listeners) { - for (IAppForeground listener : listeners) { - listener.onForeground(true); - } - } - } - }); - - } - - private void onDispatchBackground(String visibleScene) { - if (!isAppForeground || !isInit) { - return; - } - - MatrixLog.i(TAG, "onBackground... visibleScene[%s]", visibleScene); - - handler.post(new Runnable() { - @Override - public void run() { - isAppForeground = false; - synchronized (listeners) { - for (IAppForeground listener : listeners) { - listener.onForeground(false); - } - } - } - }); - - + return ProcessUILifecycleOwner.INSTANCE.getVisibleScene(); } + @Deprecated public boolean isAppForeground() { - return isAppForeground; + return ProcessUILifecycleOwner.INSTANCE.isProcessForeground(); } + /** + * use {@link ProcessUILifecycleOwner} instead: + * @param listener + */ + @Deprecated public void addListener(IAppForeground listener) { - synchronized (listeners) { - listeners.add(listener); - } + ProcessUILifecycleOwner.INSTANCE.addListener(listener); } + /** + * use {@link ProcessUILifecycleOwner} instead: + * @param listener + */ + @Deprecated public void removeListener(IAppForeground listener) { - synchronized (listeners) { - listeners.remove(listener); - } - } - - - private final class Controller implements Application.ActivityLifecycleCallbacks, ComponentCallbacks2 { - - @Override - public void onActivityStarted(Activity activity) { - updateScene(activity); - onDispatchForeground(getVisibleScene()); - } - - - @Override - public void onActivityStopped(Activity activity) { - if (getTopActivityName() == null) { - onDispatchBackground(getVisibleScene()); - } - } - - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - - } - - @Override - public void onActivityDestroyed(Activity activity) { - - } - - @Override - public void onActivityResumed(Activity activity) { - - } - - @Override - public void onActivityPaused(Activity activity) { - - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - - } - - @Override - public void onLowMemory() { - - } - - @Override - public void onTrimMemory(int level) { - MatrixLog.i(TAG, "[onTrimMemory] level:%s", level); - if (level == TRIM_MEMORY_UI_HIDDEN && isAppForeground) { // fallback - onDispatchBackground(visibleScene); - } - } - } - - private void updateScene(Activity activity) { - visibleScene = activity.getClass().getName(); - } - - private void updateScene(String currentFragmentName) { - StringBuilder ss = new StringBuilder(); - ss.append(TextUtils.isEmpty(currentFragmentName) ? "?" : currentFragmentName); - visibleScene = ss.toString(); + ProcessUILifecycleOwner.INSTANCE.removeListener(listener); } + @Deprecated public static String getTopActivityName() { - long start = System.currentTimeMillis(); - try { - Class activityThreadClass = Class.forName("android.app.ActivityThread"); - Object activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null); - Field activitiesField = activityThreadClass.getDeclaredField("mActivities"); - activitiesField.setAccessible(true); - - Map activities; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - activities = (HashMap) activitiesField.get(activityThread); - } else { - activities = (ArrayMap) activitiesField.get(activityThread); - } - if (activities.size() < 1) { - return null; - } - for (Object activityRecord : activities.values()) { - Class activityRecordClass = activityRecord.getClass(); - Field pausedField = activityRecordClass.getDeclaredField("paused"); - pausedField.setAccessible(true); - if (!pausedField.getBoolean(activityRecord)) { - Field activityField = activityRecordClass.getDeclaredField("activity"); - activityField.setAccessible(true); - Activity activity = (Activity) activityField.get(activityRecord); - return activity.getClass().getName(); - } - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - long cost = System.currentTimeMillis() - start; - MatrixLog.d(TAG, "[getTopActivityName] Cost:%s", cost); - } - return null; + return MatrixUtil.getTopActivityName(); } - } diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/Matrix.java b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/Matrix.java index 42f62f0e4..d82522b91 100644 --- a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/Matrix.java +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/Matrix.java @@ -18,6 +18,9 @@ import android.app.Application; +import com.tencent.matrix.lifecycle.MatrixLifecycleConfig; +import com.tencent.matrix.lifecycle.MatrixLifecycleOwnerInitializer; +import com.tencent.matrix.lifecycle.supervisor.ProcessSupervisor; import com.tencent.matrix.plugin.DefaultPluginListener; import com.tencent.matrix.plugin.Plugin; import com.tencent.matrix.plugin.PluginListener; @@ -32,23 +35,19 @@ public class Matrix { private static final String TAG = "Matrix.Matrix"; - private static volatile Matrix sInstance; private final HashSet plugins; - private final Application application; - private final PluginListener pluginListener; + private final Application application; - private Matrix(Application app, PluginListener listener, HashSet plugins) { + private Matrix(Application app, PluginListener listener, HashSet plugins, MatrixLifecycleConfig config) { this.application = app; - this.pluginListener = listener; this.plugins = plugins; - AppActiveMatrixDelegate.INSTANCE.init(application); + MatrixLifecycleOwnerInitializer.init(app, config); + ProcessSupervisor.INSTANCE.init(app, config.getSupervisorConfig()); for (Plugin plugin : plugins) { - plugin.init(application, pluginListener); - pluginListener.onInit(plugin); + plugin.init(application, listener); } - } public static void setLogIml(MatrixLog.MatrixLogImp imp) { @@ -127,9 +126,16 @@ public T getPluginByClass(Class pluginClass) { public static class Builder { private final Application application; - private PluginListener pluginListener; - private HashSet plugins = new HashSet<>(); + private PluginListener pluginListener; + + private MatrixLifecycleConfig mLifecycleConfig = new MatrixLifecycleConfig(); // default config + +// private SupervisorConfig supervisorConfig; +// private boolean enableFgServiceMonitor; +// private boolean enableOverlayWindowMonitor; + + private final HashSet plugins = new HashSet<>(); public Builder(Application app) { if (app == null) { @@ -154,11 +160,16 @@ public Builder pluginListener(PluginListener pluginListener) { return this; } + public Builder matrixLifecycleConfig(MatrixLifecycleConfig config) { + this.mLifecycleConfig = config; + return this; + } + public Matrix build() { if (pluginListener == null) { pluginListener = new DefaultPluginListener(application); } - return new Matrix(application, pluginListener, plugins); + return new Matrix(application, pluginListener, plugins, mLifecycleConfig); } } diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/EmptyActivityLifecycleCallbacks.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/EmptyActivityLifecycleCallbacks.kt new file mode 100644 index 000000000..1358930ac --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/EmptyActivityLifecycleCallbacks.kt @@ -0,0 +1,31 @@ +package com.tencent.matrix.lifecycle + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +/** + * Created by Yves on 2021/9/22 + */ +open class EmptyActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks{ + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + + override fun onActivityStarted(activity: Activity) { + } + + override fun onActivityResumed(activity: Activity) { + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStopped(activity: Activity) { + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityDestroyed(activity: Activity) { + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleApi.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleApi.kt new file mode 100644 index 000000000..341145cdc --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleApi.kt @@ -0,0 +1,108 @@ +package com.tencent.matrix.lifecycle + +import androidx.lifecycle.LifecycleOwner + +@Deprecated("") +abstract class IMatrixLifecycleCallback { + internal var stateObserver: IStateObserver? = null + abstract fun onForeground() + abstract fun onBackground() +} + +abstract class IMatrixForegroundCallback { + internal var stateObserver: IStateObserver? = null + abstract fun onEnterForeground() + + /** + * NOTICE: do NOT always mean background!!!, depending on the observed owner + */ + abstract fun onExitForeground() +} + +abstract class IMatrixBackgroundCallback { + internal var stateObserver: IStateObserver? = null + abstract fun onEnterBackground() + + /** + * NOTICE: do NOT always mean foreground!!!, depending on the observed owner + */ + abstract fun onExitBackground() +} + +interface IBackgroundStatefulOwner : IStatefulOwner { + fun isBackground() = active() + + fun addLifecycleCallback(callback: IMatrixBackgroundCallback) = + observeForever(object : IStateObserver { + override fun on() = callback.onEnterBackground() + override fun off() = callback.onExitBackground() + }.also { callback.stateObserver = it }) + + fun addLifecycleCallback(lifecycleOwner: LifecycleOwner, callback: IMatrixBackgroundCallback) = + observeWithLifecycle(lifecycleOwner, object : IStateObserver { + override fun on() = callback.onEnterBackground() + override fun off() = callback.onExitBackground() + }.also { callback.stateObserver = it }) + + fun removeLifecycleCallback(callback: IMatrixBackgroundCallback) { + callback.stateObserver?.let { removeObserver(it) } + } + + @Deprecated("") + fun addLifecycleCallback(callback: IMatrixLifecycleCallback) = + observeForever(object : IStateObserver { + override fun on() = callback.onBackground() + override fun off() = callback.onForeground() + }.also { callback.stateObserver = it }) + + @Deprecated("") + fun addLifecycleCallback(lifecycleOwner: LifecycleOwner, callback: IMatrixLifecycleCallback) = + observeWithLifecycle(lifecycleOwner, object : IStateObserver { + override fun on() = callback.onBackground() + override fun off() = callback.onForeground() + }.also { callback.stateObserver = it }) + + @Deprecated("") + fun removeLifecycleCallback(callback: IMatrixLifecycleCallback) { + callback.stateObserver?.let { removeObserver(it) } + } +} + +interface IForegroundStatefulOwner : IStatefulOwner { + fun isForeground() = active() + + fun addLifecycleCallback(callback: IMatrixForegroundCallback) = + observeForever(object : IStateObserver { + override fun on() = callback.onEnterForeground() + override fun off() = callback.onExitForeground() + }.also { callback.stateObserver = it }) + + fun addLifecycleCallback(lifecycleOwner: LifecycleOwner, callback: IMatrixForegroundCallback) = + observeWithLifecycle(lifecycleOwner, object : IStateObserver { + override fun on() = callback.onEnterForeground() + override fun off() = callback.onExitForeground() + }.also { callback.stateObserver = it }) + + fun removeLifecycleCallback(callback: IMatrixForegroundCallback) { + callback.stateObserver?.let { removeObserver(it) } + } + + @Deprecated("") + fun addLifecycleCallback(callback: IMatrixLifecycleCallback) = + observeForever(object : IStateObserver { + override fun on() = callback.onForeground() + override fun off() = callback.onBackground() + }.also { callback.stateObserver = it }) + + @Deprecated("") + fun addLifecycleCallback(lifecycleOwner: LifecycleOwner, callback: IMatrixLifecycleCallback) = + observeWithLifecycle(lifecycleOwner, object : IStateObserver { + override fun on() = callback.onForeground() + override fun off() = callback.onBackground() + }.also { callback.stateObserver = it }) + + @Deprecated("") + fun removeLifecycleCallback(callback: IMatrixLifecycleCallback) { + callback.stateObserver?.let { removeObserver(it) } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleLogger.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleLogger.kt new file mode 100644 index 000000000..40b2d85ce --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleLogger.kt @@ -0,0 +1,102 @@ +package com.tencent.matrix.lifecycle + +import android.app.Application +import com.tencent.matrix.lifecycle.owners.* +import com.tencent.matrix.lifecycle.supervisor.* +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.MatrixUtil + +// @formatter:off +object MatrixLifecycleLogger { + + private var application: Application? = null + private val TAG by lazy { "Matrix.lifecycle.Logger_${String.format("%-10.10s", suffix())}" } + + private fun suffix(): String { + return if (MatrixUtil.isInMainProcess(application!!)) { + "Main" + } else { + val split = MatrixUtil.getProcessName(application!!).split(":").toTypedArray() + if (split.size > 1) { + split[1].takeLast(10) + } else { + "unknown" + } + } + } + + fun init(app: Application, enable: Boolean) { + application = app + if (!enable) { + MatrixLog.i(TAG, "logger disabled") + return + } + + ProcessUIResumedStateOwner.observeForever(object : ISerialObserver { + override fun on() = MatrixLog.i(TAG, "ON_PROCESS_UI_RESUMED") + override fun off() = MatrixLog.i(TAG, "ON_PROCESS_UI_PAUSED") + }) + + ProcessUIStartedStateOwner.observeForever(object : ISerialObserver { + override fun on() = MatrixLog.i(TAG, "ON_PROCESS_UI_STARTED scene: ${ProcessUILifecycleOwner.recentScene}") + override fun off() = MatrixLog.i(TAG, "ON_PROCESS_UI_STOPPED scene: ${ProcessUILifecycleOwner.recentScene}") + }) + + ProcessExplicitBackgroundOwner.observeForever(object : ISerialObserver { + override fun on() = MatrixLog.i(TAG, "ON_PROCESS_ENTER_EXPLICIT_BACKGROUND") + override fun off() = MatrixLog.i(TAG, "ON_PROCESS_EXIT_EXPLICIT_BACKGROUND") + }) + + ProcessStagedBackgroundOwner.observeForever(object : ISerialObserver { + override fun on() = MatrixLog.i(TAG, "ON_PROCESS_ENTER_STAGED_BACKGROUND") + override fun off() = MatrixLog.i(TAG, "ON_PROCESS_EXIT_STAGED_BACKGROUND") + }) + + ProcessDeepBackgroundOwner.observeForever(object : ISerialObserver { + override fun on() = MatrixLog.i(TAG, "ON_PROCESS_ENTER_DEEP_BACKGROUND") + override fun off() = MatrixLog.i(TAG, "ON_PROCESS_EXIT_DEEP_BACKGROUND") + }) + + AppUIForegroundOwner.observeForever(object : ISerialObserver { + override fun on() = MatrixLog.i(TAG, "ON_APP_UI_ENTER_FOREGROUND scene: ${ProcessSupervisor.getRecentScene()}") + override fun off() = MatrixLog.i(TAG, "ON_APP_UI_EXIT_FOREGROUND scene: ${ProcessSupervisor.getRecentScene()}") + }) + + AppExplicitBackgroundOwner.observeForever(object : ISerialObserver { + override fun off() = MatrixLog.i(TAG, "ON_APP_EXIT_EXPLICIT_BACKGROUND") + override fun on() = MatrixLog.i(TAG, "ON_APP_ENTER_EXPLICIT_BACKGROUND") + }) + + AppStagedBackgroundOwner.observeForever(object : ISerialObserver { + override fun off() = MatrixLog.i(TAG, "ON_APP_EXIT_STAGED_BACKGROUND") + override fun on() = MatrixLog.i(TAG, "ON_APP_ENTER_STAGED_BACKGROUND") + }) + + AppDeepBackgroundOwner.observeForever(object : ISerialObserver { + override fun off() = MatrixLog.i(TAG, "ON_APP_EXIT_DEEP_BACKGROUND") + override fun on() = MatrixLog.i(TAG, "ON_APP_ENTER_DEEP_BACKGROUND") + }) + + ProcessSupervisor.addDyingListener { scene, processName, pid -> + MatrixLog.i(TAG, "Dying Listener: process $pid-$processName is dying on scene $scene") + false // NOT rescue + } + + ProcessSupervisor.addDeathListener { scene, processName, pid, isLruKill -> + MatrixLog.i( + TAG, + "Death Listener: process $pid-$processName died on scene $scene, is LRU Kill? $isLruKill" + ) + } + + ForegroundServiceLifecycleOwner.observeForever(object : ISerialObserver { + override fun on() = MatrixLog.i(TAG, "ForegroundServiceLifecycleOwner: ON") + override fun off() = MatrixLog.i(TAG, "ForegroundServiceLifecycleOwner: OFF") + }) + + OverlayWindowLifecycleOwner.observeForever(object : ISerialObserver { + override fun on() = MatrixLog.i(TAG, "OverlayWindowLifecycleOwner: ON, hasOverlay = ${OverlayWindowLifecycleOwner.hasOverlayWindow()}, hasVisible = ${OverlayWindowLifecycleOwner.hasVisibleWindow()}") + override fun off() = MatrixLog.i(TAG, "OverlayWindowLifecycleOwner: OFF, hasOverlay = ${OverlayWindowLifecycleOwner.hasOverlayWindow()}, hasVisible = ${OverlayWindowLifecycleOwner.hasVisibleWindow()}") + }) + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleOwnerInitializer.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleOwnerInitializer.kt new file mode 100644 index 000000000..e65de774c --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleOwnerInitializer.kt @@ -0,0 +1,81 @@ +package com.tencent.matrix.lifecycle + +import android.annotation.SuppressLint +import android.app.Application +import androidx.annotation.NonNull +import com.tencent.matrix.lifecycle.MatrixLifecycleOwnerInitializer.Companion.init +import com.tencent.matrix.lifecycle.owners.ForegroundServiceLifecycleOwner +import com.tencent.matrix.lifecycle.owners.OverlayWindowLifecycleOwner +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner +import com.tencent.matrix.lifecycle.supervisor.SupervisorConfig +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.safeLet + +/** + * All feature that would change the origin Matrix behavior is disabled by default. + */ +data class MatrixLifecycleConfig( + val supervisorConfig: SupervisorConfig = SupervisorConfig(), + /** + * Injects Service#mActivityManager if true + */ + val enableFgServiceMonitor: Boolean = false, + /** + * Injects WindowManagerGlobal#mRoots if true + */ + val enableOverlayWindowMonitor: Boolean = false, + + val lifecycleThreadConfig: LifecycleThreadConfig = LifecycleThreadConfig(), + + val enableLifecycleLogger: Boolean = false +) + +/** + * You should init [com.tencent.matrix.Matrix] or call [init] manually before creating any Activity + * Created by Yves on 2021/9/14 + */ +class MatrixLifecycleOwnerInitializer { + companion object { + private const val TAG = "Matrix.ProcessLifecycleOwnerInit" + + @Volatile + private var inited = false + + @JvmStatic + fun init( + @NonNull app: Application, + config: MatrixLifecycleConfig + ) { + if (inited) { + return + } + inited = true + if (hasCreatedActivities()) { + ("Matrix Warning: Matrix might be inited after launching first Activity, " + + "which would disable some features like ProcessLifecycleOwner, " + + "pls consider calling MultiProcessLifecycleInitializer#init manually " + + "or initializing matrix at Application#onCreate").let { + MatrixLog.e(TAG, it) + } + return + } + MatrixLifecycleThread.init(config.lifecycleThreadConfig) + ProcessUILifecycleOwner.init(app) + ForegroundServiceLifecycleOwner.init(app, config.enableFgServiceMonitor) + OverlayWindowLifecycleOwner.init(config.enableOverlayWindowMonitor) + MatrixLifecycleLogger.init(app, config.enableLifecycleLogger) + } + + @SuppressLint("PrivateApi", "DiscouragedPrivateApi") + @JvmStatic + private fun hasCreatedActivities() = safeLet(tag = TAG, defVal = false) { + val clazzActivityThread = Class.forName("android.app.ActivityThread") + val objectActivityThread = + clazzActivityThread.getMethod("currentActivityThread").invoke(null) + val fieldMActivities = clazzActivityThread.getDeclaredField("mActivities") + fieldMActivities.isAccessible = true + val mActivities = fieldMActivities.get(objectActivityThread) as Map<*, *>? + return mActivities != null && mActivities.isNotEmpty() + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleThread.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleThread.kt new file mode 100644 index 000000000..a87bf12f7 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/MatrixLifecycleThread.kt @@ -0,0 +1,167 @@ +package com.tencent.matrix.lifecycle + +import android.os.Handler +import android.os.Process +import com.tencent.matrix.util.MatrixHandlerThread +import com.tencent.matrix.util.MatrixLog +import java.util.concurrent.* + +private const val TAG = "Matrix.Lifecycle.Thread" + +data class LifecycleThreadConfig( + val externalExecutor: Executor? = null, + val maxPoolSize: Int = 5, + val keepAliveSeconds: Long = 30L, + val onHeavyTaskDetected: (task: String, cost: Long) -> Unit = { task, cost -> + MatrixLog.e(TAG, "heavy task(cost ${cost}ms):$task") + }, + val onWorkerBlocked: (thread: String, stacktrace: String, cost: Long) -> Unit = { thread, stacktrace, cost -> + MatrixLog.e(TAG, "thread: $thread, cost: $cost, $stacktrace") + } +) + +/** + * Created by Yves on 2022/1/11 + */ +internal object MatrixLifecycleThread { + + private const val CORE_POOL_SIZE = 0 + private const val MAX_POOL_SIZE = 5 + private const val KEEP_ALIVE_SECONDS = 30L + + private var config = LifecycleThreadConfig() + + fun init(config: LifecycleThreadConfig) { + this.config = config + } + + val handler by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + Handler(MatrixHandlerThread.getNewHandlerThread("matrix_li", Thread.NORM_PRIORITY).looper) + } + + private val workerNamePool = ArrayList().also { + for (i in 0 until MAX_POOL_SIZE) { + it.add("matrix_x_$i") + } + } + + val executor by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + + if (config.externalExecutor != null) { + return@lazy config.externalExecutor!! + } + + val idleSynchronousQueue = IdleSynchronousQueue() + + object : ThreadPoolExecutor( + CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_SECONDS, + TimeUnit.SECONDS, + idleSynchronousQueue, + { r -> + Thread(Thread.currentThread().threadGroup, { + val name = Thread.currentThread().name + val tid = Process.myTid() + val begin = System.currentTimeMillis() + MatrixLog.i(TAG, "thread run: tid = ${tid}, name =$name") + r.run() + synchronized(workerNamePool) { + workerNamePool.add(name) + MatrixLog.i( + TAG, + "thread($tid,$name) finished, alive time ${System.currentTimeMillis() - begin}, now pool size = ${MAX_POOL_SIZE - workerNamePool.size}" + ) + } + }, synchronized(workerNamePool) { + workerNamePool.removeFirstOrNull() + } ?: "matrix_x_x", 0) + }, + { r, _ -> // full now + idleSynchronousQueue.idle(r) + } + ) { + override fun execute(command: Runnable?) { + super.execute(command?.wrap()) + } + } + } + + private data class TaskInfo(val task: String = "", val time: Long = 0) { + companion object { + fun of(runnable: Runnable?) = if (runnable == null) { + TaskInfo("", System.currentTimeMillis()) + } else { + TaskInfo(runnable.toString(), System.currentTimeMillis()) + } + } + } + + private val lastIdleTaskOfThread = ConcurrentHashMap() + + private fun ConcurrentHashMap.checkTimeout() { + forEach { + if (it.value.task.isEmpty()) { + return@forEach + } + val cost = System.currentTimeMillis() - it.value.time +// MatrixLog.d(TAG, "-> ${it.value.task} -> $cost") + if (cost > TimeUnit.SECONDS.toMillis(30)) { + buildString { + append("Dispatcher Thread Not Responding:\n") + it.key.stackTrace.forEach { s -> + append("\tat $s\n") + } + }.let { stacktrace -> + config.onWorkerBlocked.invoke(it.key.name, stacktrace, cost) + } + } + } + } + + private fun Runnable.wrap() = object : Runnable { + + override fun run() { + val begin = System.currentTimeMillis() + + TaskInfo.of(this@wrap).let { + lastIdleTaskOfThread[Thread.currentThread()] = it + } + + this@wrap.run() + + lastIdleTaskOfThread[Thread.currentThread()] = TaskInfo.of(null) + + val cost = System.currentTimeMillis() - begin + if (cost > 500) { + config.onHeavyTaskDetected.invoke(this.toString(), cost) + } + } + + override fun toString(): String { + return this@wrap.toString() + } + } + + private class IdleSynchronousQueue : SynchronousQueue() { + private val idleQueue = LinkedBlockingQueue() + + override fun poll(timeout: Long, unit: TimeUnit?): Runnable? { + return idleQueue.poll() ?: super.poll(timeout, unit) + } + + override fun poll(): Runnable? { + return idleQueue.poll() ?: super.poll() + } + + override fun take(): Runnable { + return idleQueue.poll() ?: super.take() + } + + fun idle(r: Runnable) { + lastIdleTaskOfThread.checkTimeout() + idleQueue.offer(r) + } + } + +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/StatefulOwner.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/StatefulOwner.kt new file mode 100644 index 000000000..5ceb7f2ba --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/StatefulOwner.kt @@ -0,0 +1,383 @@ +package com.tencent.matrix.lifecycle + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent +import com.tencent.matrix.lifecycle.LifecycleDelegateStatefulOwner.Companion.toStateOwner +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue + +interface IStateful { + fun active(): Boolean +} + +fun IStatefulOwner.reverse(): IStatefulOwner = object : IStatefulOwner { + override fun active() = !this@reverse.active() + + inner class ReverseObserverWrapper(val origin: IStateObserver) : IStateObserver, + ISerialObserver { + override fun on() = origin.off() + override fun off() = origin.on() + override fun toString() = origin.toString() + override fun hashCode() = origin.hashCode() + override fun equals(other: Any?): Boolean { + return if (other is ReverseObserverWrapper) { + origin == other.origin + } else { + false + } + } + + override fun serial(): Boolean { + return if (origin is ISerialObserver) { + origin.serial() + } else { + false + } + } + } + + private fun IStateObserver.wrap(): ReverseObserverWrapper = ReverseObserverWrapper(this) + + override fun observeForever(observer: IStateObserver) = + this@reverse.observeForever(observer.wrap()) + + override fun observeWithLifecycle(lifecycleOwner: LifecycleOwner, observer: IStateObserver) = + this@reverse.observeWithLifecycle(lifecycleOwner, observer.wrap()) + + override fun removeObserver(observer: IStateObserver) = + this@reverse.removeObserver(observer.wrap()) +} + +fun IStatefulOwner.shadow() = shadow(false) + +internal fun IStatefulOwner.shadow(serial: Boolean): IStatefulOwner = object : StatefulOwner(serial) { + init { + this@shadow.observeForever(object : IStateObserver, ISerialObserver { + override fun on() = turnOn() + override fun off() = turnOff() + override fun serial() = serial + }) + } +} + +interface IStateObserver { + fun on() + fun off() +} + +internal interface ISerialObserver : IStateObserver { + fun serial() = true +} + +interface IStateObservable { + fun observeForever(observer: IStateObserver) + fun observeWithLifecycle(lifecycleOwner: LifecycleOwner, observer: IStateObserver) + fun removeObserver(observer: IStateObserver) +} + +interface IStatefulOwner : IStateful, IStateObservable + +interface IMultiSourceOwner { + fun addSourceOwner(owner: StatefulOwner) + fun removeSourceOwner(owner: StatefulOwner) +} + +private open class ObserverWrapper(val observer: IStateObserver, val statefulOwner: StatefulOwner) { + open fun isAttachedTo(owner: LifecycleOwner?) = false +} + +private class AutoReleaseObserverWrapper constructor( + val lifecycleOwner: LifecycleOwner, + targetObservable: StatefulOwner, + observer: IStateObserver +) : ObserverWrapper(observer, targetObservable), LifecycleObserver { + + init { + if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) { + throw IllegalStateException("NOT allow to observe with DESTROYED lifecycle owner") + } + lifecycleOwner.lifecycle.addObserver(this) + } + + override fun isAttachedTo(owner: LifecycleOwner?) = lifecycleOwner == owner + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun release() { + statefulOwner.removeObserver(observer) + } +} + +private fun ObserverWrapper.checkLifecycle(lifecycleOwner: LifecycleOwner?): Boolean { + val broken = if (lifecycleOwner != null) { + isAttachedTo(lifecycleOwner) + } else { + this is AutoReleaseObserverWrapper + } + + if (broken) { + throw IllegalArgumentException( + "Cannot add the same observer with different lifecycles" + ) + } + + return false +} + +private enum class State(val dispatch: ((observer: IStateObserver) -> Unit)?) { + INIT(null), + ON({ observer -> observer.on() }), + OFF({ observer -> observer.off() }); +} + +open class StatefulOwner(val async: Boolean = true) : IStatefulOwner { + + @Volatile + private var state = State.INIT + + private val observerMap = ConcurrentHashMap() + + override fun active() = state == State.ON + + /** + * Observe the [StatefulOwner] forever util you remove the observer. + * You can add observer at any time, even before the initialization of Matrix + */ + @Synchronized + override fun observeForever(observer: IStateObserver) { + val wrapper = observerMap[observer] + if (wrapper != null) { + wrapper.checkLifecycle(null) + } else { + observerMap[observer] = ObserverWrapper(observer, this) + if (async) { + state.dispatch?.invokeAsync(observer) + } else { + state.dispatch?.invoke(observer) + } + } + } + + /** + * Observer the [StatefulOwner] with lifecycle. It is useful for observers from Activities + * + * When lifecycle owner destroyed, the [StatefulOwner] would remove the observer automatically + * thus avoid leaking the lifecycle owner + * + * You can add observer at any time, even before the initialization of Matrix + */ + @Synchronized + override fun observeWithLifecycle(lifecycleOwner: LifecycleOwner, observer: IStateObserver) { + val wrapper = observerMap[observer] + if (wrapper != null) { + wrapper.checkLifecycle(lifecycleOwner) + } else { + observerMap[observer] = AutoReleaseObserverWrapper(lifecycleOwner, this, observer) + if (async) { + state.dispatch?.invokeAsync(observer) + } else { + state.dispatch?.invoke(observer) + } + } + } + + @Synchronized + override fun removeObserver(observer: IStateObserver) { + observerMap.remove(observer) + } + + @Synchronized + protected fun turnOn() { + if (state == State.ON) { + return + } + state = State.ON + dispatchStateChanged() + } + + @Synchronized + protected fun turnOff() { + if (state == State.OFF) { + return + } + state = State.OFF + dispatchStateChanged() + } + + private fun ((observer: IStateObserver) -> Unit).invokeAsync(observer: IStateObserver) { + if (observer is ISerialObserver) { + if (observer.serial()) { + MatrixLifecycleThread.handler.post { + this.invoke(observer) + } + return + } + } + MatrixLifecycleThread.executor.execute(object : Runnable { + override fun run() { + this@invokeAsync.invoke(observer) + } + + override fun toString(): String { + return "$observer" + } + }) + } + + private fun dispatchStateChanged() { + if (async) { + val s = state // copy current state + observerMap.forEach { + s.dispatch?.invokeAsync(it.key) + } + } else { + observerMap.forEach { + state.dispatch?.invoke(it.key) + } + } + } +} + +/** + * A delegate class for converting lifecycle to StateOwner. + * + * @author aurorani + * @since 2021/9/24 + */ +class LifecycleDelegateStatefulOwner private constructor( + private val source: LifecycleOwner, + async: Boolean = true +) : StatefulOwner(async), LifecycleObserver { + + companion object { + fun LifecycleOwner.toStateOwner(): LifecycleDelegateStatefulOwner = + LifecycleDelegateStatefulOwner(this) + } + + init { + source.lifecycle.addObserver(this) + } + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + fun onCreate() = turnOff() + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun onReceiveStart() = turnOn() + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onReceiveStop() = turnOff() + + override fun toString() = source.toString() +} + +/** + * The base class for combining multiple source owner with [reduceOperator] + * + * @author aurorani + * @since 2021/9/24 + */ +open class MultiSourceStatefulOwner( + private val reduceOperator: (statefuls: Collection) -> Boolean, + vararg statefulOwners: IStatefulOwner, +) : StatefulOwner(true), IStateObserver, IMultiSourceOwner { + + private val sourceOwners = ConcurrentLinkedQueue() + + init { + statefulOwners.forEach { + register(it) + } + } + + private fun register(owner: IStatefulOwner) { + owner.let { + if (it is MultiSourceStatefulOwner) { + throw IllegalArgumentException("NOT allow to add MultiSourceStatefulOwner as source, consider to add its shadow owner") + } + sourceOwners.add(it) + it.observeForever(this) + } + } + + private fun unregister(owner: StatefulOwner) { + owner.also { + sourceOwners.remove(it) + it.removeObserver(this) + onStateChanged() + } + } + + override fun addSourceOwner(owner: StatefulOwner) = register(owner) + + open fun addSourceOwner(owner: LifecycleOwner): StatefulOwner = + owner.toStateOwner().also { addSourceOwner(it) } + + override fun removeSourceOwner(owner: StatefulOwner) = unregister(owner) + + override fun on() = onStateChanged() + + override fun off() = onStateChanged() + + override fun active() = if (sourceOwners.isEmpty()) { + super.active() + } else { + reduceOperator(sourceOwners).also { + if (it) { + turnOn() + } else { + turnOff() + } + } + } + + /** + * Callback function while state of any source owners is changed. + */ + private fun onStateChanged() { + if (reduceOperator(sourceOwners)) { + turnOn() + } else { + turnOff() + } + } +} + +/** + * Immutable version of [MultiSourceStatefulOwner] + * + * @author aurorani + * @since 2021/9/24 + */ +open class ImmutableMultiSourceStatefulOwner( + reduceOperator: (statefuls: Collection) -> Boolean, + vararg args: IStatefulOwner +) : MultiSourceStatefulOwner(reduceOperator, *args) { + final override fun addSourceOwner(owner: StatefulOwner) { + throw UnsupportedOperationException() + } + + final override fun addSourceOwner(owner: LifecycleOwner): StatefulOwner { + throw UnsupportedOperationException() + } + + final override fun removeSourceOwner(owner: StatefulOwner) { + throw UnsupportedOperationException() + } +} + +class ReduceOperators { + companion object { + val OR = { statefuls: Collection -> + statefuls.any { it.active() } + } + + val AND = { statefuls: Collection -> + statefuls.all { it.active() } + } + + val NONE = { statefuls: Collection -> + statefuls.none { it.active() } + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ArrayListProxy.java b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ArrayListProxy.java new file mode 100644 index 000000000..f1dfc4991 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ArrayListProxy.java @@ -0,0 +1,212 @@ +package com.tencent.matrix.lifecycle.owners; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.tencent.matrix.util.MatrixLog; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Implement in Java for the override limitation of Kotlin + * + * Created by Yves on 2021/12/30 + */ +class ArrayListProxy extends ArrayList { + + private static final String TAG = "Matrix.ArrayListProxy"; + + private final ArrayList mOrigin; + + interface OnDataChangedListener { + void onAdded(Object o); + void onRemoved(Object o); + } + + private final OnDataChangedListener mListener; + + ArrayListProxy(ArrayList origin, OnDataChangedListener listener) { + mOrigin = origin; + mListener = listener; + } + + @Override + public int size() { + return mOrigin.size(); + } + + @Override + public boolean isEmpty() { + return mOrigin.isEmpty(); + } + + @Override + public boolean contains(@Nullable Object o) { + return mOrigin.contains(o); + } + + @NonNull + @Override + public Iterator iterator() { + return mOrigin.iterator(); + } + + @NonNull + @Override + public Object[] toArray() { + return mOrigin.toArray(); + } + + @NonNull + @Override + public T1[] toArray(@NonNull T1[] a) { + return mOrigin.toArray(a); + } + + @Override + public boolean add(T t) { + boolean ret = mOrigin.add(t); + + try { + mListener.onAdded(t); + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + + return ret; + } + + @Override + public boolean remove(@Nullable Object o) { + boolean ret = mOrigin.remove(o); + + try { + mListener.onRemoved(o); + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + + return ret; + } + + @Override + public boolean containsAll(@NonNull Collection c) { + return mOrigin.containsAll(c); + } + + @Override + public boolean addAll(@NonNull Collection c) { + boolean ret = mOrigin.addAll(c); + try { + for (T t : c) { + mListener.onAdded(t); + } + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + + return ret; + } + + @Override + public boolean addAll(int index, @NonNull Collection c) { + boolean ret = mOrigin.addAll(index, c); + try { + for (T t : c) { + mListener.onAdded(t); + } + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + return ret; + } + + @Override + public boolean removeAll(@NonNull Collection c) { + boolean ret = mOrigin.removeAll(c); + + try { + for (Object o : c) { + mListener.onRemoved(o); + } + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + + return ret; + } + + @Override + public boolean retainAll(@NonNull Collection c) { + return mOrigin.retainAll(c); + } + + @Override + public void clear() { + mOrigin.clear(); + } + + @Override + public T get(int index) { + return mOrigin.get(index); + } + + @Override + public T set(int index, T element) { + return mOrigin.set(index, element); + } + + @Override + public void add(int index, T element) { + mOrigin.add(index, element); + try { + mListener.onAdded(element); + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + } + + @Override + public T remove(int index) { + T ret = mOrigin.remove(index); + try { + mListener.onRemoved(ret); + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + return ret; + } + + @Override + public int indexOf(@Nullable Object o) { + return mOrigin.indexOf(o); + } + + @Override + public int lastIndexOf(@Nullable Object o) { + return mOrigin.lastIndexOf(o); + } + + @NonNull + @Override + public ListIterator listIterator() { + return mOrigin.listIterator(); + } + + @NonNull + @Override + public ListIterator listIterator(int index) { + return mOrigin.listIterator(index); + } + + @NonNull + @Override + public List subList(int fromIndex, int toIndex) { + return mOrigin.subList(fromIndex, toIndex); + } + +} diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ForegroundServiceLifecycleOwner.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ForegroundServiceLifecycleOwner.kt new file mode 100644 index 000000000..aafee2d7d --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ForegroundServiceLifecycleOwner.kt @@ -0,0 +1,246 @@ +package com.tencent.matrix.lifecycle.owners + +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.os.Build +import android.os.Handler +import android.os.Message +import android.os.Process +import android.util.ArrayMap +import com.tencent.matrix.lifecycle.MatrixLifecycleThread +import com.tencent.matrix.lifecycle.StatefulOwner +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.safeApply +import com.tencent.matrix.util.safeLet +import com.tencent.matrix.util.safeLetOrNull +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy + +/** + * Created by Yves on 2021/11/30 + */ +object ForegroundServiceLifecycleOwner : StatefulOwner() { + + private const val TAG = "Matrix.lifecycle.FgService" + + private const val CREATE_SERVICE = 114 + private const val STOP_SERVICE = 116 + + @SuppressLint("DiscouragedPrivateApi") + private val fieldServicemActivityManager = safeLetOrNull(TAG) { + Class.forName("android.app.Service").getDeclaredField("mActivityManager") + .apply { isAccessible = true } + } + + private var activityManager: ActivityManager? = null + private var ActivityThreadmServices: ArrayMap<*, *>? = null + private var ActivityThreadmH: Handler? = null + + private var fgServiceHandler: FgServiceHandler? = null + + fun init(context: Context, enable: Boolean) { + activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + if (!enable) { + MatrixLog.i(TAG, "disabled") + return + } + inject() + } + + @SuppressLint("DiscouragedPrivateApi", "PrivateApi") + private fun inject() { + + safeApply(TAG) { + + val clazzActivityThread = Class.forName("android.app.ActivityThread") + + val fieldActivityThreadmH = + clazzActivityThread.getDeclaredField("mH") + .apply { isAccessible = true } + + val activityThread = clazzActivityThread.getMethod("currentActivityThread").let { + it.isAccessible = true + it.invoke(null) + } + + ActivityThreadmServices = clazzActivityThread.getDeclaredField("mServices").let { + it.isAccessible = true + it.get(activityThread) as ArrayMap<*, *>? + } + + ActivityThreadmH = fieldActivityThreadmH.get(activityThread) as Handler + + Handler::class.java.getDeclaredField("mCallback").apply { + isAccessible = true + val origin = get(ActivityThreadmH) as Handler.Callback? + set(ActivityThreadmH, HHCallback(origin)) + MatrixLog.i(TAG, "origin is ${origin?.javaClass?.name}") + } + } + } + + fun hasForegroundService(): Boolean { + if (activityManager == null) { + throw IllegalStateException("NOT initialized yet") + } + return safeLet(TAG, defVal = false) { + @Suppress("DEPRECATION") + activityManager!!.getRunningServices(Int.MAX_VALUE) + .filter { + it.uid == Process.myUid() && it.pid == Process.myPid() + }.any { + it.foreground + } + }.also { + if (!it) { + fgServiceHandler?.clear() + } + } + } + + private class HHCallback(private val mHCallback: Handler.Callback?) : Handler.Callback { + + private var reentrantFence = false + + override fun handleMessage(msg: Message): Boolean { + if (reentrantFence) { + MatrixLog.e(TAG, "reentrant!!! ignore this msg: ${msg.what}") + return false + } + + when (msg.what) { + CREATE_SERVICE -> { + ActivityThreadmH?.post { + safeApply(TAG) { + injectAmIfNeeded() + } + } + } + STOP_SERVICE -> { + ActivityThreadmH?.post { + MatrixLifecycleThread.handler.post { + safeApply(TAG) { + hasForegroundService() + } + } + } + } + } + + reentrantFence = true + val ret = mHCallback?.handleMessage(msg) + reentrantFence = false + + return ret ?: false + } + + private fun injectAmIfNeeded() { + ActivityThreadmServices?.forEach { + val targetAm = fieldServicemActivityManager?.get(it.value) + if (Proxy.isProxyClass(targetAm!!.javaClass) && Proxy.getInvocationHandler(targetAm) == fgServiceHandler) { + return@forEach + } + if (fgServiceHandler == null) { + MatrixLog.i(TAG, "first inject") + fgServiceHandler = FgServiceHandler(targetAm) + } + MatrixLog.i(TAG, "going to inject ${it.value}") + val s = it.value as Service + checkIfAlreadyForegrounded(ComponentName(s, s.javaClass.name)) + fieldServicemActivityManager?.set(it.value, Proxy.newProxyInstance(targetAm.javaClass.classLoader, targetAm.javaClass.interfaces, fgServiceHandler!!)) + } + } + + private fun checkIfAlreadyForegrounded(componentName: ComponentName) { + MatrixLifecycleThread.handler.post { + safeApply(TAG) { + activityManager?.getRunningServices(Int.MAX_VALUE)?.filter { + it.pid == Process.myPid() + && it.uid == Process.myUid() + && it.service == componentName + && it.foreground + }?.forEach { + MatrixLog.i(TAG, "service turned fg when create: ${it.service}") + fgServiceHandler?.onStartForeground(it.service) + } + } + } + } + } + + private class FgServiceHandler(val origin: Any?) : InvocationHandler { + + private val fgServiceRecord by lazy { HashSet() } + + override fun invoke(proxy: Any?, method: Method?, vararg args: Any?): Any? { + return try { + val ret = method?.invoke(origin, *args) + + if (method?.name == "setServiceForeground") { + MatrixLog.i(TAG, "real invoked setServiceForeground: ${args.contentToString()}") + if (args.size > 3 && args[3] == null) { + onStopForeground(args[0] as ComponentName) + } else { + onStartForeground(args[0] as ComponentName) + } + } + + ret + } catch (e: Throwable) { + MatrixLog.printErrStackTrace(TAG, e, "") + null + } + } + + fun onStartForeground(componentName: ComponentName) { + var shouldTurnOn = false + synchronized(fgServiceRecord) { + MatrixLog.i(TAG, "hack onStartForeground: $componentName") + if (fgServiceRecord.isEmpty()) { + MatrixLog.i(TAG, "should turn ON") + shouldTurnOn = true + } + fgServiceRecord.add(componentName) + } + if (shouldTurnOn) { + MatrixLog.i(TAG, "do turn ON") + turnOn() // avoid holding lock of fgServiceRecord + } + } + + fun onStopForeground(componentName: ComponentName) { + var shouldTurnOff = false + synchronized(fgServiceRecord){ + MatrixLog.i(TAG, "hack onStopForeground: $componentName") + fgServiceRecord.remove(componentName) + if (fgServiceRecord.isEmpty()) { + MatrixLog.i(TAG, "should turn OFF") + shouldTurnOff = true + } + } + if (shouldTurnOff) { + MatrixLog.i(TAG, "do turn OFF") + turnOff() + } + } + + fun clear() { + var needTurnOff = false + synchronized(fgServiceRecord) { + if (fgServiceRecord.isNotEmpty()) { + fgServiceRecord.clear() + needTurnOff = true + MatrixLog.i(TAG, "clear done, should turn OFF") + } + } + if (needTurnOff) { + MatrixLog.i(TAG, "fix clear: do turn OFF") + turnOff() + } + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/OverlayWindowLifecycleOwner.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/OverlayWindowLifecycleOwner.kt new file mode 100644 index 000000000..0dee34cd9 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/OverlayWindowLifecycleOwner.kt @@ -0,0 +1,172 @@ +package com.tencent.matrix.lifecycle.owners + +import android.annotation.SuppressLint +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import com.tencent.matrix.lifecycle.StatefulOwner +import com.tencent.matrix.util.* + +/** + * Created by Yves on 2021/12/30 + */ +@SuppressLint("PrivateApi") +object OverlayWindowLifecycleOwner : StatefulOwner() { + + private const val TAG = "Matrix.OverlayWindowLifecycleOwner" + + private val overlayViews = HashSet() + private val mainHandler = Handler(Looper.getMainLooper()) + + private var WindowManagerGlobal_mRoots: ArrayList<*>? = null + + @Volatile + private var injected = false + + private val Field_ViewRootImpl_mView by lazy { + safeLetOrNull(TAG) { + ReflectFiled(Class.forName("android.view.ViewRootImpl"), "mView") + } + } + + internal fun init(enable: Boolean) { + if (!enable) { + MatrixLog.i(TAG, "disabled") + return + } + inject() + } + + private fun ViewGroup.LayoutParams.isOverlayType(): Boolean { + return this is WindowManager.LayoutParams && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + this.type == WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY || this.type == WindowManager.LayoutParams.TYPE_PHONE + } else { + this.type == WindowManager.LayoutParams.TYPE_PHONE + } + } + + private val onViewRootChangedListener: ArrayListProxy.OnDataChangedListener by lazy { + object : ArrayListProxy.OnDataChangedListener { + override fun onAdded(o: Any) { + // we post to main thread cause the View is NOT set to ViewRootImpl yet + mainHandler.post { + val view = safeLetOrNull(TAG) { + Field_ViewRootImpl_mView?.get(o) as View + } + if (view?.layoutParams?.isOverlayType() == true) { + if (overlayViews.isEmpty()) { + turnOn() + } + overlayViews.add(o) + } + } + } + + override fun onRemoved(o: Any) { + overlayViews.remove(o) + if (overlayViews.isEmpty()) { + turnOff() + } + } + } + } + + @Suppress("LocalVariableName") + private fun inject() = safeApply(TAG) { + if (injected) { + MatrixLog.e(TAG, "already injected") + return@safeApply + } + val Clazz_WindowManagerGlobal = Class.forName("android.view.WindowManagerGlobal") + + val WindowManagerGlobal_instance = + ReflectUtils.invoke(Clazz_WindowManagerGlobal, "getInstance", null) + + val WindowManagerGlobal_mLock = + ReflectUtils.get(Clazz_WindowManagerGlobal, "mLock", WindowManagerGlobal_instance) + + synchronized(WindowManagerGlobal_mLock) { + val origin = ReflectUtils.get>( + Clazz_WindowManagerGlobal, + "mRoots", + WindowManagerGlobal_instance + ) + val proxy = ArrayListProxy(origin, onViewRootChangedListener) + ReflectUtils.set( + Clazz_WindowManagerGlobal, + "mRoots", + WindowManagerGlobal_instance, + proxy + ) + WindowManagerGlobal_mRoots = proxy + } + injected = true + } + + @Volatile + private var fallbacked = false + + private fun prepareWindowGlobal() { + if (WindowManagerGlobal_mRoots == null) { + if (fallbacked) { + throw ClassNotFoundException("WindowManagerGlobal_mRoots not found") + } + MatrixLog.i(TAG, "monitor disabled, fallback init") + fallbacked = true + WindowManagerGlobal_mRoots = safeLetOrNull(TAG) { + Class.forName("android.view.WindowManagerGlobal").let { + val instance = ReflectUtils.invoke(it, "getInstance", null) + ReflectUtils.get(it, "mRoots", instance) + } + } + } + if (WindowManagerGlobal_mRoots == null) { + throw ClassNotFoundException("WindowManagerGlobal_mRoots not found") + } + if (Field_ViewRootImpl_mView == null) { + throw ClassNotFoundException("Field_ViewRootImpl_mView not found") + } + } + + /** + * including Activity window, for fallback checks + */ + fun hasVisibleWindow() = safeLet(TAG, log = true, defVal = false) { + prepareWindowGlobal() + return@safeLet WindowManagerGlobal_mRoots!!.any { + val v = Field_ViewRootImpl_mView!!.get(it) + // windowVisibility is determined by app vibility + // until the PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY is set, which is blocked in Q + return@any v != null && View.VISIBLE == v.visibility && View.VISIBLE == v.windowVisibility + } + } + + fun hasOverlayWindow() = if (injected) { + active() + } else safeLet(TAG, log = true, defVal = false) { + prepareWindowGlobal() + return@safeLet WindowManagerGlobal_mRoots!!.any { + val v = Field_ViewRootImpl_mView!!.get(it) + // windowVisibility is determined by app vibility + // until the PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY is set, which is blocked in Q + return@any v != null + && v.layoutParams?.isOverlayType() == true + && View.VISIBLE == v.visibility + && View.VISIBLE == v.windowVisibility + } + } + + /** + * requires enable the monitor + */ + internal fun getAllViews() = safeLet(TAG, log = true, defVal = emptyList()) { + prepareWindowGlobal() + return@safeLet WindowManagerGlobal_mRoots + ?.map { Field_ViewRootImpl_mView!!.get(it) } + ?.toList() + ?: emptyList() + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ProcessBackgroundStateOwner.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ProcessBackgroundStateOwner.kt new file mode 100644 index 000000000..dd36e4dbd --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ProcessBackgroundStateOwner.kt @@ -0,0 +1,223 @@ +package com.tencent.matrix.lifecycle.owners + +import androidx.lifecycle.LifecycleOwner +import com.tencent.matrix.lifecycle.* +import com.tencent.matrix.util.MatrixLog +import java.util.concurrent.TimeUnit + +private val MAX_CHECK_INTERVAL = TimeUnit.MINUTES.toMillis(1) +private const val MAX_CHECK_TIMES = 20 + +/** + * Usage: + * recommended readable APIs: + * ProcessExplicitBackgroundOwner.isBackground() + * ProcessExplicitBackgroundOwner.addLifecycleCallback(object : IMatrixBackgroundCallback() { + * override fun onExitBackground() {} + * override fun onEnterBackground() {} + * }) + * // auto remove callback when lifecycle destroyed + * ProcessExplicitBackgroundOwner.addLifecycleCallback(lifecycleOwner, object : IMatrixBackgroundCallback() { + * override fun onExitBackground() {} + * override fun onEnterBackground() {} + * }) + * + * the origin abstract APIs are also available: + * ProcessExplicitBackgroundOwner.active() // return true when state ON, in other words, it is explicit background now + * ProcessExplicitBackgroundOwner.observeForever(object : IStateObserver { + * override fun on() {} // entered the state, in other words, turned explicit background + * override fun off() {} // exit the state, in other words, turned foreground + * }) + * // auto remove callback when lifecycle destroyed + * ProcessExplicitBackgroundOwner.observeForeverWithLifecycle(lifecycleOwner, object : IStateObserver { + * override fun on() {} + * override fun off() {} + * }) + * + * State-ON: + * Activity is NOT in foreground + * AND without foreground Service + * AND without floating Views + * then callback [IStateObserver.on] and [IMatrixLifecycleCallback.onBackground] would be called + * + * NOTICE: + * + * [ForegroundServiceLifecycleOwner] is an optional StatefulOwner which is disabled by default and + * can be enabled by [MatrixLifecycleConfig.enableFgServiceMonitor] + * [OverlayWindowLifecycleOwner] is similar, which can be enabled by [MatrixLifecycleConfig.enableOverlayWindowMonitor] + * + * If the [ForegroundServiceLifecycleOwner] were disabled, this owner would start the timer checker + * to check the ForegroundService states. If there were foreground Service launched after UI turned + * background, the callback [IStateObserver.off] or [IMatrixLifecycleCallback.onForeground] + * wouldn't be call until we call the [active] or the state of upstream Owner changes. + * So do [OverlayWindowLifecycleOwner]. + */ +object ProcessExplicitBackgroundOwner : StatefulOwner(), IBackgroundStatefulOwner { + private const val TAG = "Matrix.background.Explicit" + + var maxCheckInterval = MAX_CHECK_INTERVAL + set(value) { + if (value < TimeUnit.SECONDS.toMillis(10)) { + throw IllegalArgumentException("interval should NOT be less than 10s") + } + field = value + MatrixLog.i(TAG, "set max check interval as $value") + } + + private val checkTask = object : TimerChecker(TAG, maxCheckInterval) { + override fun action(): Boolean { + val uiForeground by lazy { ProcessUIStartedStateOwner.active() } + val fgService by lazy { ForegroundServiceLifecycleOwner.hasForegroundService() } + val visibleWindow by lazy { OverlayWindowLifecycleOwner.hasOverlayWindow() } + + if (uiForeground) { + MatrixLog.i(TAG, "turn OFF for UI foreground") + turnOff() // must be NOT in explicit background and do NOT need polling checker + return false + } + + if (!fgService && !visibleWindow) { + MatrixLog.i(TAG, "turn ON") + turnOn() + return false + } + MatrixLog.i(TAG, "turn OFF: fgService=$fgService, visibleView=$visibleWindow, overlay=${OverlayWindowLifecycleOwner.hasOverlayWindow()}") + turnOff() + return true + } + } + + init { + object : ImmutableMultiSourceStatefulOwner( + ReduceOperators.OR, + ProcessUIStartedStateOwner, + ForegroundServiceLifecycleOwner, + OverlayWindowLifecycleOwner + ), ISerialObserver {}.observeForever(object : ISerialObserver { + override fun on() { // Activity foreground + checkTask.stop() + turnOff() + } + + override fun off() { // Activity background + checkTask.post() + } + }) + } + + /** + * It is possible to trigger a manual check by calling this method after calling + * stopForeground/removeFloatingView for more accurate state callbacks. + */ + override fun active(): Boolean { + return if (ProcessUIStartedStateOwner.active()) { + turnOff() + false + } else { + checkTask.checkAndPostIfNeeded() + super.active() + } + } +} + + +/** + * Usage: + * similar to [ProcessExplicitBackgroundOwner] + * + * State-ON: + * Process is explicitly in background: [ProcessExplicitBackgroundOwner] isActive + * BUT there are tasks of current process in the recent screen + * + * notice: same as [ProcessExplicitBackgroundOwner] + */ +object ProcessStagedBackgroundOwner : StatefulOwner(), IBackgroundStatefulOwner { + private const val TAG = "Matrix.background.Staged" + + var maxCheckInterval = MAX_CHECK_INTERVAL + set(value) { + if (value < TimeUnit.SECONDS.toMillis(10)) { + throw IllegalArgumentException("interval should NOT be less than 10s") + } + field = value + MatrixLog.i(TAG, "set max check interval as $value") + } + + var maxCheckTimes = MAX_CHECK_TIMES + set(value) { + if (value <= 0) { + throw IllegalArgumentException("max check times should be greater than 0") + } + field = value + MatrixLog.i(TAG, "set max check interval as $value") + } + + private val checkTask = object : TimerChecker(TAG, maxCheckInterval, maxCheckTimes) { + override fun action(): Boolean { + if (ProcessExplicitBackgroundOwner.active() + && (ProcessUILifecycleOwner.hasRunningAppTask() + .also { MatrixLog.i(TAG, "hasRunningAppTask? $it") } + || ProcessUICreatedStateOwner.active()) + ) { + MatrixLog.i(TAG, "turn ON") + turnOn() // staged background + return true + } + MatrixLog.i(TAG, "turn off") + turnOff() // means deep background + return false + } + } + + init { + ProcessExplicitBackgroundOwner.observeForever(object : ISerialObserver { + override fun on() { // explicit background + checkTask.post() + } + + override fun off() { // foreground + checkTask.stop() + turnOff() + } + }) + } + + override fun active(): Boolean { + return if (!ProcessExplicitBackgroundOwner.active()) { + turnOff() + false + } else { + checkTask.checkAndPostIfNeeded() + super.active() + } + } +} + +/** + * Usage: + * similar to [ProcessExplicitBackgroundOwner] + * + * State-ON: + * Process is explicitly in background: [ProcessExplicitBackgroundOwner] isActive + * AND there is NO task of current process in the recent screen + * + * notice: same as [ProcessExplicitBackgroundOwner] + */ +object ProcessDeepBackgroundOwner : StatefulOwner(), IBackgroundStatefulOwner { + + private val delegate = object : ImmutableMultiSourceStatefulOwner( + ReduceOperators.AND, + ProcessUICreatedStateOwner.reverse(), // move to first to avoid useless checks + ProcessExplicitBackgroundOwner, + ProcessStagedBackgroundOwner.reverse() + ), ISerialObserver {} + + override fun active() = delegate.active() + + override fun observeForever(observer: IStateObserver) = delegate.observeForever(observer) + + override fun observeWithLifecycle(lifecycleOwner: LifecycleOwner, observer: IStateObserver) = + delegate.observeWithLifecycle(lifecycleOwner, observer) + + override fun removeObserver(observer: IStateObserver) = delegate.removeObserver(observer) +} diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ProcessUILifecycleOwner.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ProcessUILifecycleOwner.kt new file mode 100644 index 000000000..fdbcc6ad7 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/ProcessUILifecycleOwner.kt @@ -0,0 +1,496 @@ +package com.tencent.matrix.lifecycle.owners + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.ActivityManager +import android.app.Application +import android.content.ComponentName +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.Process +import android.text.TextUtils +import androidx.lifecycle.* +import com.tencent.matrix.lifecycle.* +import com.tencent.matrix.listeners.IAppForeground +import com.tencent.matrix.util.* +import java.util.* + +/** + * Usage: + * recommended readable APIs: + * ProcessUIStartedStateOwner.isForeground() + * ProcessUIStartedStateOwner.addLifecycleCallback(object : IMatrixForegroundCallback() { + * override fun onEnterForeground() {} + * override fun onExitForeground() {} + * }) + * // auto remove callback when lifecycle destroyed + * ProcessUIStartedStateOwner.addLifecycleCallback(lifecycleOwner, object : IMatrixForegroundCallback() { + * override fun onEnterForeground() {} + * override fun onExitForeground() {} + * }) + * + * the origin abstract APIs are also available: + * ProcessUIStartedStateOwner.active() // return true when state ON, in other words, it is foreground now + * ProcessUIStartedStateOwner.observeForever(object : IStateObserver { + * override fun on() {} // entered the state, in other words, turned foreground + * override fun off() {} // exit the state, in other words, turned background + * }) + * // auto remove callback when lifecycle destroyed + * ProcessUIStartedStateOwner.observeForeverWithLifecycle(lifecycleOwner, object : IStateObserver { + * override fun on() {} + * override fun off() {} + * }) + * + * State-ON: + * any Activity started + * State-OFF: + * all Activity stopped + */ +object ProcessUIStartedStateOwner: IForegroundStatefulOwner by ProcessUILifecycleOwner.startedStateOwner + +/** + * API: + * similar to [ProcessUIStartedStateOwner] + * + * State-ON: + * any Activity created + * State-OFF: + * all Activity destroyed + */ +object ProcessUICreatedStateOwner: IForegroundStatefulOwner by ProcessUILifecycleOwner.createdStateOwner + +/** + * API: + * similar to [ProcessUIStartedStateOwner] + * + * State-ON: + * any Activity resumed + * State-OFF: + * all Activity paused + */ +object ProcessUIResumedStateOwner: IForegroundStatefulOwner by ProcessUILifecycleOwner.resumedStateOwner + +/** + * multi process version of [androidx.lifecycle.ProcessLifecycleOwner] + * + * for preloaded processes(never resumed or started), observer wouldn't be notified. + * otherwise, adding observer would trigger callback immediately + * + * Activity's lifecycle callback is not always reliable and compatible. onStop might be called + * without calling onStart or onResume, or onResume might be called more than once but stop once + * only. And for some tabs or special devices, It is possible to show more than two Activity + * at the same time. + * For these unusual cases we don't use simple counter here. + * + * Created by Yves on 2021/9/14 + */ +@SuppressLint("PrivateApi") +object ProcessUILifecycleOwner { + + private const val TAG = "Matrix.ProcessLifecycle" + + private const val TIMEOUT_MS = 500L //mls + private var processName: String? = null + private var packageName: String? = null + private var activityManager: ActivityManager? = null + private var activityInfoArray: Array? = null + + internal fun init(app: Application) { + activityManager = app.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + processName = MatrixUtil.getProcessName(app) + packageName = MatrixUtil.getPackageName(app) + activityInfoArray = safeLetOrNull(TAG) { + app.packageManager + .getPackageInfo( + packageName!!, + PackageManager.GET_ACTIVITIES + ).activities + } + attach(app) + MatrixLog.i(TAG, "init for [${processName}]") + } + + // ========================== base ========================== // + + private val runningHandler = MatrixLifecycleThread.handler + + private val stub = Any() + private val createdActivities = WeakHashMap() + private val resumedActivities = WeakHashMap() + private val startedActivities = WeakHashMap() + + private val destroyedActivities = WeakHashMap() + + private fun WeakHashMap.synchronized(action: WeakHashMap.() -> R): R { + synchronized(this) { + return action() + } + } + + private fun WeakHashMap.put(activity: Activity) = put(activity, stub) + + private var pauseSent = true + private var stopSent = true + + private open class AsyncOwner : StatefulOwner(), IForegroundStatefulOwner { + open fun turnOnAsync() = turnOn() + open fun turnOffAsync() = turnOff() + } + + private class CreatedStateOwner : AsyncOwner() { + override fun active(): Boolean { + return super.active() && createdActivities.synchronized { all { false == it.key?.isFinishing } } + } + } + + internal val createdStateOwner: IForegroundStatefulOwner = CreatedStateOwner() + internal val resumedStateOwner: IForegroundStatefulOwner = AsyncOwner() + internal val startedStateOwner: IForegroundStatefulOwner = AsyncOwner() + + internal interface OnSceneChangedListener { + fun onSceneChanged(newScene: String, origin: String) + } + + internal var onSceneChangedListener: OnSceneChangedListener? = null + internal set(value) { + field = value + if (value != null && startedStateOwner.active() && !TextUtils.isEmpty(recentScene)) { + value.onSceneChanged(recentScene, "") + } + } + + var recentScene = "" + private set(value) { + onSceneChangedListener?.safeApply(TAG) { + runningHandler.post { onSceneChanged(value, field) } + } + field = value + } + + private val delayedPauseRunnable = Runnable { + dispatchPauseIfNeeded() + dispatchStopIfNeeded() + } + + fun retainedActivities(): Map { + val map = HashMap() + Runtime.getRuntime().gc() + + val cpy = destroyedActivities.entries.toTypedArray() + + for (e in cpy) { + val k = e.key ?: continue + k.javaClass.simpleName.let { + var count = map.getOrPut(it, { 0 }) + map[it] = ++count + } + } + + return map + } + + private fun activityCreated(activity: Activity) { + val isEmptyBefore = createdActivities.isEmpty() + createdActivities.synchronized { + put(activity) + } + + if (isEmptyBefore) { + (createdStateOwner as AsyncOwner).turnOnAsync() + } + } + + private fun activityStarted(activity: Activity) { + val isEmptyBefore = startedActivities.isEmpty() + startedActivities.put(activity) + + if (isEmptyBefore && stopSent) { + (startedStateOwner as AsyncOwner).turnOnAsync() + } + } + + private fun activityResumed(activity: Activity) { + val isEmptyBefore = resumedActivities.isEmpty() + resumedActivities.put(activity) + if (isEmptyBefore) { + if (pauseSent) { + (resumedStateOwner as AsyncOwner).turnOnAsync() + pauseSent = false + } else { + runningHandler.removeCallbacks(delayedPauseRunnable) + } + } + } + + private fun activityPaused(activity: Activity) { + resumedActivities.remove(activity) + + if (resumedActivities.isEmpty()) { + runningHandler.postDelayed(delayedPauseRunnable, TIMEOUT_MS) + } + } + + private fun activityStopped(activity: Activity) { + startedActivities.remove(activity) + dispatchStopIfNeeded() + } + + private fun activityDestroyed(activity: Activity) { + createdActivities.synchronized { + remove(activity) + if (this.isEmpty()) { + (createdStateOwner as AsyncOwner).turnOffAsync() + } + } + destroyedActivities.put(activity) + // fallback remove + startedActivities.remove(activity)?.let { + MatrixLog.w( + TAG, + "removed [$activity] when destroy, maybe something wrong with onStart/onStop callback" + ) + } + resumedActivities.remove(activity)?.let { + MatrixLog.w( + TAG, + "removed [$activity] when destroy, maybe something wrong with onResume/onPause callback" + ) + } + } + + private fun dispatchPauseIfNeeded() { + if (resumedActivities.isEmpty()) { + pauseSent = true + (resumedStateOwner as AsyncOwner).turnOffAsync() + } + } + + private fun dispatchStopIfNeeded() { + if (startedActivities.isEmpty() && pauseSent) { + stopSent = true + (startedStateOwner as AsyncOwner).turnOffAsync() + } + } + + private fun attach(app: Application) { + startedStateOwner.observeForever(DefaultLifecycleObserver()) + + app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + activityCreated(activity) + } + + override fun onActivityStarted(activity: Activity) { + recentScene = activity.javaClass.name + updateScene(activity) + activityStarted(activity) + } + + override fun onActivityResumed(activity: Activity) { + activityResumed(activity) + } + + override fun onActivityPaused(activity: Activity) { + activityPaused(activity) + } + + override fun onActivityStopped(activity: Activity) { + activityStopped(activity) + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityDestroyed(activity: Activity) { + activityDestroyed(activity) + } + }) + } + + private val componentToProcess by lazy { HashMap() } + + private fun isComponentOfProcess(component: ComponentName?, process: String?): Boolean { + if (component == null) { + return false + } + + if (component.packageName != packageName) { + return false + } + + if (activityInfoArray == null) { + // init failed + return true + } + + return process == componentToProcess.getOrPut(component.className) { + val info = activityInfoArray!!.find { it.name == component.className } + if (info == null) { + MatrixLog.e(TAG, "got task info not appeared in package manager $info") + packageName!! + } else { + info.processName + } + } + } + + private fun ActivityManager.RecentTaskInfo.belongsTo(processName: String?): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val i = isComponentOfProcess(this.baseIntent.component, processName) + val o = isComponentOfProcess(this.origActivity, processName) + val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + isComponentOfProcess(this.baseActivity, processName) + } else { + false + } + val t = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + isComponentOfProcess(this.topActivity, processName) + } else { + false + } + + i || o || b || t + } else { + false + } + } + + @JvmStatic + fun hasRunningAppTask(): Boolean { + if (activityManager == null) { + throw IllegalStateException("NOT initialized yet") + } + return safeLet(TAG, defVal = true) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activityManager!!.appTasks + .filter { + it.taskInfo.belongsTo(processName) + }.onEach { + MatrixLog.i(TAG, "$processName task: ${it.taskInfo.contentToString()}") + }.any { + MatrixLog.d(TAG, "hasRunningAppTask run any") + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { + it.taskInfo.isRunning + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { + it.taskInfo.numActivities > 0 + } + else -> { + @Suppress("DEPRECATION") + it.taskInfo.id == -1 // // If it is not running, this will be -1 + } + } + } + } else { + false + } + } + } + + @JvmStatic + fun getRunningAppTasksOf(processName: String): Array { + if (activityManager == null) { + throw IllegalStateException("NOT initialized yet") + } + return safeLet(TAG, defVal = emptyArray()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activityManager!!.appTasks + .filter { + it.taskInfo.belongsTo(processName) + }.toTypedArray() + } else { + emptyArray() + } + } + } + + // ========================== extension for compatibility ========================== // + + private val mListeners = HashSet() + + @get:JvmName("isProcessForeground") + @Volatile + var isProcessForeground = false + private set + + // compat + var visibleScene = "default" + private set + + // compat + var currentFragmentName: String? = null + /** + * must set after [Activity#onStart] + */ + set(value) { + MatrixLog.i(TAG, "[setCurrentFragmentName] fragmentName: $value") + field = value + + if (value != null) { + updateScene(value) + } else { + updateScene("?") + } + } + + fun addListener(listener: IAppForeground) { + synchronized(mListeners) { + mListeners.add(listener) + } + } + + fun removeListener(listener: IAppForeground) { + synchronized(mListeners) { + mListeners.remove(listener) + } + } + + private fun updateScene(activity: Activity) { + visibleScene = activity.javaClass.name + } + + private fun updateScene(scene: String) { + visibleScene = scene + } + + class DefaultLifecycleObserver : ISerialObserver { + private fun onDispatchForeground() { + if (isProcessForeground) { + return + } + MatrixLog.i(TAG, "onForeground... visibleScene[$visibleScene@$processName]") + MatrixLifecycleThread.executor.execute { + isProcessForeground = true + synchronized(mListeners) { + for (listener in mListeners) { + listener.onForeground(true) + } + } + } + } + + private fun onDispatchBackground() { + if (!isProcessForeground) { + return + } + MatrixLog.i(TAG, "onBackground... visibleScene[$visibleScene@$processName]") + MatrixLifecycleThread.executor.execute { + isProcessForeground = false + synchronized(mListeners) { + for (listener in mListeners) { + listener.onForeground(false) + } + } + } + } + + override fun on() = onDispatchForeground() + + override fun off() = onDispatchBackground() + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/TimerChecker.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/TimerChecker.kt new file mode 100644 index 000000000..b8b9926ae --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/owners/TimerChecker.kt @@ -0,0 +1,92 @@ +package com.tencent.matrix.lifecycle.owners + +import com.tencent.matrix.lifecycle.MatrixLifecycleThread +import com.tencent.matrix.util.MatrixLog +import kotlin.math.min + +/** + * Created by Yves on 2021/11/24 + */ +internal abstract class TimerChecker( + private val tag: String, + private val maxIntervalMillis: Long, + private val maxCheckTimes: Int = -1 // infinity by default +) { + + private val runningHandler by lazy { MatrixLifecycleThread.handler } + + /** + * The initial delay interval is 12 + 31 = 34(ms) + * Why 34? Removing foreground widgets like floating Views should be run in main thread + * and might depend on Activity background event, which is dispatched in Matrix handler thread. + * And for most cases, the onDetach would be called within 10ms. + * + * valid intervals + * 34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,...,$max + * + */ + private class IntervalFactory(private val maxVal: Long) { + private val initialInterval = arrayOf(13L, 21L) + private var fibo = initialInterval.copyOf() + + fun next(): Long { + return min((fibo[0] + fibo[1]).also { fibo[0] = fibo[1]; fibo[1] = it }, maxVal) + } + + fun reset() { + fibo = initialInterval.copyOf() + } + } + + private val intervalFactory by lazy { IntervalFactory(maxIntervalMillis) } + + private val task by lazy { + object : Runnable { + override fun run() { + MatrixLog.i(tag, "run check task") + if (!action()) { + postTimes = 0 // reset so that check task can be resume by checkAndPostIfNeeded + } else if (maxCheckTimes == -1 || postTimes++ < maxCheckTimes) { + val interval = intervalFactory.next() + MatrixLog.i(tag, "need recheck: next $interval") + runningHandler.postDelayed(this, interval) + } else { + MatrixLog.i(tag, "paused polling check") + } + } + } + } + + @Volatile + private var postTimes = 0 + + /** + * return true if need recheck + */ + protected abstract fun action(): Boolean + + fun checkAndPostIfNeeded(): Boolean { + MatrixLog.i(tag, "checkAndPostIfNeeded") + runningHandler.removeCallbacks(task) + if (action()) { + runningHandler.postDelayed(task, intervalFactory.next()) + return true + } + return false + } + + fun post() { + intervalFactory.reset() + val interval = intervalFactory.next() + MatrixLog.i(tag, "post check: $interval") + runningHandler.removeCallbacks(task) + runningHandler.postDelayed(task, interval) + } + + fun stop() { + postTimes = 0 + MatrixLog.i(tag, "stop") + intervalFactory.reset() + runningHandler.removeCallbacks(task) + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/DispatcherStateOwner.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/DispatcherStateOwner.kt new file mode 100644 index 000000000..c25d0b9db --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/DispatcherStateOwner.kt @@ -0,0 +1,129 @@ +package com.tencent.matrix.lifecycle.supervisor + +import android.app.Application +import android.content.Context +import com.tencent.matrix.lifecycle.* +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.safeApply +import java.util.concurrent.ConcurrentHashMap + +/** + * cross process StatefulOwner + * + * DispatcherStateOwner sends attached owner state changes to supervisor + * Supervisor sync the app states back through [dispatchOn] and [dispatchOff] + * + * Created by Yves on 2021/12/2 + */ +@Suppress("LeakingThis") +internal open class DispatcherStateOwner( + reduceOperator: (stateful: Collection) -> Boolean, + val attachedSource: IStatefulOwner, + val name: String +) : MultiSourceStatefulOwner(reduceOperator), ISerialObserver { + + companion object { + private val dispatchOwners = ConcurrentHashMap() + + fun ownersToProcessTokens(context: Context) = + dispatchOwners.values + .map { ProcessToken.current(context, it.name, it.attachedSource.active()) } + .toTypedArray() + + fun dispatchOn(name: String) { + dispatchOwners[name]?.dispatchOn() + } + + fun dispatchOff(name: String) { + dispatchOwners[name]?.dispatchOff() + } + + /** + * call from supervisor + */ + fun syncStates(supervisorToken: ProcessToken, scene: String) { + if (!ProcessSupervisor.isSupervisor) { + throw IllegalStateException("call forbidden") + } + dispatchOwners.forEach { + val active = it.value.active() + MatrixLog.i(ProcessSupervisor.tag, "syncStates: ${it.key} $active") + ProcessSubordinate.manager.dispatchState(supervisorToken, scene, it.key, active) + } + } + + fun attach(application: Application) { + dispatchOwners.forEach { + it.value.apply { + attachedObserver?.let { o -> attachedSource.removeObserver(o) } // prevent double-observe + attachedObserver = object : IStateObserver, ISerialObserver { + override fun on() { + MatrixLog.d(ProcessSupervisor.tag, "attached ${it.key} turned ON") + safeApply("${ProcessSupervisor.tag}.${it.key}") { + ProcessSupervisor.supervisorProxy?.onStateChanged( + ProcessToken.current(application, it.key, true) + ) + } + } + + override fun off() { + MatrixLog.d(ProcessSupervisor.tag, "attached ${it.key} turned OFF") + safeApply("${ProcessSupervisor.tag}.${it.key}") { + ProcessSupervisor.supervisorProxy?.onStateChanged( + ProcessToken.current(application, it.key, false) + ) + } + } + } + attachedSource.observeForever(attachedObserver!!) + } + } + MatrixLog.i(ProcessSupervisor.tag, "DispatcherStateOwners attached") + } + + fun detach() { + dispatchOwners.forEach { + it.value.apply { + attachedObserver?.let { o -> attachedSource.removeObserver(o) } + attachedObserver = null + } + } + MatrixLog.i(ProcessSupervisor.tag, "DispatcherStateOwners detached") + } + + fun observe(observer: (stateName: String, state: Boolean) -> Unit) { + dispatchOwners.forEach { + it.value.observeForever(object : IStateObserver, ISerialObserver { + override fun on() { + observer.invoke(it.key, true) + } + + override fun off() { + observer.invoke(it.key, false) + } + }) + } + } + + fun addSourceOwner(name: String, source: StatefulOwner) { + dispatchOwners[name]?.addSourceOwner(source) + } + + fun removeSourceOwner(name: String, source: StatefulOwner) { + dispatchOwners[name]?.removeSourceOwner(source) + } + } + + private var attachedObserver: IStateObserver? = null + + init { + dispatchOwners[name] = this + } + + private fun dispatchOn() = turnOn() + private fun dispatchOff() = turnOff() + + override fun toString(): String { + return "DispatcherStateOwner_$name" + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessSubordinate.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessSubordinate.kt new file mode 100644 index 000000000..7faf4563f --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessSubordinate.kt @@ -0,0 +1,259 @@ +package com.tencent.matrix.lifecycle.supervisor + +import android.app.Application +import android.os.Build +import android.os.DeadObjectException +import android.os.Process +import android.text.TextUtils +import com.tencent.matrix.lifecycle.MatrixLifecycleThread +import com.tencent.matrix.lifecycle.owners.ForegroundServiceLifecycleOwner +import com.tencent.matrix.lifecycle.owners.OverlayWindowLifecycleOwner +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner +import com.tencent.matrix.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +internal interface IProcessListener { + fun addDyingListener(listener: (recentScene: String?, processName: String?, pid: Int?) -> Boolean) + fun removeDyingListener(listener: (recentScene: String?, processName: String?, pid: Int?) -> Boolean) + fun addDeathListener(listener: (recentScene: String?, processName: String?, pid: Int?, isLruKill: Boolean?) -> Unit) + fun removeDeathListener(listener: (recentScene: String?, processName: String?, pid: Int?, isLruKill: Boolean?) -> Unit) + fun getRecentScene(): String +} + +/** + * Created by Yves on 2021/12/30 + */ +internal object ProcessSubordinate { + + private val TAG by lazy { "${ProcessSupervisor.tag}.Subordinate" } + + internal val manager by lazy { + if (ProcessSupervisor.isSupervisor) { + Manager() + } else { + throw IllegalAccessException("NOT allow for subordinate processes") + } + } + + internal class Manager { + private val subordinateProxies by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ConcurrentHashMap() } + + private fun Map.forEachSafe(action: (Map.Entry) -> Unit) { + forEach { e -> + safeLet(unsafe = { action(e) }, failed = { + MatrixLog.printErrStackTrace(TAG, it, "${e.key.pid}${e.key.name}") + if (it is DeadObjectException) { + MatrixLog.e(TAG, "remote process of proxy is dead, remove proxy: ${e.key}") + subordinateProxies.remove(e.key) + } + }) + } + } + + + fun addProxy(process: ProcessToken, subordinate: ISubordinateProxy) = + subordinateProxies.put(process, subordinate) + + fun removeProxy(process: ProcessToken) = + subordinateProxies.remove(process) + + /** + * NOTICE: avoid loopback + */ + fun dispatchState( + supervisorToken: ProcessToken, + scene: String?, + stateName: String?, + state: Boolean + ) { + subordinateProxies.filter { it.key != supervisorToken } + .forEachSafe { it.value.dispatchState(scene, stateName, state) } + } + + fun dispatchKill(scene: String?, targetProcess: String?, targetPid: Int) { + subordinateProxies.forEachSafe { + it.value.dispatchKill( + scene, + targetProcess, + targetPid + ) + } + } + + fun dispatchDeath( + scene: String?, + targetProcess: String?, + targetPid: Int, + isLruKill: Boolean + ) { + subordinateProxies.forEachSafe { + it.value.dispatchDeath( + scene, + targetProcess, + targetPid, + isLruKill + ) + } + } + + fun getMemInfo(): Array { + val memInfoList = ArrayList() + subordinateProxies.forEachSafe { + it.value.memInfo?.let { m-> + memInfoList.add(m) + } + } + return memInfoList.toTypedArray() + } + } + + private val dyingListeners = + ArrayList<(recentScene: String?, processName: String?, pid: Int?) -> Boolean>() + + private fun ArrayList<(recentScene: String?, processName: String?, pid: Int?) -> Boolean>.invokeAll( + recentScene: String?, + processName: String?, + pid: Int? + ): Boolean { + var rescue = false + forEach { + safeApply(ProcessSupervisor.tag) { + val r = it.invoke(recentScene, processName, pid) + if (r) { + MatrixLog.e(ProcessSupervisor.tag, "${it.javaClass} try to rescue process") + } + rescue = rescue || r + } + } + return rescue + } + + private val deathListeners = + ArrayList<(recentScene: String?, processName: String?, pid: Int?, isLruKill: Boolean?) -> Unit>() + + private fun ArrayList<(recentScene: String?, processName: String?, pid: Int?, isLruKill: Boolean?) -> Unit>.invokeAll( + recentScene: String?, + processName: String?, + pid: Int?, + isLruKill: Boolean? + ) = forEach { + safeApply(ProcessSupervisor.tag) { + it.invoke(recentScene, processName, pid, isLruKill) + } + } + + internal val processListener = object : IProcessListener { + + override fun addDyingListener(listener: (recentScene: String?, processName: String?, pid: Int?) -> Boolean) { + dyingListeners.add(listener) + } + + override fun removeDyingListener(listener: (recentScene: String?, processName: String?, pid: Int?) -> Boolean) { + dyingListeners.remove(listener) + } + + override fun addDeathListener(listener: (scene: String?, processName: String?, pid: Int?, isLruKill: Boolean?) -> Unit) { + deathListeners.add(listener) + } + + override fun removeDeathListener(listener: (scene: String?, processName: String?, pid: Int?, isLruKill: Boolean?) -> Unit) { + deathListeners.remove(listener) + } + + override fun getRecentScene() = SupervisorService.recentScene.let { + if (!TextUtils.isEmpty(it)) { + it + } else { + safeLet(ProcessSupervisor.tag, defVal = "") { + ProcessSupervisor.supervisorProxy?.recentScene ?: "" + } + } + } + } + + // ISubordinateProxy + + internal fun getSubordinate(app: Application): ISubordinateProxy.Stub = Subordinate(app) + + class Subordinate(val app: Application) : ISubordinateProxy.Stub() { + + private var rescued: Boolean = false + + override fun dispatchState(scene: String, stateName: String, state: Boolean) { + safeApply(TAG) { + if (state) { + DispatcherStateOwner.dispatchOn(stateName) + } else { + DispatcherStateOwner.dispatchOff(stateName) + } + } + } + + override fun dispatchKill(scene: String, targetProcess: String, targetPid: Int) { + safeApply(TAG) { + MatrixLog.d(ProcessSupervisor.tag, "receive kill target: $targetPid-$targetProcess") + val toRescue = dyingListeners.invokeAll(scene, targetProcess, targetPid) + if (targetProcess == MatrixUtil.getProcessName(app) && Process.myPid() == targetPid) { + val token = ProcessToken.current(app) + if (toRescue && rescued) { + rescued = true + ProcessSupervisor.supervisorProxy?.onProcessRescuedFromKill(token) + MatrixLog.e(ProcessSupervisor.tag, "rescued once !!!") + return + } + + MatrixLifecycleThread.handler.postDelayed({ + if (!ProcessUILifecycleOwner.startedStateOwner.active() + && !ForegroundServiceLifecycleOwner.hasForegroundService() + && !OverlayWindowLifecycleOwner.hasVisibleWindow() + ) { + safeApply(TAG) { + ProcessSupervisor.supervisorProxy?.onProcessKilled(token) + } + MatrixLog.e( + ProcessSupervisor.tag, + "actual kill !!! supervisor = ${ProcessSupervisor.supervisorProxy}" + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ProcessUILifecycleOwner.getRunningAppTasksOf( + MatrixUtil.getProcessName(app) + ).forEach { + MatrixLog.e( + ProcessSupervisor.tag, + "removed task ${it.taskInfo.contentToString()}" + ) + it.finishAndRemoveTask() + } + } + Process.killProcess(Process.myPid()) + } else { + safeApply(TAG) { + ProcessSupervisor.supervisorProxy?.onProcessKillCanceled(token) + } + MatrixLog.i(ProcessSupervisor.tag, "recheck: process is on foreground") + } + }, TimeUnit.SECONDS.toMillis(10)) + } + } + } + + override fun dispatchDeath( + scene: String, + targetProcess: String, + targetPid: Int, + isLruKill: Boolean + ) { + safeApply(TAG) { + deathListeners.invokeAll(scene, targetProcess, targetPid, isLruKill) + } + } + + override fun getMemInfo(): MemInfo = safeLet( + TAG, + defVal = MemInfo(amsPssInfo = PssInfo(), debugPssInfo = PssInfo()) + ) { MemInfo.getCurrentProcessFullMemInfo() } + + } + +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessSupervisor.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessSupervisor.kt new file mode 100644 index 000000000..5926389bf --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessSupervisor.kt @@ -0,0 +1,254 @@ +package com.tencent.matrix.lifecycle.supervisor + +import android.app.Application +import android.content.ComponentName +import android.content.Context.* +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.PackageManager +import android.os.IBinder +import android.util.Log +import com.tencent.matrix.lifecycle.* +import com.tencent.matrix.lifecycle.owners.* +import com.tencent.matrix.util.* + +/** + * Created by Yves on 2021/9/26 + */ +const val LRU_KILL_SUCCESS = 1 +const val LRU_KILL_RESCUED = 2 +const val LRU_KILL_CANCELED = 3 +const val LRU_KILL_NOT_FOUND = 4 + +/** + * Created by Yves on 2021/10/22 + */ +data class SupervisorConfig( + val enable: Boolean = false, + /** + * If you specify an existing process as Supervisor but don't want to modify the boot order + * pls set [autoCreate] to false and than it would be init manually by startService when + * Matrix init in the Supervisor process + */ + val autoCreate: Boolean = false, + val lruKillerWhiteList: List = emptyList() +) + +// @formatter:off +/** + * Usage: + * similar to [ProcessUIStartedStateOwner] + * + * State-ON: + * [ProcessUIStartedStateOwner] of ANY process is active, in other words, at least one processes is in foreground + */ +object AppUIForegroundOwner: IForegroundStatefulOwner by ProcessSupervisor.appUIForegroundOwner + +/** + * Usage: + * similar to [ProcessExplicitBackgroundOwner] + * + * State-ON: + * [ProcessExplicitBackgroundOwner] of ALL process is active, in other words, ALL processes are in background + */ +object AppExplicitBackgroundOwner: IBackgroundStatefulOwner by ProcessSupervisor.appExplicitBackgroundOwner + +/** + * Usage: + * similar to [ProcessDeepBackgroundOwner] + * + * State-ON: + * [ProcessDeepBackgroundOwner] of ALL process is active, in other words, ALL processes are in deep background. + */ +object AppDeepBackgroundOwner: IBackgroundStatefulOwner by ProcessSupervisor.appDeepBackgroundOwner + +/** + * State-ON: + * [ProcessStagedBackgroundOwner] of ANY process is active, in other words, at least one process has AppTask in the recent screen + */ +object AppStagedBackgroundOwner: IBackgroundStatefulOwner by ProcessSupervisor.appStagedBackgroundOwner +// @formatter:on + +object ProcessSupervisor : IProcessListener by ProcessSubordinate.processListener { + + private const val TAG = "Matrix.ProcessSupervisor" + + internal val tag by lazy { "${TAG}_${suffix()}" } + + private fun suffix(): String { + return if (MatrixUtil.isInMainProcess(application)) { + "Main" + } else { + val split = MatrixUtil.getProcessName(application).split(":").toTypedArray() + if (split.size > 1) { + split[1] + } else { + "unknown" + } + } + } + + @Volatile + private var application: Application? = null + internal var config: SupervisorConfig? = null + + val isAppUIForeground: Boolean + get() = appUIForegroundOwner.active() + + val isAppExplicitBackground: Boolean + get() = appExplicitBackgroundOwner.active() + + val isAppStagedBackground: Boolean + get() = appStagedBackgroundOwner.active() + + val isAppDeepBackground: Boolean + get() = appDeepBackgroundOwner.active() + + val isSupervisor: Boolean by lazy { + if (application == null) { + throw IllegalStateException("Supervisor NOT initialized yet or Supervisor is disabled!!!") + } + + val serviceInfo = safeLetOrNull(TAG) { + application!!.packageManager.getPackageInfo( + application!!.packageName, PackageManager.GET_SERVICES + ).services.find { + it.name == SupervisorService::class.java.name + } + } + + // serviceInfo might be null + return@lazy MatrixUtil.getProcessName(application!!) == serviceInfo?.processName || SupervisorService.isSupervisor + } + + @Volatile + internal var supervisorProxy: ISupervisorProxy? = null + + internal const val STARTED_STATE_OWNER = "StartedStateOwner" + internal const val EXPLICIT_BACKGROUND_OWNER = "ExplicitBackgroundOwner" + internal const val DEEP_BACKGROUND_OWNER = "DeepBackgroundOwner" + + // @formatter:off + internal val appUIForegroundOwner: IForegroundStatefulOwner = object : DispatcherStateOwner(ReduceOperators.OR, ProcessUILifecycleOwner.startedStateOwner, STARTED_STATE_OWNER), IForegroundStatefulOwner {} + internal val appExplicitBackgroundOwner: IBackgroundStatefulOwner = object : DispatcherStateOwner(ReduceOperators.AND, ProcessExplicitBackgroundOwner, EXPLICIT_BACKGROUND_OWNER), IBackgroundStatefulOwner {} + internal val appDeepBackgroundOwner: IBackgroundStatefulOwner = object : DispatcherStateOwner(ReduceOperators.AND, ProcessDeepBackgroundOwner, DEEP_BACKGROUND_OWNER), IBackgroundStatefulOwner {} + // @formatter:on + + private class AppStagedBackgroundOwner( + private val delegate: MultiSourceStatefulOwner = object : MultiSourceStatefulOwner( + ReduceOperators.AND, + appExplicitBackgroundOwner.shadow(true), + appDeepBackgroundOwner.reverse().shadow(true) + ), ISerialObserver {} + ) : IBackgroundStatefulOwner, IStatefulOwner by delegate + + internal val appStagedBackgroundOwner: IBackgroundStatefulOwner = AppStagedBackgroundOwner() + + fun init(app: Application, config: SupervisorConfig?): Boolean { + this.config = config + if (true != config?.enable) { + MatrixLog.i(TAG, "Supervisor is disabled") + return false + } + application = app + if (isSupervisor) { + initSupervisor(app) + } + inCharge(config.autoCreate, app) + return isSupervisor + } + + fun backgroundLruKill(killedResult: (result: Int, process: String?, pid: Int) -> Unit) = + SupervisorService.instance?.backgroundLruKill(killedResult) + + private fun initSupervisor(app: Application) { + SupervisorService.start(app) + MatrixLog.i(tag, "initSupervisor") + } + + /** + * call by all processes + */ + private fun inCharge(autoCreate: Boolean, app: Application) { + + val intent = Intent(app, SupervisorService::class.java) + + Log.i(tag, "bind to Supervisor") + + SupervisorPacemaker.install(app) + + val conn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + MatrixLifecycleThread.handler.post { // do NOT run ipc in main thread + SupervisorPacemaker.uninstall() + SubordinatePacemaker.uninstall(app) + supervisorProxy = ISupervisorProxy.Stub.asInterface(service) + MatrixLog.i(tag, "on Supervisor Connected $supervisorProxy") + + ProcessUILifecycleOwner.onSceneChangedListener = + object : ProcessUILifecycleOwner.OnSceneChangedListener { + override fun onSceneChanged(newScene: String, origin: String) { + MatrixLog.d(tag, "onSceneChanged: $origin -> $newScene") + supervisorProxy?.safeApply(tag) { onSceneChanged(newScene) } + } + } + + supervisorProxy?.safeApply(tag, msg = "supervisor is $supervisorProxy") { + registerSubordinate( + DispatcherStateOwner.ownersToProcessTokens(app), + ProcessSubordinate.getSubordinate(app) + ) + } + DispatcherStateOwner.attach(application!!) + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + MatrixLifecycleThread.handler.post { + MatrixLog.e(tag, "onServiceDisconnected $name") + supervisorProxy = null + ProcessUILifecycleOwner.onSceneChangedListener = null + DispatcherStateOwner.detach() + SupervisorPacemaker.install(app) + // try to re-bind supervisor, but don't auto create here + safeApply(log = false) { app.unbindService(this) } + + safeLet({ + app.bindService(intent, this, BIND_WAIVE_PRIORITY) + MatrixLog.e(tag, "rebound supervisor") + }, failed = { + // install subordinate pacemaker + MatrixLog.printErrStackTrace(tag, it, "rebound supervisor failed") + SubordinatePacemaker.install(app) { + safeApply(tag) { + app.bindService(intent, this, BIND_WAIVE_PRIORITY) + SubordinatePacemaker.uninstall(app) + MatrixLog.i(tag, "subordinate pacemaker rebound supervisor") + } + } + }) + } + } + } + + app.bindService( + intent, + conn, + if (autoCreate) (BIND_AUTO_CREATE) else BIND_WAIVE_PRIORITY + ) + + MatrixLog.i(tag, "inCharge") + } + + fun getAllProcessMemInfo() : Array? { + if (application == null) { + MatrixLog.e(tag, "Supervisor NOT initialized yet or Supervisor is disabled!!!") + return null + } + if (!isSupervisor) { + MatrixLog.e(tag, "Only support for supervisor process") + return null + } + return ProcessSubordinate.manager.getMemInfo() + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessToken.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessToken.kt new file mode 100644 index 000000000..9b11c1835 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/ProcessToken.kt @@ -0,0 +1,90 @@ +package com.tencent.matrix.lifecycle.supervisor + +import android.content.Context +import android.os.* +import com.tencent.matrix.util.MatrixUtil + +/** + * Created by Yves on 2021/11/11 + */ +class ProcessToken : Parcelable { + val binder: IBinder + val pid: Int + val name: String + val statefulName: String + val state: Boolean + + companion object { + + @JvmStatic + fun current(context: Context, statefulName: String = "", state: Boolean = false) = ProcessToken( + Process.myPid(), + MatrixUtil.getProcessName(context), + statefulName, + state + ) + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(src: Parcel): ProcessToken { + return ProcessToken(src) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + constructor(pid: Int, processName: String, statefulName: String, state: Boolean) { + this.binder = Binder() + this.pid = pid + this.name = processName + this.statefulName = statefulName + this.state = state + } + + constructor(src: Parcel) { + this.binder = src.readStrongBinder() + this.pid = src.readInt() + this.name = src.readString() ?: "" + this.statefulName = src.readString() ?: "" + this.state = src.readInt() != 0 + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeStrongBinder(binder) + dest.writeInt(pid) + dest.writeString(name) + dest.writeString(statefulName) + dest.writeInt(if (state) 1 else 0) + } + + fun linkToDeath(recipient: IBinder.DeathRecipient) { + binder.linkToDeath(recipient, 0) + } + + override fun equals(other: Any?): Boolean { + if (other == null) { + return false + } + if (other !is ProcessToken) { + return false + } + return name == other.name && pid == other.pid + } + + override fun hashCode(): Int { + var result = pid + result = 31 * result + name.hashCode() + return result + } + + override fun toString(): String { + return "ProcessToken(pid=$pid, name='$name', statefulName = $statefulName, state = $state)" + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SubordinatePacemaker.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SubordinatePacemaker.kt new file mode 100644 index 000000000..2ea5bec3d --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SubordinatePacemaker.kt @@ -0,0 +1,84 @@ +package com.tencent.matrix.lifecycle.supervisor + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.tencent.matrix.lifecycle.IStateObserver +import com.tencent.matrix.lifecycle.MatrixLifecycleThread +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.MatrixUtil +import com.tencent.matrix.util.safeApply + +/** + * Created by Yves on 2022/2/10 + */ +internal object SubordinatePacemaker : BroadcastReceiver() { + + private const val SUPERVISOR_INSTALLED = "SUPERVISOR_INSTALLED" + + private var packageName: String? = null + private val permission by lazy { "${packageName!!}.matrix.permission.PROCESS_SUPERVISOR" } + + @Volatile + private var pacemaker: IStateObserver? = null + + private var callback: (() -> Unit)? = null + + fun install(context: Context?, callback: () -> Unit) { + if (pacemaker != null) { + MatrixLog.e(ProcessSupervisor.tag, "SubordinatePacemaker: already installed") + return + } + if (ProcessSupervisor.isSupervisor) { + return + } + this.callback = callback + packageName = MatrixUtil.getPackageName(context) + val filter = IntentFilter() + filter.addAction(SUPERVISOR_INSTALLED) + context?.registerReceiver(this, filter, permission, null) + + pacemaker = object : IStateObserver { + override fun on() { + MatrixLifecycleThread.handler.post { + MatrixLog.i(ProcessSupervisor.tag, "SubordinatePacemaker: callback when foreground") + callback.invoke() + } + } + + override fun off() {} + } + ProcessUILifecycleOwner.startedStateOwner.observeForever(pacemaker!!) + } + + fun uninstall(context: Context?) { + if (pacemaker!= null) { + ProcessUILifecycleOwner.startedStateOwner.removeObserver(pacemaker!!) + pacemaker = null + safeApply(ProcessSupervisor.tag) { + context?.unregisterReceiver(this) + } + MatrixLog.i(ProcessSupervisor.tag, "SubordinatePacemaker: uninstalled") + } + } + + fun notifySupervisorInstalled(context: Context?) { + packageName = MatrixUtil.getPackageName(context) + Intent(SUPERVISOR_INSTALLED).apply { + context?.sendBroadcast(this, permission) + } + } + + override fun onReceive(context: Context?, intent: Intent?) { + when(intent?.action) { + SUPERVISOR_INSTALLED -> { + MatrixLifecycleThread.handler.post { + MatrixLog.i(ProcessSupervisor.tag, "SubordinatePacemaker: callback when supervisor installed") + callback?.invoke() + } + } + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SupervisorPacemaker.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SupervisorPacemaker.kt new file mode 100644 index 000000000..7dc4c2456 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SupervisorPacemaker.kt @@ -0,0 +1,97 @@ +package com.tencent.matrix.lifecycle.supervisor + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Process +import com.tencent.matrix.lifecycle.IStateObserver +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner +import com.tencent.matrix.util.* + +/** + * Created by Yves on 2021/10/12 + */ +internal object SupervisorPacemaker : BroadcastReceiver() { + + private const val KEY_PROCESS_NAME = "KEY_PROCESS_NAME" + private const val KEY_PROCESS_PID = "KEY_PROCESS_PID" + + private const val TELL_SUPERVISOR_FOREGROUND = "TELL_SUPERVISOR_FOREGROUND" + + private var packageName: String? = null + private val permission by lazy { "${packageName!!}.matrix.permission.PROCESS_SUPERVISOR" } + + @Volatile + private var pacemaker: IStateObserver? = null + + private fun installPacemaker(context: Context?) { + if (pacemaker == null) { + pacemaker = object : IStateObserver { + override fun on() { + MatrixLog.i(ProcessSupervisor.tag, "pacemaker: call supervisor") + if (ProcessSupervisor.config!!.autoCreate) { + SupervisorService.start(context!!) + } else { + tellSupervisorForeground(context) + } + } + + override fun off() { + } + } + ProcessUILifecycleOwner.startedStateOwner.observeForever(pacemaker!!) + MatrixLog.i(ProcessSupervisor.tag, "pacemaker: install pacemaker") + } + } + + internal fun uninstall() { + if (pacemaker != null) { + ProcessUILifecycleOwner.startedStateOwner.removeObserver(pacemaker!!) + pacemaker = null + MatrixLog.i(ProcessSupervisor.tag, "pacemaker: uninstall pacemaker") + } + } + + internal fun install(context: Context?) { + packageName = MatrixUtil.getPackageName(context) + val filter = IntentFilter() + if (ProcessSupervisor.isSupervisor) { + filter.addAction(TELL_SUPERVISOR_FOREGROUND) + context?.registerReceiver(this, filter, permission, null) + MatrixLog.i(ProcessSupervisor.tag, "pacemaker: receiver installed") + } else { + installPacemaker(context) + } + } + + private fun tellSupervisorForeground(context: Context?) { + Intent(TELL_SUPERVISOR_FOREGROUND).apply { + putExtra(KEY_PROCESS_NAME, MatrixUtil.getProcessName(context)) + putExtra(KEY_PROCESS_PID, Process.myPid()) + context?.sendBroadcast(this, permission) + } + } + + override fun onReceive(context: Context, intent: Intent?) { + when (intent?.action) { + TELL_SUPERVISOR_FOREGROUND -> { + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(ProcessSupervisor.tag, "supervisor was disabled") + return + } + if (!ProcessSupervisor.isSupervisor) { + MatrixLog.e(ProcessSupervisor.tag, "ERROR: this is NOT supervisor process") + return + } + val processName = intent.getStringExtra(KEY_PROCESS_NAME) + val pid = intent.getIntExtra(KEY_PROCESS_PID, -1) + MatrixLog.i( + ProcessSupervisor.tag, + "pacemaker: receive TELL_SUPERVISOR_FOREGROUND from $pid-$processName" + ) + SupervisorService.start(context.applicationContext) + } + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SupervisorService.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SupervisorService.kt new file mode 100644 index 000000000..90ba8c342 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/lifecycle/supervisor/SupervisorService.kt @@ -0,0 +1,390 @@ +package com.tencent.matrix.lifecycle.supervisor + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.* +import com.tencent.matrix.lifecycle.MatrixLifecycleThread +import com.tencent.matrix.lifecycle.StatefulOwner +import com.tencent.matrix.util.* +import junit.framework.Assert +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue + +/** + * Created by Yves on 2021/11/11 + */ +class SupervisorService : Service() { + + companion object { + private const val TAG = "Matrix.ProcessSupervisor.Service" + + @Volatile + internal var isSupervisor = false + private set + + @Volatile + internal var recentScene: String = "" + + internal fun start(context: Context) = safeApply(TAG) { + if (instance != null) { + MatrixLog.e(TAG, "duplicated start") + return@safeApply + } + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return@safeApply + } + val intent = Intent(context, SupervisorService::class.java) + context.startService(intent) + MatrixLog.i(TAG, "start service") + } + + @Volatile + internal var instance: SupervisorService? = null + private set + } + + private val runningHandler = MatrixLifecycleThread.handler + + private val tokenRecord: TokenRecord = TokenRecord() + + private val backgroundProcessLru by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ConcurrentLinkedQueue() } + + private fun ConcurrentLinkedQueue.contentToString(): String { + val it: Iterator = iterator() + if (!it.hasNext()) return "[]" + + val sb = StringBuilder() + sb.append('[') + while (true) { + val e: ProcessToken = it.next() + sb.append("${e.pid}-${e.name}") + if (!it.hasNext()) return sb.append(']').toString() + sb.append(',').append(' ') + } + } + + private fun ConcurrentLinkedQueue.moveOrAddFirst(token: ProcessToken) { + remove(token) + add(token) + } + + private var targetKilledCallback: ((result: Int, target: String?, pid: Int) -> Unit)? = null + + private val binder = object : ISupervisorProxy.Stub() { + override fun registerSubordinate( + tokens: Array, + subordinateProxy: ISubordinateProxy + ) { + val pid = Binder.getCallingPid() + + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + + runningHandler.post { + MatrixLog.d( + TAG, + "supervisor called register, tokens(${tokens.size}): ${tokens.contentToString()}" + ) + + tokens.first().safeApply(TAG) { + tokenRecord.addToken(this) + ProcessSubordinate.manager.addProxy(this, subordinateProxy) + backgroundProcessLru.moveOrAddFirst(this) + MatrixLog.i( + TAG, + "CREATED: [${this.pid}-${name}] -> [${backgroundProcessLru.size}]${backgroundProcessLru.contentToString()}" + ) + + linkToDeath { + safeApply(TAG) { + val dead = tokenRecord.removeToken(pid) + val lruRemoveSuccess = backgroundProcessLru.remove(dead) + ProcessSubordinate.manager.removeProxy(dead) + val proxyRemoveSuccess = + RemoteProcessLifecycleProxy.removeProxy(dead) + ProcessSubordinate.manager.dispatchDeath( + recentScene, + dead.name, + dead.pid, + !lruRemoveSuccess && !proxyRemoveSuccess + ) + MatrixLog.i( + TAG, + "$pid-$dead was dead. is LRU kill? ${!lruRemoveSuccess && !proxyRemoveSuccess}" + ) + } + } + } + + tokens.forEach { + MatrixLog.d(TAG, "register: ${it.name}, ${it.statefulName}, ${it.state}") + RemoteProcessLifecycleProxy.getProxy(it).onStateChanged(it.state) + } + + if (tokenRecord.isEmpty()) { + MatrixLog.i( + TAG, + "stateRegister: no other process registered, ignore state changes" + ) + return@post + } + DispatcherStateOwner.syncStates( + ProcessToken.current(applicationContext), + recentScene + ) // sync state for new process + } + } + + override fun onStateChanged(token: ProcessToken) { + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + runningHandler.post { + MatrixLog.i( + TAG, + "onStateChanged: ${token.statefulName} ${token.state} ${token.name}" + ) + RemoteProcessLifecycleProxy.getProxy(token).onStateChanged(token.state) + onProcessStateChanged(token) + } + } + + private fun onProcessStateChanged(token: ProcessToken) { + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + if (ProcessSupervisor.EXPLICIT_BACKGROUND_OWNER == token.statefulName) { + if (token.state) { + backgroundProcessLru.moveOrAddFirst(token) + MatrixLog.i( + TAG, + "BACKGROUND: [${token.pid}-${token.name}] -> [${backgroundProcessLru.size}]${backgroundProcessLru.contentToString()}" + ) + } else { + backgroundProcessLru.remove(token) + MatrixLog.i( + TAG, + "FOREGROUND: [${token.pid}-${token.name}] <- [${backgroundProcessLru.size}]${backgroundProcessLru.contentToString()}" + ) + } + } + } + + override fun onSceneChanged(scene: String) { + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + SupervisorService.recentScene = scene + } + + override fun onProcessKilled(token: ProcessToken) { + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + runningHandler.post { + safeApply(TAG) { + targetKilledCallback?.invoke(LRU_KILL_SUCCESS, token.name, token.pid) + } + backgroundProcessLru.remove(token) + RemoteProcessLifecycleProxy.removeProxy(token) + MatrixLog.i( + TAG, + "KILL: [${token.pid}-${token.name}] X [${backgroundProcessLru.size}]${backgroundProcessLru.contentToString()}" + ) + } + } + + override fun onProcessRescuedFromKill(token: ProcessToken) { + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + runningHandler.post { + safeApply(TAG) { + targetKilledCallback?.invoke(LRU_KILL_RESCUED, token.name, token.pid) + } + } + } + + override fun onProcessKillCanceled(token: ProcessToken) { + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + runningHandler.post { + safeApply(TAG) { + targetKilledCallback?.invoke(LRU_KILL_CANCELED, token.name, token.pid) + } + } + } + + override fun getRecentScene() = if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + "" + } else { + SupervisorService.recentScene + } + } + + override fun onCreate() { + super.onCreate() + + MatrixLog.i(TAG, "onCreate") + isSupervisor = true + instance = this + + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + + DispatcherStateOwner.observe { stateName, state -> + if (tokenRecord.isEmpty()) { + MatrixLog.i(TAG, "observe: no other process registered, ignore state changes") + return@observe + } + MatrixLog.d(TAG, "supervisor dispatch $stateName $state") + ProcessSubordinate.manager.dispatchState( + ProcessToken.current(applicationContext), + recentScene, + stateName, + state + ) + } + + SubordinatePacemaker.notifySupervisorInstalled(applicationContext) + } + + override fun onBind(intent: Intent?): IBinder { + MatrixLog.d(TAG, "onBind") + return binder + } + + override fun onDestroy() { + super.onDestroy() + MatrixLog.e(TAG, "SupervisorService destroyed!!!") + instance = null + } + + internal fun backgroundLruKill( + killedCallback: (result: Int, process: String?, pid: Int) -> Unit + ) { + if (true != ProcessSupervisor.config?.enable) { + MatrixLog.e(TAG, "supervisor was disabled") + return + } + if (!isSupervisor) { + throw IllegalStateException("backgroundLruKill should only be called in supervisor") + } + + if (instance == null) { + throw IllegalStateException("not initialized yet !") + } + + targetKilledCallback = killedCallback + val candidate = backgroundProcessLru.firstOrNull { + it.name != MatrixUtil.getProcessName(this) + && !ProcessSupervisor.config!!.lruKillerWhiteList.contains(it.name) + } + + if (candidate != null) { +// DispatchReceiver.dispatchKill(this, candidate.name, candidate.pid) + ProcessSubordinate.manager.dispatchKill(recentScene, candidate.name, candidate.pid) + } else { + killedCallback.invoke(LRU_KILL_NOT_FOUND, null, -1) + } + } + + private class TokenRecord { + private val pidToToken: ConcurrentHashMap + by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ConcurrentHashMap() } + private val nameToToken: ConcurrentHashMap + by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ConcurrentHashMap() } + + fun addToken(token: ProcessToken) { + pidToToken[token.pid] = token + nameToToken[token.name] = token + } + + fun getToken(pid: Int) = pidToToken[pid] + fun getToken(name: String) = nameToToken[name] + + fun removeToken(pid: Int): ProcessToken { + val rm = pidToToken.remove(pid) + rm?.let { + nameToToken.remove(rm.name) + return rm + } + throw IllegalStateException("token with pid=$pid not found") + } + + fun isEmpty(): Boolean = + pidToToken.isEmpty() || pidToToken.all { it.key == Process.myPid() } + + fun removeToken(name: String): ProcessToken { + val rm = nameToToken.remove(name) + rm?.let { + pidToToken.remove(rm.pid) + return rm + } + throw IllegalStateException("token with name=$name not found") + } + } + + private class RemoteProcessLifecycleProxy(val token: ProcessToken) : StatefulOwner() { + + init { + DispatcherStateOwner.addSourceOwner( + token.statefulName, + this + ) + } + + companion object { + + private val processProxies by lazy { ConcurrentHashMap>() } + + fun getProxy(token: ProcessToken) = + processProxies.getOrPut(token, { ConcurrentHashMap() }) + .getOrPut(token.statefulName, { RemoteProcessLifecycleProxy(token) })!! + + + fun removeProxy(token: ProcessToken): Boolean { + val proxies = processProxies.remove(token) + if (proxies == null || proxies.isEmpty()) { + return false + } + proxies.forEach { + DispatcherStateOwner.removeSourceOwner(it.key, it.value) + } + return true + } + + fun profile() { + processProxies.forEach { + it.value.forEach { p -> + MatrixLog.d(TAG, "===> ${p.value}") + } + } + } + } + + fun onStateChanged(state: Boolean) = if (state) { + turnOn() + } else { + turnOff() + } + + override fun toString(): String { + return "OwnerProxy_${token.statefulName}_${active()}@${hashCode()}_${token.name}_${token.pid}" + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/Plugin.java b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/Plugin.java index e953b8efc..a218a6f37 100644 --- a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/Plugin.java +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/Plugin.java @@ -18,7 +18,7 @@ import android.app.Application; -import com.tencent.matrix.AppActiveMatrixDelegate; +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner; import com.tencent.matrix.listeners.IAppForeground; import com.tencent.matrix.report.Issue; import com.tencent.matrix.report.IssuePublisher; @@ -56,7 +56,8 @@ public void init(Application app, PluginListener listener) { status = PLUGIN_INITED; this.application = app; this.pluginListener = listener; - AppActiveMatrixDelegate.INSTANCE.addListener(this); + listener.onInit(this); + ProcessUILifecycleOwner.INSTANCE.addListener(this); } @Override @@ -155,7 +156,7 @@ public void onForeground(boolean isForeground) { } public boolean isForeground() { - return AppActiveMatrixDelegate.INSTANCE.isAppForeground(); + return ProcessUILifecycleOwner.INSTANCE.isProcessForeground(); } diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MatrixHandlerThread.java b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MatrixHandlerThread.java index c935ddce4..9e035eb94 100644 --- a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MatrixHandlerThread.java +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MatrixHandlerThread.java @@ -21,7 +21,7 @@ import android.os.Looper; import android.util.Printer; -import com.tencent.matrix.AppActiveMatrixDelegate; +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner; import com.tencent.matrix.listeners.IAppForeground; import java.util.Collections; @@ -75,7 +75,7 @@ public static Handler getDefaultHandler() { return defaultHandler; } - public static HandlerThread getNewHandlerThread(String name, int priority) { + public static synchronized HandlerThread getNewHandlerThread(String name, int priority) { for (Iterator i = handlerThreads.iterator(); i.hasNext(); ) { HandlerThread element = i.next(); if (!element.isAlive()) { @@ -97,8 +97,8 @@ private static final class LooperPrinter implements Printer, IAppForeground { private boolean isForeground; LooperPrinter() { - AppActiveMatrixDelegate.INSTANCE.addListener(this); - this.isForeground = AppActiveMatrixDelegate.INSTANCE.isAppForeground(); + ProcessUILifecycleOwner.INSTANCE.addListener(this); + this.isForeground = ProcessUILifecycleOwner.INSTANCE.isProcessForeground(); } @Override diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MatrixUtil.java b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MatrixUtil.java index 145578d7c..76dd7d1e8 100644 --- a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MatrixUtil.java +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MatrixUtil.java @@ -16,9 +16,12 @@ package com.tencent.matrix.util; +import android.app.Activity; import android.app.ActivityManager; import android.content.Context; +import android.os.Build; import android.os.Looper; +import android.util.ArrayMap; import android.util.Log; import java.io.BufferedReader; @@ -28,12 +31,16 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.Field; import java.math.BigInteger; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; /** * Created by zhangshaowen on 17/6/1. @@ -42,6 +49,7 @@ public final class MatrixUtil { private static final String TAG = "Matrix.MatrixUtil"; private static String processName = null; + private static String packageName = null; private MatrixUtil() { } @@ -50,10 +58,15 @@ public static String formatTime(String format, final long timeMilliSecond) { return new java.text.SimpleDateFormat(format).format(new java.util.Date(timeMilliSecond)); } + @Deprecated public static boolean isInMainThread(final long threadId) { return Looper.getMainLooper().getThread().getId() == threadId; } + public static boolean isInMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } + public static boolean isInMainProcess(Context context) { String pkgName = context.getPackageName(); String processName = getProcessName(context); @@ -64,6 +77,19 @@ public static boolean isInMainProcess(Context context) { return pkgName.equals(processName); } + public static boolean isMainProcessName(String processName, Context context) { + String pkgName = context.getPackageName(); + return Objects.equals(pkgName, processName); + } + + public static String getPackageName(final Context context) { + if (packageName != null) { + return packageName; + } + packageName = context.getPackageName(); + return packageName; + } + /** * add process name cache * @@ -327,4 +353,41 @@ public static void printFileByLine(String printTAG, String filePath) throws IOEx } } } + + public static String getTopActivityName() { + long start = System.currentTimeMillis(); + try { + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Object activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null); + Field activitiesField = activityThreadClass.getDeclaredField("mActivities"); + activitiesField.setAccessible(true); + + Map activities; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + activities = (HashMap) activitiesField.get(activityThread); + } else { + activities = (ArrayMap) activitiesField.get(activityThread); + } + if (activities.size() < 1) { + return null; + } + for (Object activityRecord : activities.values()) { + Class activityRecordClass = activityRecord.getClass(); + Field pausedField = activityRecordClass.getDeclaredField("paused"); + pausedField.setAccessible(true); + if (!pausedField.getBoolean(activityRecord)) { + Field activityField = activityRecordClass.getDeclaredField("activity"); + activityField.setAccessible(true); + Activity activity = (Activity) activityField.get(activityRecord); + return activity.getClass().getName(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + long cost = System.currentTimeMillis() - start; + MatrixLog.d(TAG, "[getTopActivityName] Cost:%s", cost); + } + return null; + } } diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MemInfoFactory.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MemInfoFactory.kt new file mode 100644 index 000000000..5cff2a955 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/MemInfoFactory.kt @@ -0,0 +1,1045 @@ +package com.tencent.matrix.util + +import android.app.ActivityManager +import android.app.Application +import android.content.Context +import android.os.* +import android.text.TextUtils +import com.tencent.matrix.Matrix +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner +import com.tencent.matrix.lifecycle.owners.ProcessUIStartedStateOwner +import com.tencent.matrix.lifecycle.supervisor.ProcessSupervisor +import org.json.JSONObject +import java.io.File +import java.util.regex.Matcher +import java.util.regex.Pattern + +private const val TAG = "Matrix.MemoryInfoFactory" + +/** + * Created by Yves on 2021/9/22 + */ +object MemInfoFactory { + @Volatile + @JvmStatic + private var manualInitialized = false + + @Volatile + @JvmStatic + private var application: Application? = null + + @JvmStatic + fun init(app: Application) { + application = app + manualInitialized = true + } + + init { + if (!manualInitialized && !Matrix.isInstalled()) { + throw IllegalStateException("Matrix is NOT installed or MemoryInfoFactory is not initialized!!!") + } + } + + val activityManager = (if (manualInitialized) application else Matrix.with().application) + ?.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + + val memClass = activityManager.memoryClass + val largeMemClass = activityManager.largeMemoryClass +} + +data class ProcessInfo( + val pid: Int = Process.myPid(), + val name: String = MatrixUtil.getProcessName(Matrix.with().application), + val activity: String = ProcessUILifecycleOwner.recentScene.substringAfterLast('.'), + val isProcessFg: Boolean = ProcessUIStartedStateOwner.active(), + val isAppFg: Boolean = ProcessSupervisor.isAppUIForeground +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readInt(), + parcel.readString() ?: "default", + parcel.readString() ?: "default", + parcel.readByte() != 0.toByte(), + parcel.readByte() != 0.toByte() + ) + + override fun toString(): String { + return String.format( + "%-21s\t%-21s %-21s %-21s %-21s", + name, + "Activity=$activity", + "AppForeground=$isAppFg", + "ProcessForeground=$isProcessFg", + "Pid=$pid" + ) + } + + fun toJson() = safeLet(tag = TAG, defVal = JSONObject()) { + JSONObject().apply { + put("pid", pid) + put("name", name) + put("activity", activity) + put("isProcessFg", isProcessFg) + put("isAppFg", isAppFg) + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(pid) + parcel.writeString(name) + parcel.writeString(activity) + parcel.writeByte(if (isProcessFg) 1 else 0) + parcel.writeByte(if (isAppFg) 1 else 0) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ProcessInfo { + return ProcessInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + +data class PssInfo( + var totalPssK: Int = -1, + var pssJavaK: Int = -1, + var pssNativeK: Int = -1, + var pssGraphicK: Int = -1, + var pssSystemK: Int = -1, + var pssSwapK: Int = -1, + var pssCodeK: Int = -1, + var pssStackK: Int = -1, + var pssPrivateOtherK: Int = -1 +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readInt(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt() + ) + + override fun toString(): String { + return String.format( + "%-21s %-21s %-21s %-21s %-21s %-21s %-21s %-21s %-21s", + "totalPss=$totalPssK K", + "Java=$pssJavaK K", + "Native=$pssNativeK K", + "Graphic=$pssGraphicK K", + "System=$pssSystemK K", + "Swap=$pssSwapK K", + "Code=$pssCodeK K", + "Stack=$pssStackK K", + "PrivateOther=$pssPrivateOtherK K" + ) + } + + fun toJson() = safeLet(tag = TAG, defVal = JSONObject()) { + JSONObject().apply { + put("total", totalPssK) + put("java", pssJavaK) + put("native", pssNativeK) + put("graphic", pssGraphicK) + put("system", pssSystemK) + put("swap", pssSwapK) + put("code", pssCodeK) + put("stack", pssStackK) + put("other", pssPrivateOtherK) + } + } + + companion object { + + fun getFromDebug(): PssInfo { + val dbgInfo = Debug.MemoryInfo() + Debug.getMemoryInfo(dbgInfo) + return get(dbgInfo) + } + + fun getFromAms(): PssInfo { + val mi = + MemInfoFactory.activityManager + .getProcessMemoryInfo(arrayOf(Process.myPid()).toIntArray()) + .firstOrNull() + return if (mi != null) { + get(mi) + } else { + PssInfo() + } + } + + @JvmStatic + fun get(memoryInfo: Debug.MemoryInfo): PssInfo { + return PssInfo().also { + it.totalPssK = memoryInfo.totalPss + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + memoryInfo.memoryStats.apply { + + fun Map.getInt(key: String) = get(key)?.toInt() ?: -1 + + it.pssJavaK = getInt("summary.java-heap") + it.pssNativeK = getInt("summary.native-heap") + it.pssCodeK = getInt("summary.code") + it.pssStackK = getInt("summary.stack") + it.pssGraphicK = getInt("summary.graphics") + it.pssPrivateOtherK = getInt("summary.private-other") + it.pssSystemK = getInt("summary.system") + it.pssSwapK = getInt("summary.total-swap") + } + } else { + memoryInfo.apply { + it.pssJavaK = dalvikPrivateDirty + it.pssNativeK = nativePrivateDirty + it.pssSystemK = totalPss - totalPrivateClean - totalPrivateDirty + } + } + } + } + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): PssInfo { + return PssInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(totalPssK) + parcel.writeInt(pssJavaK) + parcel.writeInt(pssNativeK) + parcel.writeInt(pssGraphicK) + parcel.writeInt(pssSystemK) + parcel.writeInt(pssSwapK) + parcel.writeInt(pssCodeK) + parcel.writeInt(pssStackK) + parcel.writeInt(pssPrivateOtherK) + } + + override fun describeContents(): Int { + return 0 + } +} + +data class StatusInfo( + val state: String = "default", + val fdSize: Long = -1, + val vmSizeK: Long = -1, + val vmRssK: Long = -1, + val vmSwapK: Long = -1, + val threads: Long = -1, + val oomAdj: Int = -1, + val oomScoreAdj: Int = -1 +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readString() ?: "default", + parcel.readLong(), + parcel.readLong(), + parcel.readLong(), + parcel.readLong(), + parcel.readLong(), + parcel.readInt(), + parcel.readInt() + ) + + override fun toString(): String { + return String.format( + "%-21s %-21s %-21s %-21s %-21s %-21s %-21s %-21s", + "State=$state", + "FDSize=$fdSize", + "VmSize=$vmSizeK K", + "VmRss=$vmRssK K", + "VmSwap=$vmSwapK K", + "Threads=$threads", + "oom_adj=$oomAdj", + "oom_score_adj=$oomScoreAdj" + ) + } + + fun toJson() = safeLet(tag = TAG, defVal = JSONObject()) { + JSONObject().apply { + put("state", state) + put("vmSize", vmSizeK) + put("vmRss", vmRssK) + put("vmSwap", vmSwapK) + put("threads", threads) + put("fdSize", fdSize) + put("oom_adj", oomAdj) + put("oom_score_adj", oomScoreAdj) + } + } + + companion object { + @JvmStatic + fun get(pid: Int = Process.myPid()): StatusInfo { + return convertProcStatus(pid).safeLet(TAG, defVal = StatusInfo()) { + fun Map.getString(key: String) = get(key) ?: "unknown" + + fun Map.getInt(key: String): Long { + getString(key).let { + val matcher = Pattern.compile("\\d+").matcher(it) + while (matcher.find()) { + return matcher.group().toLong() + } + } + return -2 + } + + StatusInfo( + state = it.getString("State").trimIndent(), + fdSize = it.getInt("FDSize"), + vmSizeK = it.getInt("VmSize"), + vmRssK = it.getInt("VmRSS"), + vmSwapK = it.getInt("VmSwap"), + threads = it.getInt("Threads"), + oomAdj = getOomAdj(pid), + oomScoreAdj = getOomScoreAdj(pid) + ) + } + } + + private fun getOomAdj(pid: Int): Int = safeLet(TAG, defVal = Int.MAX_VALUE, log = false) { + File("/proc/$pid/oom_adj").useLines { + it.first().toInt() + } + } + + private fun getOomScoreAdj(pid: Int): Int = safeLet(TAG, defVal = Int.MAX_VALUE, log = false) { + File("/proc/$pid/oom_score_adj").useLines { + it.first().toInt() + } + } + + private fun convertProcStatus(pid: Int): Map { + safeApply(TAG) { + File("/proc/${pid}/status").useLines { seq -> + return seq.flatMap { + val split = it.split(":") + if (split.size == 2) { + return@flatMap sequenceOf(split[0] to split[1]) + } else { + MatrixLog.e(TAG, "ERROR : $it") + return@flatMap emptySequence() + } + }.toMap() + } + } + + return emptyMap() + } + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): StatusInfo { + return StatusInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(state) + parcel.writeLong(fdSize) + parcel.writeLong(vmSizeK) + parcel.writeLong(vmRssK) + parcel.writeLong(vmSwapK) + parcel.writeLong(threads) + parcel.writeInt(oomAdj) + parcel.writeInt(oomScoreAdj) + } + + override fun describeContents(): Int { + return 0 + } +} + +data class JavaMemInfo( + val heapSizeByte: Long = Runtime.getRuntime().totalMemory(), + val recycledByte: Long = Runtime.getRuntime().freeMemory(), + val usedByte: Long = heapSizeByte - recycledByte, + val maxByte: Long = Runtime.getRuntime().maxMemory(), + val memClass: Int = MemInfoFactory.memClass, + val largeMemClass: Int = MemInfoFactory.largeMemClass +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readLong(), + parcel.readLong(), + parcel.readLong(), + parcel.readLong(), + parcel.readInt(), + parcel.readInt() + ) + + override fun toString(): String { + return String.format( + "%-21s %-21s %-21s %-21s %-21s %-21s", + "Used=$usedByte B", + "Recycled=$recycledByte B", + "HeapSize=$heapSizeByte B", + "Max=$maxByte B", + "MemClass:$memClass M", + "LargeMemClass=$largeMemClass M" + ) + } + + fun toJson() = safeLet(tag = TAG, defVal = JSONObject()) { + JSONObject().apply { + put("used", usedByte) + put("recycled", recycledByte) + put("heapSize", heapSizeByte) + put("max", maxByte) + put("memClass", memClass) + put("largeMemClass", largeMemClass) + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeLong(heapSizeByte) + parcel.writeLong(recycledByte) + parcel.writeLong(usedByte) + parcel.writeLong(maxByte) + parcel.writeInt(memClass) + parcel.writeInt(largeMemClass) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): JavaMemInfo { + return JavaMemInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + +data class NativeMemInfo( + val heapSizeByte: Long = Debug.getNativeHeapSize(), + val recycledByte: Long = Debug.getNativeHeapFreeSize(), + val usedByte: Long = Debug.getNativeHeapAllocatedSize() +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readLong(), + parcel.readLong(), + parcel.readLong() + ) + + override fun toString(): String { + return String.format( + "%-21s %-21s %-21s", + "Used=$usedByte B", + "Recycled=$recycledByte B", + "HeapSize=$heapSizeByte B" + ) + } + + fun toJson() = safeLet(tag = TAG, defVal = JSONObject()) { + JSONObject().apply { + put("used", usedByte) + put("recycled", recycledByte) + put("heapSize", heapSizeByte) + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeLong(heapSizeByte) + parcel.writeLong(recycledByte) + parcel.writeLong(usedByte) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): NativeMemInfo { + return NativeMemInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + +data class SystemInfo( + val totalMemByte: Long = -1, + val availMemByte: Long = -1, + val lowMemory: Boolean = false, + val thresholdByte: Long = -1 +) : Parcelable { + companion object { + fun get(): SystemInfo { + val info = ActivityManager.MemoryInfo() + MemInfoFactory.activityManager.getMemoryInfo(info) + return SystemInfo( + totalMemByte = info.totalMem, + availMemByte = info.availMem, + lowMemory = info.lowMemory, + thresholdByte = info.threshold + ) + } + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SystemInfo { + return SystemInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + constructor(parcel: Parcel) : this( + parcel.readLong(), + parcel.readLong(), + parcel.readByte() != 0.toByte(), + parcel.readLong() + ) + + override fun toString(): String { + return String.format( + "%-21s %-21s %-21s %-21s", + "totalMem=$totalMemByte B", + "availMem=$availMemByte B", + "lowMemory=$lowMemory", + "threshold=$thresholdByte B" + ) + } + + fun toJson() = safeLet(tag = TAG, defVal = JSONObject()) { + JSONObject().apply { + put("totalMemByte", totalMemByte) + put("availMemByte", availMemByte) + put("lowMem", lowMemory) + put("threshold", thresholdByte) + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeLong(totalMemByte) + parcel.writeLong(availMemByte) + parcel.writeByte(if (lowMemory) 1 else 0) + parcel.writeLong(thresholdByte) + } + + override fun describeContents(): Int { + return 0 + } +} + +data class FgServiceInfo(val fgServices: List = getRunningForegroundServices()) { + override fun toString(): String { + return fgServices.toTypedArray().contentToString() + } + + companion object { + + @JvmStatic + fun getCurrentProcessFgServices() = FgServiceInfo(getRunningForegroundServices(false)) + + @JvmStatic + fun getAllProcessFgServices() = FgServiceInfo(getRunningForegroundServices(true)) + + private fun getRunningForegroundServices(allProcess: Boolean = false): List { + val fgServices = ArrayList() + val runningServiceInfoList: List = + safeLet(TAG, true, defVal = emptyList()) { + MemInfoFactory.activityManager.getRunningServices(Int.MAX_VALUE) + } + for (serviceInfo in runningServiceInfoList) { + if (serviceInfo.uid != Process.myUid()) { + continue + } + if (!allProcess && serviceInfo.pid != Process.myPid()) { + continue + } + if (serviceInfo.foreground) { + fgServices.add(serviceInfo.service.className) + } + } + return fgServices + } + } +} + +data class MemInfo( + var processInfo: ProcessInfo? = ProcessInfo(), + var statusInfo: StatusInfo? = StatusInfo.get(), + var javaMemInfo: JavaMemInfo? = JavaMemInfo(), + var nativeMemInfo: NativeMemInfo? = NativeMemInfo(), + var systemInfo: SystemInfo? = SystemInfo.get(), + var amsPssInfo: PssInfo? = null, + var debugPssInfo: PssInfo? = null, + var fgServiceInfo: FgServiceInfo? = FgServiceInfo() +) : Parcelable { + var cost = 0L + + constructor(parcel: Parcel) : this( + parcel.readParcelable(ProcessInfo::class.java.classLoader), + parcel.readParcelable(StatusInfo::class.java.classLoader), + parcel.readParcelable(JavaMemInfo::class.java.classLoader), + parcel.readParcelable(NativeMemInfo::class.java.classLoader), + parcel.readParcelable(SystemInfo::class.java.classLoader), + parcel.readParcelable(PssInfo::class.java.classLoader), + parcel.readParcelable(PssInfo::class.java.classLoader), + null + ) { + cost = parcel.readLong() + } + + override fun toString(): String { + return "\n" + """ + |>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> MemInfo <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + |> Process : $processInfo + |> Status : $statusInfo + |> SystemInfo: $systemInfo + |> Java : $javaMemInfo + |> Native : $nativeMemInfo + |> Dbg-Pss : $debugPssInfo + |> AMS-Pss : $amsPssInfo + |> FgService : $fgServiceInfo + |>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + """.trimIndent() + "\n".run { if (cost <= 0) this else "$this| cost : $cost" } + } + + fun toJson() = safeLet(tag = TAG, defVal = JSONObject()) { + JSONObject().apply { + processInfo?.let { put("processInfo", it.toJson()) } + statusInfo?.let { put("statusInfo", it.toJson()) } + javaMemInfo?.let { put("javaMemInfo", it.toJson()) } + nativeMemInfo?.let { put("nativeMemInfo", it.toJson()) } + systemInfo?.let { put("systemInfo", it.toJson()) } + amsPssInfo?.let { put("amsPssInfo", it.toJson()) } + debugPssInfo?.let { put("debugPssInfo", it.toJson()) } + } + } + + companion object { + @JvmStatic + fun getAllProcessPss(): Array { + val begin = System.currentTimeMillis() + val memInfoArray = prepareAllProcessInfo() + val pidMemInfoArray = + MemInfoFactory.activityManager.getProcessMemoryInfo(memInfoArray.toPidArray()) + + if (pidMemInfoArray != null) { + for (i in memInfoArray.indices) { + if (pidMemInfoArray[i] == null) { + memInfoArray[i].amsPssInfo = PssInfo(0, 0, 0, 0, 0, 0, 0, 0, 0) + continue + } + memInfoArray[i].amsPssInfo = PssInfo.get(pidMemInfoArray[i]) + } + } + MatrixLog.i(TAG, "getAllProcessPss cost: ${System.currentTimeMillis() - begin}") + return memInfoArray + } + + @JvmStatic + fun getCurrentProcessMemInfo(): MemInfo { + val begin = System.currentTimeMillis() + return MemInfo().also { it.cost = System.currentTimeMillis() - begin } + } + + @JvmStatic + fun getCurrentProcessMemInfoWithPss(): MemInfo { + val begin = System.currentTimeMillis() + return MemInfo(debugPssInfo = PssInfo.getFromDebug()).also { + it.cost = System.currentTimeMillis() - begin + } + } + + @JvmStatic + fun getCurrentProcessMemInfoWithAmsPss(): MemInfo { + val begin = System.currentTimeMillis() + return MemInfo(amsPssInfo = PssInfo.getFromAms()).also { + it.cost = System.currentTimeMillis() - begin + } + } + + @JvmStatic + fun getCurrentProcessFullMemInfo(): MemInfo { + val begin = System.currentTimeMillis() + return MemInfo( + amsPssInfo = PssInfo.getFromAms(), + debugPssInfo = PssInfo.getFromDebug() + ).also { it.cost = System.currentTimeMillis() - begin } + } + + private fun Array.toPidArray(): IntArray { + val pidArray = IntArray(size) + forEachIndexed { i, info -> + pidArray[i] = info.processInfo!!.pid // processInfo must be not null + } + return pidArray + } + + private fun prepareAllProcessInfo(): Array { + val processInfoList = MemInfoFactory.activityManager.runningAppProcesses + val memoryInfoList: MutableList = ArrayList() + + if (processInfoList == null) { + MatrixLog.e(TAG, "ERROR: activityManager.runningAppProcesses - no running process") + return emptyArray() + } + + MatrixLog.d(TAG, "processInfoList[$processInfoList]") + + val systemInfo = SystemInfo.get() + for (i in processInfoList.indices) { + val processInfo = processInfoList[i] + val pkgName = Matrix.with().application.packageName + if (Process.myUid() != processInfo.uid + || TextUtils.isEmpty(processInfo.processName) + || !processInfo.processName.startsWith(pkgName) + ) { + MatrixLog.e( + TAG, + "info with uid [${processInfo.uid}] & process name [${processInfo.processName}] is not current app [${Process.myUid()}][${pkgName}]", + ) + continue + } + + memoryInfoList.add( + MemInfo( + processInfo = ProcessInfo( + processInfo.pid, + processInfo.processName, + ), + statusInfo = null, + javaMemInfo = null, + nativeMemInfo = null, + systemInfo = systemInfo, + debugPssInfo = PssInfo(), + amsPssInfo = PssInfo() + ) + ) + } + return memoryInfoList.toTypedArray() + } + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): MemInfo { + return MemInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(processInfo, flags) + parcel.writeParcelable(statusInfo, flags) + parcel.writeParcelable(javaMemInfo, flags) + parcel.writeParcelable(nativeMemInfo, flags) + parcel.writeParcelable(systemInfo, flags) + parcel.writeParcelable(amsPssInfo, flags) + parcel.writeParcelable(debugPssInfo, flags) + parcel.writeLong(cost) + } + + override fun describeContents(): Int { + return 0 + } +} + +data class SmapsItem( + var name: String? = null, + var permission: String? = null, + var count: Long = 0, + var vmSize: Long = 0, + var rss: Long = 0, + var pss: Long = 0, + var sharedClean: Long = 0, + var sharedDirty: Long = 0, + var privateClean: Long = 0, + var privateDirty: Long = 0, + var swapPss: Long = 0 +) + +data class MergedSmapsInfo( + val list: List? = null +) { + fun toBriefString(): String { + val sb = StringBuilder() + sb.append("\n") + sb.append( + String.format( + FORMAT, + "PSS", + "RSS", + "SIZE", + "SWAP_PSS", + "SH_C", + "SH_D", + "PRI_C", + "PRI_D", + "COUNT", + "PERM", + "NAME" + ) + ).append("\n") + sb.append( + String.format( + FORMAT, + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----" + ) + ).append("\n") + for ((name, permission, count, vmSize, rss, pss, sharedClean, sharedDirty, privateClean, privateDirty, swapPss) in list!!) { + if (pss < 1024 /* K */) { + break + } + sb.append( + String.format( + FORMAT, + pss, + rss, + vmSize, + swapPss, + sharedClean, + sharedDirty, + privateClean, + privateDirty, + count, + permission, + name + ) + ).append("\n") + } + sb.append( + String.format( + FORMAT, + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----" + ) + ) + sb.append("\n") + + return sb.toString() + } + + override fun toString(): String { + val sb = StringBuilder() + sb.append("\n") + sb.append( + String.format( + FORMAT, + "PSS", + "RSS", + "SIZE", + "SWAP_PSS", + "SH_C", + "SH_D", + "PRI_C", + "PRI_D", + "COUNT", + "PERM", + "NAME" + ) + ).append("\n") + sb.append( + String.format( + FORMAT, + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----" + ) + ).append("\n") + for ((name, permission, count, vmSize, rss, pss, sharedClean, sharedDirty, privateClean, privateDirty, swapPss) in list!!) { + sb.append( + String.format( + FORMAT, + pss, + rss, + vmSize, + swapPss, + sharedClean, + sharedDirty, + privateClean, + privateDirty, + count, + permission, + name + ) + ).append("\n") + } + sb.append( + String.format( + FORMAT, + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----", + "----" + ) + ) + sb.append("\n") + + return sb.toString() + } + + companion object { + + // PSS RSS SIZE SWAP_PSS SH_C SH_D PRI_C PRI_D COUNT PERM NAME + const val FORMAT = "%8s %8s %8s %8s %8s %8s %8s %8s %8s %8s %s" + + @JvmStatic + fun get(pid: Int = Process.myPid()): MergedSmapsInfo { + return MergedSmapsInfo(mergeSmaps(pid)) + } + + private fun mergeSmaps(pid: Int): ArrayList { + val pattern = + Pattern.compile("^[0-9a-f]+-[0-9a-f]+\\s+([rwxps-]{4})\\s+[0-9a-f]+\\s+[0-9a-f]+:[0-9a-f]+\\s+\\d+\\s*(.*)$") + + val merged: HashMap = HashMap() + var currentInfo: SmapsItem? = null + + safeApply(TAG) { + File("/proc/${pid}/smaps").reader().forEachLine { line -> + currentInfo?.let { + var found = true + when { + line.startsWith("Size:") -> { + val sizes = line.substring("Size:".length).trim { it <= ' ' } + .split(" ").toTypedArray() + currentInfo!!.vmSize += sizes[0].toLong() + } + line.startsWith("Rss:") -> { + val sizes = + line.substring("Rss:".length).trim { it <= ' ' }.split(" ") + .toTypedArray() + currentInfo!!.rss += sizes[0].toLong() + } + line.startsWith("Pss:") -> { + val sizes = + line.substring("Pss:".length).trim { it <= ' ' }.split(" ") + .toTypedArray() + currentInfo!!.pss += sizes[0].toLong() + } + line.startsWith("Shared_Clean:") -> { + val sizes = + line.substring("Shared_Clean:".length).trim { it <= ' ' } + .split(" ").toTypedArray() + currentInfo!!.sharedClean += sizes[0].toLong() + } + line.startsWith("Shared_Dirty:") -> { + val sizes = + line.substring("Shared_Dirty:".length).trim { it <= ' ' } + .split(" ").toTypedArray() + currentInfo!!.sharedDirty += sizes[0].toLong() + } + line.startsWith("Private_Clean:") -> { + val sizes = + line.substring("Private_Clean:".length).trim { it <= ' ' } + .split(" ").toTypedArray() + currentInfo!!.privateClean += sizes[0].toLong() + } + line.startsWith("Private_Dirty:") -> { + val sizes = + line.substring("Private_Dirty:".length).trim { it <= ' ' } + .split(" ").toTypedArray() + currentInfo!!.privateDirty += sizes[0].toLong() + } + line.startsWith("SwapPss:") -> { + val sizes = line.substring("SwapPss:".length).trim { it <= ' ' } + .split(" ").toTypedArray() + currentInfo!!.swapPss += sizes[0].toLong() + } + else -> { + found = false + } + } + if (found) return@forEachLine + } + + val matcher: Matcher = pattern.matcher(line) + if (matcher.find()) { + val permission = matcher.group(1) + var name = matcher.group(2) + if (name.isNullOrBlank()) { + name = "[no-name]" + } + currentInfo = merged["$permission|$name"] + if (currentInfo == null) { + currentInfo = SmapsItem() + currentInfo!!.let { + merged["$permission|$name"] = it + it.permission = permission + it.name = name + } + } + currentInfo!!.count++ + } + } + } + + val list: ArrayList = ArrayList(merged.values) + + list.sortWith { o1, o2 -> ((o2.pss - o1.pss).toInt()) } + + return list + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/RecentTaskInfoExt.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/RecentTaskInfoExt.kt new file mode 100644 index 000000000..db4737e1e --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/RecentTaskInfoExt.kt @@ -0,0 +1,26 @@ +package com.tencent.matrix.util + +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.os.Build + +/** + * Created by Yves on 2021/12/2 + */ +@SuppressLint("NewApi") +fun ActivityManager.RecentTaskInfo.contentToString(): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + this.toString() + } else { + try { + "RecentTaskInfo{id=" + id + + " persistentId=" + persistentId + + " baseIntent=" + baseIntent + " baseActivity=" + baseActivity + + " topActivity=" + topActivity + " origActivity=" + origActivity + + " numActivities=" + numActivities + + "}" + } catch (e: Throwable) { + this.toString() + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/Safe.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/Safe.kt new file mode 100644 index 000000000..41c2ac9f6 --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/Safe.kt @@ -0,0 +1,68 @@ +package com.tencent.matrix.util + +/** + * Created by Yves on 2021/11/15 + */ + +const val DEFAULT_TAG = "Matrix.Safe" + +inline fun T.safeApply( + tag: String = DEFAULT_TAG, + log: Boolean = true, + msg: String = "", + unsafe: T.() -> Unit +): T { + try { + unsafe() + } catch (e: Throwable) { + if (log) { + MatrixLog.printErrStackTrace(tag, e, msg) + } + } + return this +} + +inline fun T.safeLet( + unsafe: (T) -> R, + success: (R) -> Unit = {}, + failed: (Throwable) -> Unit = {} +) { + try { + success(unsafe(this)) + } catch (e: Throwable) { + failed(e) + } +} + +inline fun T.safeLet( + tag: String = DEFAULT_TAG, + log: Boolean = true, + msg: String = "", + defVal: R, + unsafe: (T) -> R +): R { + return try { + unsafe(this) + } catch (e: Throwable) { + if (log) { + MatrixLog.printErrStackTrace(tag, e, msg) + } + defVal + } +} + +inline fun T.safeLetOrNull( + tag: String = DEFAULT_TAG, + log: Boolean = true, + msg: String = "", + unsafe: (T) -> R +): R? { + return try { + unsafe(this) + } catch (e: Throwable) { + if (log) { + MatrixLog.printErrStackTrace(tag, e, msg) + } + null + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/ViewDumper.kt b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/ViewDumper.kt new file mode 100644 index 000000000..c6144671d --- /dev/null +++ b/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/util/ViewDumper.kt @@ -0,0 +1,49 @@ +package com.tencent.matrix.util + +import android.view.View +import android.view.ViewGroup +import com.tencent.matrix.lifecycle.owners.OverlayWindowLifecycleOwner +import java.lang.StringBuilder + +/** + * Created by Yves on 2021/12/27 + */ +object ViewDumper { + + @JvmStatic + fun dump(): Array = OverlayWindowLifecycleOwner.getAllViews().map { + dumpInternal(it) + }.toTypedArray() + + private fun dumpInternal(view: View, level: Int = 0): String { + + if (view is ViewGroup) { + return dumpViewGroup(view, level) + } + + return dumpView(view, level) + } + + private fun dumpView(view: View, level: Int): String { + val prefix = buildString { + for (j in 0 until level) { // prefix + append("-") + } + } + return "$prefix${view.visibility}:${view.windowVisibility}:$view:[${view.x},${view.y},${view.width},${view.height}]:${view.context}\n" + } + + private fun dumpViewGroup(viewGroup: ViewGroup, level: Int): String { + val childCount = viewGroup.childCount + val builder = StringBuilder() + for (j in 0 until level) { // prefix + builder.append("-") + } + builder.append("${viewGroup.visibility}:${viewGroup.windowVisibility}:$viewGroup:[${viewGroup.x},${viewGroup.y},${viewGroup.width},${viewGroup.height}]:${viewGroup.context}\n") + for (i in 0 until childCount) { + val child = viewGroup.getChildAt(i) + builder.append(dumpInternal(child, level + 1)) + } + return builder.toString() + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-apk-canary/build.gradle b/matrix/matrix-android/matrix-apk-canary/build.gradle index a47def1bd..d0d57ffc1 100644 --- a/matrix/matrix-android/matrix-apk-canary/build.gradle +++ b/matrix/matrix-android/matrix-apk-canary/build.gradle @@ -10,7 +10,7 @@ group rootProject.ext.GROUP dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.google.code.gson:gson:2.7' + implementation 'com.google.code.gson:gson:2.8.9' implementation project(':matrix-commons') implementation 'com.android.tools:common:25.1.0' } diff --git a/matrix/matrix-android/matrix-apk-canary/libs/apktool-lib-2.4.0.jar b/matrix/matrix-android/matrix-apk-canary/libs/apktool-cli-2.6.0.jar similarity index 62% rename from matrix/matrix-android/matrix-apk-canary/libs/apktool-lib-2.4.0.jar rename to matrix/matrix-android/matrix-apk-canary/libs/apktool-cli-2.6.0.jar index 1f7ae92c4..9db848b0e 100644 Binary files a/matrix/matrix-android/matrix-apk-canary/libs/apktool-lib-2.4.0.jar and b/matrix/matrix-android/matrix-apk-canary/libs/apktool-cli-2.6.0.jar differ diff --git a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedAssetsTask.java b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedAssetsTask.java index b70ff018d..ecc8ef77c 100644 --- a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedAssetsTask.java +++ b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedAssetsTask.java @@ -35,6 +35,8 @@ import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.iface.ClassDef; +import org.jf.dexlib2.iface.MultiDexContainer; + import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -115,15 +117,18 @@ private void findAssetsFile(File dir) throws IOException { private void decodeCode() throws IOException { for (String dexFileName : dexFileNameList) { - DexBackedDexFile dexFile = DexFileFactory.loadDexFile(new File(inputFile, dexFileName), Opcodes.forApi(15)); + MultiDexContainer dexFiles = DexFileFactory.loadDexContainer(new File(inputFile, dexFileName), Opcodes.forApi(15)); - BaksmaliOptions options = new BaksmaliOptions(); - List classDefs = Ordering.natural().sortedCopy(dexFile.getClasses()); + for (String dexEntryName : dexFiles.getDexEntryNames()) { + MultiDexContainer.DexEntry dexEntry = dexFiles.getEntry(dexEntryName); + BaksmaliOptions options = new BaksmaliOptions(); + List classDefs = Ordering.natural().sortedCopy(dexEntry.getDexFile().getClasses()); - for (ClassDef classDef : classDefs) { - String[] lines = ApkUtil.disassembleClass(classDef, options); - if (lines != null) { - readSmaliLines(lines); + for (ClassDef classDef : classDefs) { + String[] lines = ApkUtil.disassembleClass(classDef, options); + if (lines != null) { + readSmaliLines(lines); + } } } diff --git a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedResourcesTask.java b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedResourcesTask.java index 46fb778fc..1e738d216 100644 --- a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedResourcesTask.java +++ b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedResourcesTask.java @@ -29,6 +29,7 @@ import com.tencent.matrix.apk.model.result.TaskResultFactory; import com.tencent.matrix.apk.model.task.util.ApkResourceDecoder; import com.tencent.matrix.apk.model.task.util.ApkUtil; +import com.tencent.matrix.apk.model.task.util.ResguardUtil; import com.tencent.matrix.javalib.util.FileUtil; import com.tencent.matrix.javalib.util.Log; import com.tencent.matrix.javalib.util.Util; @@ -37,6 +38,7 @@ import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.dexbacked.DexBackedDexFile; import org.jf.dexlib2.iface.ClassDef; +import org.jf.dexlib2.iface.MultiDexContainer; import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedReader; @@ -50,6 +52,8 @@ import java.util.Map; import java.util.Set; import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import brut.androlib.AndrolibException; @@ -68,6 +72,7 @@ public class UnusedResourcesTask extends ApkTask { private File resMappingTxt; private final List dexFileNameList; private final Map rclassProguardMap; + private final Map resguardMap; private final Map resourceDefMap; private final Map> styleableMap; private final Set resourceRefSet; @@ -82,6 +87,7 @@ public UnusedResourcesTask(JobConfig config, Map params) { dexFileNameList = new ArrayList<>(); ignoreSet = new HashSet<>(); rclassProguardMap = new HashMap<>(); + resguardMap = new HashMap<>(); resourceDefMap = new HashMap<>(); styleableMap = new HashMap<>(); resourceRefSet = new HashSet<>(); @@ -156,6 +162,8 @@ private String parseResourceId(String resId) { return ""; } + private static final Pattern sRClassPattern = Pattern.compile("(([a-zA-Z0-9_]*\\.)*)R\\$([a-z]+)"); + private String parseResourceNameFromProguard(String entry) { if (!Util.isNullOrNil(entry)) { String[] columns = entry.split("->"); @@ -169,7 +177,17 @@ private String parseResourceNameFromProguard(String entry) { if (rclassProguardMap.containsKey(resource)) { return rclassProguardMap.get(resource); } else { - return ""; + final Matcher matcher = sRClassPattern.matcher(className); + if (matcher.find()) { + final StringBuilder resultBuilder = new StringBuilder(); + resultBuilder.append("R."); + resultBuilder.append(matcher.group(3)); + resultBuilder.append("."); + resultBuilder.append(fieldName); + return resultBuilder.toString(); + } else { + return ""; + } } } else { if (ApkUtil.isRClassName(ApkUtil.getPureClassName(className))) { @@ -273,15 +291,18 @@ private void readMappingTxtFile() throws IOException { private void decodeCode() throws IOException { for (String dexFileName : dexFileNameList) { - DexBackedDexFile dexFile = DexFileFactory.loadDexFile(new File(inputFile, dexFileName), Opcodes.forApi(15)); + MultiDexContainer dexFiles = DexFileFactory.loadDexContainer(new File(inputFile, dexFileName), Opcodes.forApi(15)); - BaksmaliOptions options = new BaksmaliOptions(); - List classDefs = Ordering.natural().sortedCopy(dexFile.getClasses()); + for (String dexEntryName : dexFiles.getDexEntryNames()) { + MultiDexContainer.DexEntry dexEntry = dexFiles.getEntry(dexEntryName); + BaksmaliOptions options = new BaksmaliOptions(); + List classDefs = Ordering.natural().sortedCopy(dexEntry.getDexFile().getClasses()); - for (ClassDef classDef : classDefs) { - String[] lines = ApkUtil.disassembleClass(classDef, options); - if (lines != null) { - readSmaliLines(lines); + for (ClassDef classDef : classDefs) { + String[] lines = ApkUtil.disassembleClass(classDef, options); + if (lines != null) { + readSmaliLines(lines); + } } } @@ -359,6 +380,13 @@ private void readSmaliLines(String[] lines) { resourceRefSet.add(resourceDefMap.get(resId)); } } + if (line.trim().startsWith("0x")) { + final String resId = parseResourceId(line.trim()); + if (!Util.isNullOrNil(resId) && resourceDefMap.containsKey(resId)) { + Log.d(TAG, "array field resource, %s", resId); + resourceRefSet.add(resourceDefMap.get(resId)); + } + } } } } @@ -379,8 +407,6 @@ private void decodeResources() throws IOException, InterruptedException, Androli ApkResourceDecoder.decodeResourcesRef(manifestFile, arscFile, resDir, fileResMap, valuesReferences); - Map resguardMap = config.getResguardMap(); - for (String resource : fileResMap.keySet()) { Set result = new HashSet<>(); for (String resName : fileResMap.get(resource)) { @@ -450,6 +476,7 @@ public TaskResult call() throws TaskExecuteException { long startTime = System.currentTimeMillis(); readMappingTxtFile(); readResourceTxtFile(); + ResguardUtil.readResMappingTxtFile(resMappingTxt, null, resguardMap); unusedResSet.addAll(resourceDefMap.values()); Log.i(TAG, "find resource declarations %d items.", unusedResSet.size()); decodeCode(); diff --git a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnzipTask.java b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnzipTask.java index 409f36dcd..f7156e793 100644 --- a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnzipTask.java +++ b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnzipTask.java @@ -25,6 +25,7 @@ import com.tencent.matrix.apk.model.result.TaskResult; import com.tencent.matrix.apk.model.result.TaskResultFactory; import com.android.utils.Pair; +import com.tencent.matrix.apk.model.task.util.ResguardUtil; import com.tencent.matrix.javalib.util.FileUtil; import com.tencent.matrix.javalib.util.Log; import com.tencent.matrix.javalib.util.Util; @@ -134,59 +135,6 @@ private void readMappingTxtFile() throws IOException { } } - private String parseResourceNameFromResguard(String resName) { - if (!Util.isNullOrNil(resName)) { - int index = resName.indexOf('R'); - if (index >= 0) { - return resName.substring(index); - } - } - return ""; - } - - - private void readResMappingTxtFile() throws IOException { - if (resMappingTxt != null) { - BufferedReader bufferedReader = new BufferedReader(new FileReader(resMappingTxt)); - try { - String line = bufferedReader.readLine(); - boolean readResStart = false; - boolean readPathStart = false; - while (line != null) { - if (line.trim().equals("res path mapping:")) { - readPathStart = true; - } else if (line.trim().equals("res id mapping:")) { - readResStart = true; - readPathStart = false; - } else if (readPathStart) { - String[] columns = line.split("->"); - if (columns.length == 2) { - String before = columns[0].trim(); - String after = columns[1].trim(); - if (!Util.isNullOrNil(before) && !Util.isNullOrNil(after)) { - Log.d(TAG, "%s->%s", before, after); - resDirMap.put(after, before); - } - } - } else if (readResStart) { - String[] columns = line.split("->"); - if (columns.length == 2) { - String before = parseResourceNameFromResguard(columns[0].trim()); - String after = parseResourceNameFromResguard(columns[1].trim()); - if (!Util.isNullOrNil(before) && !Util.isNullOrNil(after)) { - Log.d(TAG, "%s->%s", before, after); - resguardMap.put(after, before); - } - } - } - line = bufferedReader.readLine(); - } - } finally { - bufferedReader.close(); - } - } - } - private String parseResourceNameFromPath(String dir, String filename) { if (Util.isNullOrNil(dir) || Util.isNullOrNil(filename)) { return ""; @@ -310,7 +258,7 @@ public TaskResult call() throws TaskExecuteException { readMappingTxtFile(); config.setProguardClassMap(proguardClassMap); - readResMappingTxtFile(); + ResguardUtil.readResMappingTxtFile(resMappingTxt, resDirMap, resguardMap); config.setResguardMap(resguardMap); Enumeration entries = zipFile.entries(); diff --git a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ApkUtil.java b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ApkUtil.java index 5bb291e18..5a8c35ab2 100644 --- a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ApkUtil.java +++ b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ApkUtil.java @@ -22,8 +22,8 @@ import org.jf.baksmali.Adaptors.ClassDefinition; import org.jf.baksmali.BaksmaliOptions; +import org.jf.baksmali.formatter.BaksmaliWriter; import org.jf.dexlib2.iface.ClassDef; -import org.jf.util.IndentingWriter; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; @@ -125,8 +125,8 @@ public static String[] disassembleClass(ClassDef classDef, BaksmaliOptions optio BufferedWriter bufWriter = new BufferedWriter(new OutputStreamWriter(baos, "UTF8")); - writer = new IndentingWriter(bufWriter); - classDefinition.writeTo((IndentingWriter) writer); + writer = new BaksmaliWriter(bufWriter, classDef.getType()); + classDefinition.writeTo((BaksmaliWriter) writer); writer.flush(); return baos.toString().split("\n"); } catch (Exception ex) { diff --git a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ManifestParser.java b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ManifestParser.java index 59a14a96d..0cb40ec38 100644 --- a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ManifestParser.java +++ b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ManifestParser.java @@ -126,7 +126,7 @@ private void handleEndElement() { } else { JsonObject preObject = jsonStack.peek(); JsonArray jsonArray = null; - if (preObject.has(name)) { + if (preObject.has(name)&&preObject.isJsonArray()) { jsonArray = preObject.getAsJsonArray(name); jsonArray.add(jsonObject); } else { diff --git a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ResguardUtil.java b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ResguardUtil.java new file mode 100644 index 000000000..a4ce95b5d --- /dev/null +++ b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/ResguardUtil.java @@ -0,0 +1,81 @@ +package com.tencent.matrix.apk.model.task.util; + +import com.tencent.matrix.javalib.util.Log; +import com.tencent.matrix.javalib.util.Util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ResguardUtil { + + private static final String TAG = "Matrix.ResguardUtil"; + + public static void readResMappingTxtFile(File resMappingTxt, Map resDirMap, Map resguardMap) throws IOException { + if (resMappingTxt != null) { + BufferedReader bufferedReader = new BufferedReader(new FileReader(resMappingTxt)); + try { + String line = bufferedReader.readLine(); + boolean readResStart = false; + boolean readPathStart = false; + while (line != null) { + if (line.trim().equals("res path mapping:")) { + readPathStart = true; + } else if (line.trim().equals("res id mapping:")) { + readResStart = true; + readPathStart = false; + } else if (readPathStart && resDirMap != null) { + String[] columns = line.split("->"); + if (columns.length == 2) { + String before = columns[0].trim(); + String after = columns[1].trim(); + if (!Util.isNullOrNil(before) && !Util.isNullOrNil(after)) { + Log.d(TAG, "%s->%s", before, after); + resDirMap.put(after, before); + } + } + } else if (readResStart && resguardMap != null) { + String[] columns = line.split("->"); + if (columns.length == 2) { + String before = parseResourceNameFromResguard(columns[0].trim()); + String after = parseResourceNameFromResguard(columns[1].trim()); + if (!Util.isNullOrNil(before) && !Util.isNullOrNil(after)) { + Log.d(TAG, "%s->%s", before, after); + resguardMap.put(after, before); + } + } + } + line = bufferedReader.readLine(); + } + } finally { + bufferedReader.close(); + } + } + } + + private static final Pattern RESOURCE_ID_PATTERN = Pattern.compile("^(\\S+\\.)*R\\.(\\S+?)\\.(\\S+)"); + + private static String parseResourceNameFromResguard(String resName) { + if (Util.isNullOrNil(resName)) return ""; + final Matcher matcher = RESOURCE_ID_PATTERN.matcher(resName); + if (matcher.find()) { + final StringBuilder builder = new StringBuilder(); + builder.append("R."); + builder.append(matcher.group(2)); + /* + The resource ID from resguard is read from ARSC file, which format is package-like + (for example: R.style.Theme.AppCompat.Light.DarkActionBar). We should convert it as the regular format + in code (for example: R.style.Theme_AppCompat_Light_DarkActionBar). + */ + builder.append('.'); + builder.append(matcher.group(3).replace('.', '_')); + return builder.toString(); + } else { + return ""; + } + } +} diff --git a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/XmlPullResourceRefDecoder.java b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/XmlPullResourceRefDecoder.java index c160884b2..9df230a68 100644 --- a/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/XmlPullResourceRefDecoder.java +++ b/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/util/XmlPullResourceRefDecoder.java @@ -93,6 +93,16 @@ private void handleElement() { int index = value.indexOf('/'); if (index > 1) { resourceRefSet.add(ApkConstants.R_ATTR_PREFIX + "." + value.substring(index + 1).replace('.', '_')); + } else { + // Attribute reference may be omitted the type, for example: + // ?attr/xxx -> ?xxx + // ?android:attr/xxx -> ?android:xxx + int colonIndex = value.indexOf(':'); + if (colonIndex > 1) { + resourceRefSet.add(ApkConstants.R_ATTR_PREFIX + value.substring(colonIndex + 1).replace('.', '_')); + } else { + resourceRefSet.add(ApkConstants.R_ATTR_PREFIX + value.substring(1).replace('.', '_')); + } } } } diff --git a/matrix/matrix-android/matrix-arscutil/build.gradle b/matrix/matrix-android/matrix-arscutil/build.gradle index c85af8788..1a850c4b5 100644 --- a/matrix/matrix-android/matrix-arscutil/build.gradle +++ b/matrix/matrix-android/matrix-arscutil/build.gradle @@ -8,7 +8,7 @@ group rootProject.ext.GROUP dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'commons-io:commons-io:2.6' - compile project(':matrix-commons') + implementation project(':matrix-commons') } if("External" == rootProject.ext.PUBLISH_CHANNEL) { diff --git a/matrix/matrix-android/matrix-arscutil/src/main/java/com/tencent/mm/arscutil/ArscUtil.java b/matrix/matrix-android/matrix-arscutil/src/main/java/com/tencent/mm/arscutil/ArscUtil.java index d666e4a1f..54c358e99 100644 --- a/matrix/matrix-android/matrix-arscutil/src/main/java/com/tencent/mm/arscutil/ArscUtil.java +++ b/matrix/matrix-android/matrix-arscutil/src/main/java/com/tencent/mm/arscutil/ArscUtil.java @@ -115,7 +115,7 @@ public static void removeResource(ResTable resTable, int resourceId, String reso resType.refresh(); } if (resNameStringPoolIndex != -1) { - Log.i(TAG, "try to remove %s (%H), find resource %s", resourceName, resourceId, ResStringBlock.resolveStringPoolEntry(resPackage.getResNamePool().getStrings().get(resNameStringPoolIndex).array(), resPackage.getResNamePool().getCharSet())); + Log.d(TAG, "try to remove %s (%H), find resource %s", resourceName, resourceId, ResStringBlock.resolveStringPoolEntry(resPackage.getResNamePool().getStrings().get(resNameStringPoolIndex).array(), resPackage.getResNamePool().getCharSet())); } resPackage.shrinkResNameStringPool(); resPackage.refresh(); @@ -126,7 +126,7 @@ public static void removeResource(ResTable resTable, int resourceId, String reso public static boolean replaceFileResource(ResTable resTable, int sourceResId, String sourceFile, int targetResId, String targetFile) throws IOException { int sourcePkgId = getPackageId(sourceResId); int targetPkgId = getPackageId(targetResId); - Log.i(TAG, "try to replace %H(%s) with %H(%s)", sourceResId, sourceFile, targetResId, targetFile); + Log.d(TAG, "try to replace %H(%s) with %H(%s)", sourceResId, sourceFile, targetResId, targetFile); if (sourcePkgId == targetPkgId) { ResPackage resPackage = findResPackage(resTable, sourcePkgId); if (resPackage != null) { @@ -252,7 +252,7 @@ public static void replaceResEntryName(ResTable resTable, Map r } public static boolean replaceResFileName(ResTable resTable, int resId, String srcFileName, String targetFileName) { - Log.i(TAG, "try to replace resource (%H) file %s with %s", resId, srcFileName, targetFileName); + Log.d(TAG, "try to replace resource (%H) file %s with %s", resId, srcFileName, targetFileName); ResPackage resPackage = findResPackage(resTable, getPackageId(resId)); boolean result = false; if (resPackage != null) { diff --git a/matrix/matrix-android/matrix-backtrace/src/main/java/com/tencent/matrix/backtrace/ProcessUtil.java b/matrix/matrix-android/matrix-backtrace/src/main/java/com/tencent/matrix/backtrace/ProcessUtil.java index db8544186..3192b6d07 100644 --- a/matrix/matrix-android/matrix-backtrace/src/main/java/com/tencent/matrix/backtrace/ProcessUtil.java +++ b/matrix/matrix-android/matrix-backtrace/src/main/java/com/tencent/matrix/backtrace/ProcessUtil.java @@ -22,6 +22,7 @@ import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.InputStream; +import java.util.List; public class ProcessUtil { @@ -41,9 +42,12 @@ private static String getProcessNameByPidImpl(final Context context, final int p } try { final ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - for (final ActivityManager.RunningAppProcessInfo i : am.getRunningAppProcesses()) { - if (i.pid == pid && i.processName != null && !i.processName.equals("")) { - return i.processName; + List processes = am.getRunningAppProcesses(); + if (processes != null) { + for (final ActivityManager.RunningAppProcessInfo i : processes) { + if (i.pid == pid && i.processName != null && !i.processName.equals("")) { + return i.processName; + } } } } catch (Exception ignore) { diff --git a/matrix/matrix-android/matrix-battery-canary/build.gradle b/matrix/matrix-android/matrix-battery-canary/build.gradle index 4b5f2a0ec..721d0a13d 100644 --- a/matrix/matrix-android/matrix-battery-canary/build.gradle +++ b/matrix/matrix-android/matrix-battery-canary/build.gradle @@ -10,7 +10,9 @@ android { versionCode 1 versionName rootProject.ext.VERSION_NAME + multiDexEnabled true testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + // testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner" consumerProguardFiles 'consumer-rules.pro' } @@ -37,15 +39,20 @@ dependencies { testImplementation 'org.jmockit:jmockit:1.28' testImplementation 'com.google.code.gson:gson:2.8.6' androidTestImplementation 'commons-io:commons-io:2.6' - androidTestImplementation 'androidx.core:core:1.3.2' + // androidTestImplementation 'androidx.core:core:1.3.2' androidTestImplementation 'androidx.annotation:annotation:1.0.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation('androidx.multidex:multidex-instrumentation:2.0.0') androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' androidTestImplementation "org.mockito:mockito-core:2.8.9" androidTestImplementation "org.mockito:mockito-android:2.8.9" implementation project(':matrix-android-lib') api project(path: ':matrix-trace-canary') + implementation 'com.tencent:mmkv:1.2.11' + + api "androidx.appcompat:appcompat:1.1.0" + api 'androidx.recyclerview:recyclerview:1.1.0' } version = rootProject.ext.VERSION_NAME diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/AndroidManifest.xml b/matrix/matrix-android/matrix-battery-canary/src/androidTest/AndroidManifest.xml index 94d1999af..0d8e7adc6 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/AndroidManifest.xml +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/AndroidManifest.xml @@ -1,4 +1,5 @@ @@ -8,12 +9,22 @@ - + + + + + + + diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/ApisTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/ApisTest.java index 0463d7018..e269a08a7 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/ApisTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/ApisTest.java @@ -35,6 +35,7 @@ import com.tencent.matrix.batterycanary.monitor.feature.AppStatMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.BlueToothMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.BlueToothMonitorFeature.BlueToothSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.CompositeMonitors; import com.tencent.matrix.batterycanary.monitor.feature.DeviceStatMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot; @@ -133,42 +134,19 @@ public void onWakeLockTimeout(WakeLockRecord record, long backgroundMillis) { } @Override - protected void onCanaryDump(AppStats appStats) { + protected void onCanaryDump(CompositeMonitors monitors) { // Dump battery stats data periodically + AppStats appStats = monitors.getAppStats(); long statMinute = appStats.getMinute(); boolean foreground = appStats.isForeground(); boolean charging = appStats.isCharging(); - super.onCanaryDump(appStats); + super.onCanaryDump(monitors); } @Override - protected void onReportJiffies(@NonNull Delta delta) { - // Report all threads jiffies consumed during the statMinute time - } - - @Override - protected void onReportAlarm(@NonNull Delta delta) { - // Report all alarm set during the statMinute time - } - - @Override - protected void onReportWakeLock(@NonNull Delta delta) { - // Report all wakelock acquired during the statMinute time - } - - @Override - protected void onReportBlueTooth(@NonNull Delta delta) { - // Report all bluetooth scanned during the statMinute time - } - - @Override - protected void onReportWifi(@NonNull Delta delta) { - // Report all wifi scanned during the statMinute time - } - - @Override - protected void onReportLocation(@NonNull Delta delta) { - // Report all gps scanned during the statMinute time + protected void onCanaryReport(CompositeMonitors monitors) { + super.onCanaryReport(monitors); + // Report all battery canary data wrapped within CompositeMonitors } }) .build(); diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/DebugApp.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/DebugApp.java new file mode 100644 index 000000000..a86ea50f6 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/DebugApp.java @@ -0,0 +1,18 @@ +package com.tencent.matrix.batterycanary; + +import android.app.Application; +import android.content.Context; + +import androidx.multidex.MultiDex; + +/** + * @author Kaede + * @since 22/8/2022 + */ +public class DebugApp extends Application { + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + MultiDex.install(base); + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/Examples.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/Examples.java index 57cfb8a79..96c50be21 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/Examples.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/Examples.java @@ -20,6 +20,7 @@ import android.content.Context; import com.tencent.matrix.Matrix; +import com.tencent.matrix.batterycanary.monitor.AppStats; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; import com.tencent.matrix.batterycanary.monitor.feature.AlarmMonitorFeature; @@ -36,6 +37,8 @@ import com.tencent.matrix.batterycanary.monitor.feature.TrafficMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.WakeLockMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.WifiMonitorFeature; +import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; +import com.tencent.matrix.batterycanary.utils.Consumer; import org.junit.After; import org.junit.Assert; @@ -43,6 +46,10 @@ import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; @@ -54,6 +61,11 @@ public class Examples { Context mContext; + /** + * 计算 Cpu Load: + * 1. CpuLoad 计算口径与 adb shell top 里的 CPU 负载计算一致 + * 2. CpuLoad = [0, 100 * cpu 核心数] + */ @Test public void exampleForCpuLoad() { if (TestUtils.isAssembleTest()) { @@ -65,20 +77,28 @@ public void exampleForCpuLoad() { if (Matrix.isInstalled()) { BatteryMonitorPlugin monitor = Matrix.with().getPluginByClass(BatteryMonitorPlugin.class); if (monitor != null) { + /* + * 注意: + * 1. CompositeMonitors 设计为非线程安全, 需要做好并发控制 + * 2. #start() 和 #finish() 必须成对调用, 以下代码监控了 doSomething() 这段时间内的功耗数据 + * 3. 监控结束后可通过 CompositeMonitors 实例访问功耗数据 + */ CompositeMonitors compositor = new CompositeMonitors(monitor.core()); compositor.metric(JiffiesMonitorFeature.JiffiesSnapshot.class); - compositor.metric(CpuStatFeature.CpuStateSnapshot.class); compositor.start(); doSomething(); compositor.finish(); int cpuLoad = compositor.getCpuLoad(); - Assert.assertTrue(cpuLoad > 0); + Assert.assertTrue("cpuLoad: " + cpuLoad, cpuLoad >= 0); } } } + /** + * CpuFreq 采样 + */ @Test public void exampleForCpuFreqSampling() { if (TestUtils.isAssembleTest()) { @@ -90,8 +110,16 @@ public void exampleForCpuFreqSampling() { if (Matrix.isInstalled()) { BatteryMonitorPlugin monitor = Matrix.with().getPluginByClass(BatteryMonitorPlugin.class); if (monitor != null) { + /* + * 注意: + * 1. CompositeMonitors 设计为非线程安全, 需要做好并发控制 + * 2. #start() 和 #finish() 必须成对调用, 以下代码监控了 doSomething() 这段时间内的功耗数据 + * 3. 监控结束后可通过 CompositeMonitors 实例访问功耗数据 + * 4. samplingIntervalMs 为采样周期, 建议取值 1minute, 最低值不应低于 5s (此处 10ms 仅为单元测试) + */ CompositeMonitors compositor = new CompositeMonitors(monitor.core()); - compositor.sample(DeviceStatMonitorFeature.CpuFreqSnapshot.class, 10L); + long samplingIntervalMs = 10L; + compositor.sample(DeviceStatMonitorFeature.CpuFreqSnapshot.class, samplingIntervalMs); compositor.start(); doSomething(); @@ -104,6 +132,70 @@ public void exampleForCpuFreqSampling() { } } + /** + * 计算 Cpu Load(叠加 CpuFreq 采样权重): + * 1. CpuLoad 计算口径与 adb shell top 里的 CPU 负载计算一致 + * 2. CpuLoad = [0, 100 * cpu 核心数] + * 3. CpuLoad 只能反馈 CPU 资源的使用率, 如果需要考虑 CPU 整体负载还要考虑 CPU 大小核的工作频率, 因此需要额外加多 CpuFreq 的数据 + * 4. CpuLoadNormalized = cpuLoad * avgCpuFreq / maxCpuFreq + */ + @Test + public void exampleForCpuLoadNormalize() { + if (TestUtils.isAssembleTest()) { + return; + } else { + mockSetup(); + } + + if (Matrix.isInstalled()) { + BatteryMonitorPlugin monitor = Matrix.with().getPluginByClass(BatteryMonitorPlugin.class); + if (monitor != null) { + /* + * 注意: + * 1. CompositeMonitors 设计为非线程安全, 需要做好并发控制 + * 2. #start() 和 #finish() 必须成对调用, 以下代码监控了 doSomething() 这段时间内的功耗数据 + * 3. 监控结束后可通过 CompositeMonitors 实例访问功耗数据 + * 4. samplingIntervalMs 为采样周期, 建议取值 1minute, 最低值不应低于 5s (此处 10ms 仅为单元测试) + */ + CompositeMonitors compositor = new CompositeMonitors(monitor.core()); + compositor.metric(JiffiesMonitorFeature.JiffiesSnapshot.class); + long samplingIntervalMs = 10L; + compositor.sample(DeviceStatMonitorFeature.CpuFreqSnapshot.class, samplingIntervalMs); + compositor.start(); + + doSomething(); + + compositor.finish(); + int cpuLoad = compositor.getCpuLoad(); + Assert.assertTrue("cpuLoad: " + cpuLoad, cpuLoad >= 0); + + MonitorFeature.Snapshot.Sampler.Result result = compositor.getSamplingResult(DeviceStatMonitorFeature.CpuFreqSnapshot.class); + Assert.assertNotNull(result); + List cpuFreqSteps = BatteryCanaryUtil.getCpuFreqSteps(); + Assert.assertEquals(BatteryCanaryUtil.getCpuCoreNum(), cpuFreqSteps.size()); + + long sumMax = 0; + for (int[] steps : cpuFreqSteps) { + int max = 0; + for (int item : steps) { + if (item > max) { + max = item; + } + } + sumMax += max; + } + Assert.assertTrue("cpuFreqSumAvg: " + result.sampleAvg + "vs cpuFreqSumMax: " + sumMax, sumMax >= result.sampleAvg); + int cpuLoadNormalized = (int) (cpuLoad * result.sampleAvg / sumMax); + Assert.assertTrue("cpuLoadNormalized: " + cpuLoadNormalized + "vs cpuLoad: " + sumMax, cpuLoad >= cpuLoadNormalized); + + Assert.assertEquals(cpuLoadNormalized, compositor.getNorCpuLoad()); + } + } + } + + /** + * 电量温度采样 + */ @Test public void exampleForTemperatureSampling() { if (TestUtils.isAssembleTest()) { @@ -115,8 +207,16 @@ public void exampleForTemperatureSampling() { if (Matrix.isInstalled()) { BatteryMonitorPlugin monitor = Matrix.with().getPluginByClass(BatteryMonitorPlugin.class); if (monitor != null) { + /* + * 注意: + * 1. CompositeMonitors 设计为非线程安全, 需要做好并发控制 + * 2. #start() 和 #finish() 必须成对调用, 以下代码监控了 doSomething() 这段时间内的功耗数据 + * 3. 监控结束后可通过 CompositeMonitors 实例访问功耗数据 + * 4. samplingIntervalMs 为采样周期, 建议取值 1minute, 最低值不应低于 5s (此处 10ms 仅为单元测试) + */ CompositeMonitors compositor = new CompositeMonitors(monitor.core()); - compositor.sample(DeviceStatMonitorFeature.BatteryTmpSnapshot.class, 10L); + long samplingIntervalMs = 10L; + compositor.sample(DeviceStatMonitorFeature.CpuFreqSnapshot.class, samplingIntervalMs); compositor.start(); doSomething(); @@ -129,9 +229,130 @@ public void exampleForTemperatureSampling() { } } + /** + * 综合监控示例 + */ + @Test + public void exampleForGeneralUseCase() { + if (TestUtils.isAssembleTest()) { + return; + } else { + mockSetup(); + } + + if (Matrix.isInstalled()) { + BatteryMonitorPlugin monitor = Matrix.with().getPluginByClass(BatteryMonitorPlugin.class); + if (monitor != null) { + // 设置监控器 + CompositeMonitors compositor = new CompositeMonitors(monitor.core()) + .metric(JiffiesMonitorFeature.JiffiesSnapshot.class) + .metric(CpuStatFeature.CpuStateSnapshot.class) + .sample(DeviceStatMonitorFeature.BatteryTmpSnapshot.class) + .sample(DeviceStatMonitorFeature.CpuFreqSnapshot.class, 10L); + + // 开始监控 + compositor.start(); + + doSomething(); + + // 结束监控 + compositor.finish(); + + // 获取监控数据: + // 1. 获取 App & Dev 状态 + compositor.getAppStats(new Consumer() { + @Override + public void accept(AppStats appStats) { + if (appStats.isValid) { + long minute = appStats.getMinute(); // 时间窗口(分钟) + + int appStat = appStats.getAppStat(); // App 状态 + int fgRatio = appStats.appFgRatio; // 前台时间占比 + int bgRatio = appStats.appBgRatio; // 后台时间占比 + int fgSrvRatio = appStats.appFgSrvRatio; // 前台服务时间占比 + int floatRatio = appStats.appFloatRatio; // 浮窗时间占比 + + int devStat = appStats.getDevStat(); // Device 状态 + int unChargingRatio = appStats.devUnChargingRatio; // 未充电状态时间占比 + int screenOff = appStats.devSceneOffRatio; // 息屏状态时间占比 + int lowEnergyRatio = appStats.devLowEnergyRatio; // 低电耗状态时间占比 + int chargingRatio = appStats.devChargingRatio; // 充电状态时间占比 + + String scene = appStats.sceneTop1; // Top1 Activity + int sceneRatio = appStats.sceneTop1Ratio; // Top1 Activity 占比 + String scene2 = appStats.sceneTop2; // Top2 Activity + int scene2Ratio = appStats.sceneTop2Ratio; // Top2 Activity 占比 + + if (appStats.isForeground()) { + // 监控期间 App 是否前台 + } + if (appStats.isCharging()) { + // 监控期间 Dev 是否充电 + } + } + } + }); + + // 2. 获取采样数据 + // 2.1 电池温度 + compositor.getSamplingResult(DeviceStatMonitorFeature.BatteryTmpSnapshot.class, new Consumer() { + @Override + public void accept(MonitorFeature.Snapshot.Sampler.Result sampling) { + long duringMillis = sampling.duringMillis; // 时间窗口 + long interval = sampling.interval ; // 采样周期 + int count = sampling.count; // 采样次数 + double sampleFst = sampling.sampleFst; // 第一次采样 + double sampleLst = sampling.sampleLst; // 最后一次采样 + double sampleMax = sampling.sampleMax; // 最大采样值 + double sampleMin = sampling.sampleMin; // 最小采样值 + double sampleAvg = sampling.sampleAvg; // 平均采样值 + } + }); + // 2.3 CpuFreq + compositor.getSamplingResult(DeviceStatMonitorFeature.BatteryTmpSnapshot.class, new Consumer() { + @Override + public void accept(MonitorFeature.Snapshot.Sampler.Result sampling) { + long duringMillis = sampling.duringMillis; // 时间窗口 + long interval = sampling.interval ; // 采样周期 + int count = sampling.count; // 采样次数 + double sampleFst = sampling.sampleFst; // 第一次采样 + double sampleLst = sampling.sampleLst; // 最后一次采样 + double sampleMax = sampling.sampleMax; // 最大采样值 + double sampleMin = sampling.sampleMin; // 最小采样值 + double sampleAvg = sampling.sampleAvg; // 平均采样值 + } + }); + + // 3. 进程 & 线程 Cpu Load + // 3.1 进程 Cpu Load, 值区间 [0, Cpu Core Num * 100] + final int procCpuLoad = compositor.getCpuLoad(); + // 3.2 获取线程数据 + compositor.getDelta(JiffiesMonitorFeature.JiffiesSnapshot.class, new Consumer>() { + @Override + public void accept(MonitorFeature.Snapshot.Delta procDelta) { + long totalJiffies = procDelta.dlt.totalJiffies.get(); + for (JiffiesMonitorFeature.JiffiesSnapshot.ThreadJiffiesEntry threadEntry : procDelta.dlt.threadEntries.getList()) { + String name = threadEntry.name; // 线程名 + int tid = threadEntry.tid; // tid + String status = threadEntry.stat; // 线程状态 + long jiffies = threadEntry.get(); // 线程在这段时间内的 Jiffies + int threadCpuLoad = (int) (procCpuLoad * ((float) jiffies / totalJiffies)); + } + } + }); + } + } + } + private void doSomething() { + Thread Thread = new Thread(new Runnable() { + @Override + public void run() { + for (;;) {} + } + }, "CpuLoadTest"); try { - Thread.sleep(1000L); + Thread.sleep(2000L); } catch (InterruptedException ignored) { } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/asm/KotlinTesting.kt b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/asm/KotlinTesting.kt index b8a98c142..677034199 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/asm/KotlinTesting.kt +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/asm/KotlinTesting.kt @@ -8,6 +8,7 @@ import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanSettings import android.content.Context +import android.hardware.Camera import android.location.Criteria import android.location.LocationListener import android.location.LocationManager @@ -79,4 +80,10 @@ class KotlinTesting { am.cancel(pendingIntent) am.cancel {} } + + fun testCamera(context: Context) { + val camera = Camera.open() + camera.startPreview() + camera.release() + } } \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/CompositorTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/CompositorTest.java index e9199b6a1..e0d173a81 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/CompositorTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/CompositorTest.java @@ -18,12 +18,17 @@ import android.app.Application; import android.content.Context; +import android.os.SystemClock; import com.tencent.matrix.Matrix; import com.tencent.matrix.batterycanary.BatteryEventDelegate; import com.tencent.matrix.batterycanary.BatteryMonitorPlugin; +import com.tencent.matrix.batterycanary.monitor.AppStats; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature.CpuStateSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import org.junit.After; @@ -92,30 +97,30 @@ public void testMetric() { monitor.start(); CompositeMonitors compositeMonitor = new CompositeMonitors(monitor); - Assert.assertNull(compositeMonitor.mBgnSnapshots.get(JiffiesMonitorFeature.JiffiesSnapshot.class)); - Assert.assertNull(compositeMonitor.mBgnSnapshots.get(CpuStatFeature.CpuStateSnapshot.class)); - compositeMonitor.metric(JiffiesMonitorFeature.JiffiesSnapshot.class); + Assert.assertNull(compositeMonitor.mBgnSnapshots.get(JiffiesSnapshot.class)); + Assert.assertNull(compositeMonitor.mBgnSnapshots.get(CpuStateSnapshot.class)); + compositeMonitor.metric(JiffiesSnapshot.class); compositeMonitor.start(); - Assert.assertNotNull(compositeMonitor.mBgnSnapshots.get(JiffiesMonitorFeature.JiffiesSnapshot.class)); - Assert.assertNull(compositeMonitor.mBgnSnapshots.get(CpuStatFeature.CpuStateSnapshot.class)); - Assert.assertNull(compositeMonitor.mDeltas.get(JiffiesMonitorFeature.JiffiesSnapshot.class)); - Assert.assertNull(compositeMonitor.mDeltas.get(CpuStatFeature.CpuStateSnapshot.class)); + Assert.assertNotNull(compositeMonitor.mBgnSnapshots.get(JiffiesSnapshot.class)); + Assert.assertNull(compositeMonitor.mBgnSnapshots.get(CpuStateSnapshot.class)); + Assert.assertNull(compositeMonitor.mDeltas.get(JiffiesSnapshot.class)); + Assert.assertNull(compositeMonitor.mDeltas.get(CpuStateSnapshot.class)); compositeMonitor.finish(); - Assert.assertNotNull(compositeMonitor.mDeltas.get(JiffiesMonitorFeature.JiffiesSnapshot.class)); - Assert.assertNull(compositeMonitor.mDeltas.get(CpuStatFeature.CpuStateSnapshot.class)); + Assert.assertNotNull(compositeMonitor.mDeltas.get(JiffiesSnapshot.class)); + Assert.assertNull(compositeMonitor.mDeltas.get(CpuStateSnapshot.class)); compositeMonitor.clear(); compositeMonitor - .metric(JiffiesMonitorFeature.JiffiesSnapshot.class) - .metric(CpuStatFeature.CpuStateSnapshot.class); + .metric(JiffiesSnapshot.class) + .metric(CpuStateSnapshot.class); compositeMonitor.start(); - Assert.assertNotNull(compositeMonitor.mBgnSnapshots.get(JiffiesMonitorFeature.JiffiesSnapshot.class)); - Assert.assertNotNull(compositeMonitor.mBgnSnapshots.get(CpuStatFeature.CpuStateSnapshot.class)); - Assert.assertNull(compositeMonitor.mDeltas.get(JiffiesMonitorFeature.JiffiesSnapshot.class)); - Assert.assertNull(compositeMonitor.mDeltas.get(CpuStatFeature.CpuStateSnapshot.class)); + Assert.assertNotNull(compositeMonitor.mBgnSnapshots.get(JiffiesSnapshot.class)); + Assert.assertNotNull(compositeMonitor.mBgnSnapshots.get(CpuStateSnapshot.class)); + Assert.assertNull(compositeMonitor.mDeltas.get(JiffiesSnapshot.class)); + Assert.assertNull(compositeMonitor.mDeltas.get(CpuStateSnapshot.class)); compositeMonitor.finish(); - Assert.assertNotNull(compositeMonitor.mDeltas.get(JiffiesMonitorFeature.JiffiesSnapshot.class)); - Assert.assertNotNull(compositeMonitor.mDeltas.get(CpuStatFeature.CpuStateSnapshot.class)); + Assert.assertNotNull(compositeMonitor.mDeltas.get(JiffiesSnapshot.class)); + Assert.assertNotNull(compositeMonitor.mDeltas.get(CpuStateSnapshot.class)); } @Test @@ -156,14 +161,14 @@ public void testPutDelta() { monitor.start(); CompositeMonitors compositeMonitor = new CompositeMonitors(monitor); - compositeMonitor.putDelta(AbsTaskMonitorFeature.TaskJiffiesSnapshot.class, Mockito.mock(MonitorFeature.Snapshot.Delta.class)); + compositeMonitor.putDelta(AbsTaskMonitorFeature.TaskJiffiesSnapshot.class, Mockito.mock(Delta.class)); Assert.assertNotNull(compositeMonitor.getDelta(AbsTaskMonitorFeature.TaskJiffiesSnapshot.class)); - compositeMonitor.putDelta(InternalMonitorFeature.InternalSnapshot.class, Mockito.mock(MonitorFeature.Snapshot.Delta.class)); + compositeMonitor.putDelta(InternalMonitorFeature.InternalSnapshot.class, Mockito.mock(Delta.class)); Assert.assertNotNull(compositeMonitor.getDeltaRaw(InternalMonitorFeature.InternalSnapshot.class)); } @Test - public void testGetCpuLoad() { + public void testGetCpuLoad() throws InterruptedException { final BatteryMonitorCore monitor = mockMonitor(); BatteryMonitorPlugin plugin = new BatteryMonitorPlugin(monitor.getConfig()); Matrix.with().getPlugins().add(plugin); @@ -172,15 +177,59 @@ public void testGetCpuLoad() { CompositeMonitors compositeMonitor = new CompositeMonitors(monitor); Assert.assertEquals(-1, compositeMonitor.getCpuLoad()); - compositeMonitor.metric(JiffiesMonitorFeature.JiffiesSnapshot.class); + + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + } + } + }, "TEST"); + thread.start(); + + compositeMonitor.metric(JiffiesSnapshot.class); compositeMonitor.start(); + Thread.sleep(1000L); compositeMonitor.finish(); - Assert.assertEquals(-1, compositeMonitor.getCpuLoad()); + int cpuLoadR = compositeMonitor.getCpuLoad(); + Assert.assertTrue(cpuLoadR >= 0 && cpuLoadR <= BatteryCanaryUtil.getCpuCoreNum() * 100); - compositeMonitor.metric(CpuStatFeature.CpuStateSnapshot.class); + compositeMonitor.metric(CpuStateSnapshot.class); compositeMonitor.start(); + Thread.sleep(1000L); compositeMonitor.finish(); - Assert.assertTrue(compositeMonitor.getCpuLoad() >= 0 && compositeMonitor.getCpuLoad() <= BatteryCanaryUtil.getCpuCoreNum() * 100); + int devCpuLoad = compositeMonitor.getDevCpuLoad(); + Assert.assertTrue("devCpuLoad: " + devCpuLoad, devCpuLoad >= 0 && devCpuLoad <= BatteryCanaryUtil.getCpuCoreNum() * 100); + + Assert.assertEquals(devCpuLoad, cpuLoadR, 10); + + compositeMonitor = new CompositeMonitors(monitor); + Assert.assertFalse(compositeMonitor.mMetrics.contains(JiffiesSnapshot.class)); + Assert.assertFalse(compositeMonitor.mMetrics.contains(CpuStateSnapshot.class)); + compositeMonitor.metricCpuLoad(); + compositeMonitor.start(); + long wallTimeBgn = System.currentTimeMillis(); + long upTimeBgn = SystemClock.uptimeMillis(); + Thread.sleep(10 * 1000L); + compositeMonitor.finish(); + long wallTimeEnd = System.currentTimeMillis(); + long upTimeEnd = SystemClock.uptimeMillis(); + devCpuLoad = compositeMonitor.getDevCpuLoad(); + Assert.assertTrue("devCpuLoad: " + devCpuLoad,devCpuLoad >= 0 && devCpuLoad <= BatteryCanaryUtil.getCpuCoreNum() * 100); + + long wallTimeDelta = wallTimeEnd - wallTimeBgn; + long uptimeDelta = upTimeEnd - upTimeBgn; + Assert.assertEquals(wallTimeDelta, uptimeDelta, 100L); + + Delta appJiffies = compositeMonitor.getDelta(JiffiesSnapshot.class); + Assert.assertNotNull(appJiffies); + Delta cpuJiffies = compositeMonitor.getDelta(CpuStateSnapshot.class); + Assert.assertNotNull(cpuJiffies); + Assert.assertEquals((uptimeDelta / 10) * BatteryCanaryUtil.getCpuCoreNum(), cpuJiffies.dlt.totalCpuJiffies(), 20L); + + cpuLoadR = (int) ((appJiffies.dlt.totalJiffies.get() / (uptimeDelta / 10f)) * 100); + Assert.assertTrue("cpuLoadR: " + cpuLoadR, cpuLoadR >= 0 && cpuLoadR <= BatteryCanaryUtil.getCpuCoreNum() * 100); + Assert.assertEquals(devCpuLoad, cpuLoadR, 10); } @Test @@ -211,4 +260,33 @@ public void testSampling() throws InterruptedException { Assert.assertTrue(compositeMonitor.getSamplingResult(DeviceStatMonitorFeature.BatteryTmpSnapshot.class).count > compositeMonitor.getSamplingResult(DeviceStatMonitorFeature.CpuFreqSnapshot.class).count); } + + @Test + public void testSamplingStop() throws InterruptedException { + final BatteryMonitorCore monitor = mockMonitor(); + BatteryMonitorPlugin plugin = new BatteryMonitorPlugin(monitor.getConfig()); + Matrix.with().getPlugins().add(plugin); + monitor.enableForegroundLoopCheck(true); + monitor.start(); + + long interval = 100L; + long samplingTime = 1000L; + CompositeMonitors compositeMonitor = new CompositeMonitors(monitor); + compositeMonitor.sample(DeviceStatMonitorFeature.BatteryTmpSnapshot.class, interval); + compositeMonitor.sample(DeviceStatMonitorFeature.CpuFreqSnapshot.class, interval); + compositeMonitor.sample(DeviceStatMonitorFeature.ChargeWattageSnapshot.class, interval); + compositeMonitor.sample(CpuStatFeature.CpuStateSnapshot.class, interval); + compositeMonitor.sample(JiffiesMonitorFeature.UidJiffiesSnapshot.class, interval); + + compositeMonitor.start(); + Thread.sleep(samplingTime); + compositeMonitor.finish(); + + Thread.sleep(4000L); + Assert.assertEquals(samplingTime/interval, compositeMonitor.getSamplingResult(DeviceStatMonitorFeature.BatteryTmpSnapshot.class).count, 2); + Assert.assertEquals(samplingTime/interval, compositeMonitor.getSamplingResult(DeviceStatMonitorFeature.CpuFreqSnapshot.class).count, 2); + Assert.assertEquals(samplingTime/interval, compositeMonitor.getSamplingResult(DeviceStatMonitorFeature.ChargeWattageSnapshot.class).count, 2); + Assert.assertEquals(samplingTime/interval, compositeMonitor.getSamplingResult(CpuStatFeature.CpuStateSnapshot.class).count, 3); + Assert.assertEquals(samplingTime/interval, compositeMonitor.getSamplingResult(JiffiesMonitorFeature.UidJiffiesSnapshot.class).count, 2); + } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeatureLooperTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeatureLooperTest.java index a934a231a..1e4a8536b 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeatureLooperTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeatureLooperTest.java @@ -35,6 +35,7 @@ import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; import com.tencent.matrix.batterycanary.utils.TimeBreaker; import com.tencent.matrix.trace.core.LooperMonitor; +import com.tencent.matrix.trace.listeners.ILooperListener; import org.junit.After; import org.junit.Assert; @@ -47,7 +48,6 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -77,24 +77,23 @@ public void testLooperMonitor() throws InterruptedException { HandlerThread handlerThread = new HandlerThread("looper-test"); handlerThread.start(); LooperMonitor looperMonitor = LooperMonitor.of(handlerThread.getLooper()); - looperMonitor.addListener(new LooperMonitor.LooperDispatchListener() { + looperMonitor.addListener(new ILooperListener() { @Override public boolean isValid() { return true; } @Override - public void dispatchStart() { + public void onDispatchBegin(String log) { Assert.assertEquals("looper-test", Thread.currentThread().getName()); if (!hasStart.get()) { Assert.assertFalse(hasFinish.get()); } hasStart.set(true); - } @Override - public void dispatchEnd() { + public void onDispatchEnd(String log, long beginNs, long endNs) { Assert.assertEquals("looper-test", Thread.currentThread().getName()); hasFinish.set(true); Assert.assertTrue(hasStart.get()); @@ -136,7 +135,7 @@ public void testLooperMonitorV2() throws InterruptedException { HandlerThread handlerThread = new HandlerThread("looper-test"); handlerThread.start(); LooperMonitor looperMonitor = LooperMonitor.of(handlerThread.getLooper()); - looperMonitor.addListener(new LooperMonitor.LooperDispatchListener() { + looperMonitor.addListener(new ILooperListener() { @Override public boolean isValid() { return true; @@ -146,8 +145,7 @@ public boolean isValid() { * >>>>> Dispatching to Handler (android.os.Handler) {b54e421} com.tencent.matrix.batterycanary.utils.LooperMonitorTest$TestTask@32d6046: 0 */ @Override - public void onDispatchStart(String x) { - super.onDispatchStart(x); + public void onDispatchBegin(String x) { if (!hasStart.get()) { Assert.assertFalse(hasFinish.get()); } @@ -188,8 +186,7 @@ public void onDispatchStart(String x) { * <<<<< Finished to Handler (android.os.Handler) {b54e421} com.tencent.matrix.batterycanary.utils.LooperMonitorTest$TestTask@32d6046 */ @Override - public void onDispatchEnd(String x) { - super.onDispatchEnd(x); + public void onDispatchEnd(String x, long beginNs, long endNs) { hasFinish.set(true); Assert.assertTrue(hasStart.get()); @@ -341,7 +338,7 @@ public void run() { Assert.assertEquals(1, feature.mDeltaList.size()); Assert.assertTrue(feature.mDeltaList.get(0).dlt.name.contains(MonitorFeatureLooperTest.class.getName() + "$")); - List> deltas = feature.currentJiffies(); + List> deltas = feature.currentJiffies(0); Assert.assertEquals(1, deltas.size()); Assert.assertEquals(feature.mDeltaList, deltas); @@ -416,7 +413,7 @@ public void run() { Assert.assertEquals(1, feature.mDeltaList.size()); Assert.assertTrue(feature.mDeltaList.get(0).dlt.name.contains(MonitorFeatureLooperTest.class.getName() + "$")); - List> deltas = feature.currentJiffies(); + List> deltas = feature.currentJiffies(0); Assert.assertEquals(1, deltas.size()); Assert.assertEquals(feature.mDeltaList, deltas); @@ -545,7 +542,7 @@ public void run() { Assert.assertEquals(1, feature.mDeltaList.size()); Assert.assertTrue(feature.mDeltaList.get(0).dlt.name.contains(MonitorFeatureLooperTest.class.getName() + "$")); - List> deltas = feature.currentJiffies(); + List> deltas = feature.currentJiffies(0); Assert.assertEquals(1, deltas.size()); Assert.assertEquals(feature.mDeltaList, deltas); diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeatureOverAllTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeatureOverAllTest.java index 4fc36c443..80da2307f 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeatureOverAllTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeatureOverAllTest.java @@ -81,10 +81,16 @@ public BatteryPrinter attach(BatteryMonitorCore monitorCore) { BatteryPrinter core = super.attach(monitorCore); mCompositeMonitors.sample(DeviceStatMonitorFeature.BatteryTmpSnapshot.class, 100L); mCompositeMonitors.sample(DeviceStatMonitorFeature.CpuFreqSnapshot.class, 100L); + mCompositeMonitors.sample(DeviceStatMonitorFeature.ChargeWattageSnapshot.class, 100L); + mCompositeMonitors.sample(CpuStatFeature.CpuStateSnapshot.class, 100L); + mCompositeMonitors.sample(JiffiesMonitorFeature.UidJiffiesSnapshot.class, 100L); + mCompositeMonitors.sample(TrafficMonitorFeature.RadioStatSnapshot.class, 100L); return core; } }) .build(); + + return new BatteryMonitorCore(config); } diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/SamplerTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/SamplerTest.java index dd6ffd77d..3a2b9f199 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/SamplerTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/monitor/feature/SamplerTest.java @@ -84,6 +84,13 @@ private BatteryMonitorCore mockMonitor() { return new BatteryMonitorCore(config); } + @Test + public void testInvalidSamplingResult() { + Number result = Integer.MIN_VALUE; + Assert.assertTrue(result.equals(Integer.MIN_VALUE)); + Assert.assertEquals(Integer.MIN_VALUE, result); + } + @Test public void testSampling() throws InterruptedException { final BatteryMonitorCore monitor = mockMonitor(); @@ -95,7 +102,7 @@ public void testSampling() throws InterruptedException { final int samplingCount = 10; final AtomicInteger counter = new AtomicInteger(0); - final MonitorFeature.Snapshot.Sampler sampler = new MonitorFeature.Snapshot.Sampler(monitor.getHandler(), new Callable() { + final MonitorFeature.Snapshot.Sampler sampler = new MonitorFeature.Snapshot.Sampler("test-1", monitor.getHandler(), new Callable() { @Override public Integer call() throws InterruptedException { int count = counter.incrementAndGet(); @@ -128,7 +135,7 @@ public Integer call() throws InterruptedException { Assert.assertEquals(55d / samplingCount, result.sampleAvg, 0.1); final AtomicInteger counterDec = new AtomicInteger(0); - final MonitorFeature.Snapshot.Sampler samplerDec = new MonitorFeature.Snapshot.Sampler(monitor.getHandler(), new Callable() { + final MonitorFeature.Snapshot.Sampler samplerDec = new MonitorFeature.Snapshot.Sampler("test-2", monitor.getHandler(), new Callable() { @Override public Integer call() throws InterruptedException { int count = counterDec.incrementAndGet(); diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/shell/TopThreadTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/shell/TopThreadTest.java new file mode 100644 index 000000000..2f06f77d4 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/shell/TopThreadTest.java @@ -0,0 +1,126 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.batterycanary.shell; + +import android.app.Application; +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; + +import com.tencent.matrix.Matrix; +import com.tencent.matrix.batterycanary.BatteryEventDelegate; +import com.tencent.matrix.batterycanary.BatteryMonitorPlugin; +import com.tencent.matrix.batterycanary.TestUtils; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + + +@RunWith(AndroidJUnit4.class) +public class TopThreadTest { + static final String TAG = "Matrix.test.TopThreadTest"; + + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + if (!Matrix.isInstalled()) { + Matrix.init(new Matrix.Builder(((Application) mContext.getApplicationContext())).build()); + } + if (!BatteryEventDelegate.isInit()) { + BatteryEventDelegate.init((Application) mContext.getApplicationContext()); + } + } + + @After + public void shutDown() { + } + + private BatteryMonitorCore mockMonitor() { + BatteryMonitorConfig config = new BatteryMonitorConfig.Builder() + .enable(JiffiesMonitorFeature.class) + .enable(TopThreadFeature.class) + .enableBuiltinForegroundNotify(false) + .build(); + return new BatteryMonitorCore(config); + } + + @Test + public void testTopShellSchedule() throws InterruptedException { + mContext.startService(new Intent(mContext, TestSubProcService.class)); + + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + new Handler(Looper.getMainLooper()); + } + } + }); + + thread.setPriority(Thread.MAX_PRIORITY); + thread.setName("test-jiffies-thread"); + thread.start(); + + final BatteryMonitorCore monitor = mockMonitor(); + BatteryMonitorPlugin plugin = new BatteryMonitorPlugin(monitor.getConfig()); + Matrix.with().getPlugins().add(plugin); + monitor.enableForegroundLoopCheck(true); + monitor.start(); + + final TopThreadFeature topFeat = monitor.getMonitorFeature(TopThreadFeature.class); + topFeat.topShell(1); + + if (TestUtils.isAssembleTest()) { + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + topFeat.stopShell(); + } + }, 5000L); + Thread.sleep(10000L); + return; + } + + Thread.sleep(5000000L); + } + + + public static class TestSubProcService extends IntentService { + + public TestSubProcService() { + super("TestSubProcService"); + } + + @Override + protected void onHandleIntent(@Nullable @org.jetbrains.annotations.Nullable Intent intent) { + while (true) {} + } + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryRecordTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryRecordTest.java new file mode 100644 index 000000000..9795ac44c --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryRecordTest.java @@ -0,0 +1,474 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.batterycanary.stats; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.ArrayMap; + +import com.tencent.mmkv.MMKV; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class BatteryRecordTest { + static final String TAG = "Matrix.test.BatteryRecordTest"; + + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + String rootDir = MMKV.initialize(mContext); + System.out.println("mmkv root: " + rootDir); + } + + @After + public void shutDown() { + } + + @Test + public void testEventRecordIOManipulation() { + BatteryRecord.EventStatRecord record = new BatteryRecord.EventStatRecord(); + record.id = 22; + record.event = "EVENT"; + + MMKV mmkv = MMKV.defaultMMKV(); + String key = "stats_event_" + record.id; + + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + record.writeToParcel(parcel, 0); + mmkv.encode(key, parcel.marshall()); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + + byte[] bytes = mmkv.decodeBytes(key); + Assert.assertNotNull(bytes); + + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + BatteryRecord.EventStatRecord recordLoaded = BatteryRecord.EventStatRecord.CREATOR.createFromParcel(parcel); + Assert.assertNotNull(recordLoaded); + + } finally { + parcel.recycle(); + mmkv.remove(key); + } + } + + @Test + public void testReportRecordIOManipulation() { + BatteryRecord.ReportRecord record = new BatteryRecord.ReportRecord(); + record.id = record.hashCode(); + record.threadInfoList = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + BatteryRecord.ReportRecord.ThreadInfo threadInfo = new BatteryRecord.ReportRecord.ThreadInfo(); + threadInfo.stat = "R" + i; + threadInfo.tid = 10000 + i; + threadInfo.name = "ThreadName_" + i; + threadInfo.jiffies = SystemClock.currentThreadTimeMillis() / 10; + record.threadInfoList.add(0, threadInfo); + } + record.entryList = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + BatteryRecord.ReportRecord.EntryInfo entryInfo = new BatteryRecord.ReportRecord.EntryInfo(); + entryInfo.name = "Entry Name " + i; + entryInfo.entries = new ArrayMap<>(); + entryInfo.entries.put("Key 1", "Value 1"); + entryInfo.entries.put("Key 2", "Value 2"); + entryInfo.entries.put("Key 3", "Value 3"); + record.entryList.add(entryInfo); + } + + MMKV mmkv = MMKV.defaultMMKV(); + String key = "stats_report_" + record.id; + + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + record.writeToParcel(parcel, 0); + mmkv.encode(key, parcel.marshall()); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + + byte[] bytes = mmkv.decodeBytes(key); + Assert.assertNotNull(bytes); + + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + BatteryRecord.ReportRecord recordLoaded = BatteryRecord.ReportRecord.CREATOR.createFromParcel(parcel); + Assert.assertNotNull(recordLoaded); + + } finally { + parcel.recycle(); + mmkv.remove(key); + } + } + + @Test + public void testUniversalRecordIOManipulation() { + MMKV mmkv = MMKV.defaultMMKV(); + + BatteryRecord.EventStatRecord eventRecord = new BatteryRecord.EventStatRecord(); + eventRecord.id = 22; + eventRecord.event = "EVENT"; + + String key1 = "stats_" + eventRecord.id; + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.writeInt(1); + eventRecord.writeToParcel(parcel, 0); + mmkv.encode(key1, parcel.marshall()); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + + BatteryRecord.ReportRecord reportRecord = new BatteryRecord.ReportRecord(); + reportRecord.id = 33; + reportRecord.threadInfoList = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + BatteryRecord.ReportRecord.ThreadInfo threadInfo = new BatteryRecord.ReportRecord.ThreadInfo(); + threadInfo.stat = "R" + i; + threadInfo.tid = 10000 + i; + threadInfo.name = "ThreadName_" + i; + threadInfo.jiffies = SystemClock.currentThreadTimeMillis() / 10; + reportRecord.threadInfoList.add(0, threadInfo); + } + reportRecord.entryList = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + BatteryRecord.ReportRecord.EntryInfo entryInfo = new BatteryRecord.ReportRecord.EntryInfo(); + entryInfo.name = "Entry Name " + i; + entryInfo.entries = new ArrayMap<>(); + entryInfo.entries.put("Key 1", "Value 1"); + entryInfo.entries.put("Key 2", "Value 2"); + entryInfo.entries.put("Key 3", "Value 3"); + reportRecord.entryList.add(entryInfo); + } + + String key2 = "stats_" + reportRecord.id; + try { + parcel = Parcel.obtain(); + parcel.writeInt(2); + reportRecord.writeToParcel(parcel, 0); + mmkv.encode(key2, parcel.marshall()); + } finally { + parcel.recycle(); + } + + byte[] bytes = mmkv.decodeBytes(key1); + Assert.assertNotNull(bytes); + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + int type = parcel.readInt(); + Assert.assertEquals(1, type); + BatteryRecord.EventStatRecord recordLoaded = BatteryRecord.EventStatRecord.CREATOR.createFromParcel(parcel); + Assert.assertNotNull(recordLoaded); + + } finally { + parcel.recycle(); + mmkv.remove(key1); + } + + bytes = mmkv.decodeBytes(key2); + Assert.assertNotNull(bytes); + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + int type = parcel.readInt(); + Assert.assertEquals(2, type); + BatteryRecord.ReportRecord recordLoaded = BatteryRecord.ReportRecord.CREATOR.createFromParcel(parcel); + Assert.assertNotNull(recordLoaded); + + } finally { + parcel.recycle(); + mmkv.remove(key1); + } + } + + @Test + public void testEventRecordIOManipulationBenchmark() { + MMKV mmkv = MMKV.defaultMMKV(); + int count = 1000; + + for (int i = 0; i < count; i++) { + String key = "stats_event_" + i; + BatteryRecord.EventStatRecord record = new BatteryRecord.EventStatRecord(); + record.id = i; + record.event = "EVENT_" + i; + + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + record.writeToParcel(parcel, 0); + mmkv.encode(key, parcel.marshall()); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } + + for (int i = 0; i < count; i++) { + String key = "stats_event_" + i; + byte[] bytes = mmkv.decodeBytes(key); + Assert.assertNotNull(bytes); + + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + BatteryRecord.EventStatRecord recordLoaded = BatteryRecord.EventStatRecord.CREATOR.createFromParcel(parcel); + Assert.assertNotNull(recordLoaded); + + } finally { + if (parcel != null) { + parcel.recycle(); + } + mmkv.remove(key); + } + } + } + + @Test + public void testRecordWithType() { + BatteryRecord.DevStatRecord devStatRecord = new BatteryRecord.DevStatRecord(); + devStatRecord.devStat = 5; + byte[] bytes = BatteryRecord.encode(devStatRecord); + Assert.assertNotNull(devStatRecord); + BatteryRecord record = BatteryRecord.decode(bytes); + Assert.assertTrue(record instanceof BatteryRecord.DevStatRecord); + Assert.assertEquals(devStatRecord.version, devStatRecord.version); + Assert.assertEquals(devStatRecord.millis, devStatRecord.millis); + Assert.assertEquals(devStatRecord.devStat, devStatRecord.devStat); + + MMKV mmkv = MMKV.defaultMMKV(); + String key = "test_key"; + mmkv.encode(key, bytes); + bytes = mmkv.decodeBytes(key); + record = BatteryRecord.decode(bytes); + Assert.assertEquals(devStatRecord.version, devStatRecord.version); + Assert.assertEquals(devStatRecord.millis, devStatRecord.millis); + Assert.assertEquals(devStatRecord.devStat, devStatRecord.devStat); + } + + @Test + public void testVersionControl() { + Version0 record = new Version0(); + record.id = 22; + record.event = "Version0"; + + byte[] bytes = null; + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + record.writeToParcel(parcel, 0); + bytes = parcel.marshall(); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + + Assert.assertNotNull(bytes); + + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + Version1 recordLoaded = Version1.CREATOR.createFromParcel(parcel); + Assert.assertNotNull(recordLoaded); + Assert.assertEquals(record.id, recordLoaded.id); + Assert.assertEquals(record.event, recordLoaded.event); + Assert.assertSame(recordLoaded.extras, Collections.emptyMap()); + } finally { + parcel.recycle(); + } + + Version1 recordNew = new Version1(); + recordNew.id = 22; + recordNew.event = "Version1"; + recordNew.extras = new HashMap<>(); + recordNew.extras.put("xxx", "yyy"); + recordNew.extras.put("zzz", 1000); + + bytes = null; + try { + parcel = Parcel.obtain(); + recordNew.writeToParcel(parcel, 0); + bytes = parcel.marshall(); + } finally { + parcel.recycle(); + } + + Assert.assertNotNull(bytes); + + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + Version1 recordLoaded = Version1.CREATOR.createFromParcel(parcel); + Assert.assertNotNull(recordLoaded); + Assert.assertEquals(recordNew.id, recordLoaded.id); + Assert.assertEquals(recordNew.event, recordLoaded.event); + Assert.assertEquals(recordNew.extras, recordLoaded.extras); + Assert.assertNotSame(recordLoaded.extras, Collections.emptyMap()); + } finally { + parcel.recycle(); + } + + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + Version0 recordLoaded = Version0.CREATOR.createFromParcel(parcel); + Assert.assertNotNull(recordLoaded); + Assert.assertEquals(recordNew.id, recordLoaded.id); + Assert.assertEquals(recordNew.event, recordLoaded.event); + } finally { + parcel.recycle(); + } + } + + + public static class Version0 extends BatteryRecord implements Parcelable { + public static final int VERSION = 0; + + public long id; + public String event; + + public Version0() { + id = 0; + version = VERSION; + } + + protected Version0(Parcel in) { + super(in); + id = in.readLong(); + event = in.readString(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public Version0 createFromParcel(Parcel in) { + return new Version0(in); + } + + @Override + public Version0[] newArray(int size) { + return new Version0[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(id); + dest.writeString(event); + } + } + + public static class Version1 extends BatteryRecord implements Parcelable { + public static final int VERSION = 1; + + public long id; + public String event; + public Map extras = Collections.emptyMap(); // Since version 1 + + public Version1() { + id = 0; + version = VERSION; + } + + protected Version1(Parcel in) { + super(in); + id = in.readLong(); + event = in.readString(); + if (version >= 1) { + extras = new HashMap<>(); + in.readMap(extras, getClass().getClassLoader()); + } + } + + public static final Creator CREATOR = new Creator() { + @Override + public Version1 createFromParcel(Parcel in) { + return new Version1(in); + } + + @Override + public Version1[] newArray(int size) { + return new Version1[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(id); + dest.writeString(event); + dest.writeMap(extras); + } + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryRecorderTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryRecorderTest.java new file mode 100644 index 000000000..24263e9dd --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryRecorderTest.java @@ -0,0 +1,345 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.batterycanary.stats; + +import android.app.Application; +import android.content.Context; +import android.os.Process; +import android.os.SystemClock; +import android.util.ArrayMap; + +import com.tencent.matrix.Matrix; +import com.tencent.matrix.batterycanary.BatteryMonitorPlugin; +import com.tencent.matrix.batterycanary.monitor.AppStats; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; +import com.tencent.mmkv.MMKV; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class BatteryRecorderTest { + static final String TAG = "Matrix.test.BatteryRecorderTest"; + + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + if (!Matrix.isInstalled()) { + Matrix.init(new Matrix.Builder(((Application) mContext.getApplicationContext())).build()); + } + String rootDir = MMKV.initialize(mContext); + System.out.println("mmkv root: " + rootDir); + } + + @After + public void shutDown() { + } + + @Test + @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") + public void testRecorderWithProcSet() { + MMKV mmkv = MMKV.defaultMMKV(); + mmkv.clearAll(); + BatteryRecorder.MMKVRecorder recorder = new BatteryRecorder.MMKVRecorder(mmkv); + Assert.assertTrue(recorder.getProcSet().isEmpty()); + + recorder.updateProc("main"); + Assert.assertArrayEquals(Arrays.asList("main").toArray(), recorder.getProcSet().toArray()); + + recorder.updateProc("sub1"); + recorder.updateProc("sub2"); + Assert.assertEquals(3, recorder.getProcSet().size()); + Assert.assertTrue(recorder.getProcSet().containsAll(Arrays.asList("main", "sub1", "sub2"))); + } + + @Test + public void testRecorderWithEventRecord() { + BatteryMonitorConfig config = new BatteryMonitorConfig.Builder().enable(JiffiesMonitorFeature.class).build(); + BatteryMonitorCore core = new BatteryMonitorCore(config); + core.start(); + BatteryMonitorPlugin plugin = new BatteryMonitorPlugin(config); + Matrix.with().getPlugins().add(plugin); + + BatteryRecord.EventStatRecord record = new BatteryRecord.EventStatRecord(); + record.id = 22; + record.event = "EVENT"; + + String date = BatteryStatsFeature.getDateString(0); + MMKV mmkv = MMKV.defaultMMKV(); + mmkv.clearAll(); + BatteryRecorder.MMKVRecorder recorder = new BatteryRecorder.MMKVRecorder(mmkv); + recorder.clean(date, "main"); + Assert.assertTrue(recorder.read(date, "main").isEmpty()); + + recorder.write(date, record); + Assert.assertTrue(recorder.read(BatteryStatsFeature.getDateString(-1), "main").isEmpty()); + + List records = recorder.read(date, "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(record.id, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals(record.event, ((BatteryRecord.EventStatRecord) records.get(0)).event); + + recorder.clean(BatteryStatsFeature.getDateString(-1), "main"); + Assert.assertFalse(recorder.read(date, "main").isEmpty()); + recorder.clean(date, "main"); + Assert.assertTrue(recorder.read(date, "main").isEmpty()); + } + + @Test + public void testRecorderWithEventRecordWithMultiProc() { + BatteryMonitorConfig config = new BatteryMonitorConfig.Builder().enable(JiffiesMonitorFeature.class).build(); + BatteryMonitorCore core = new BatteryMonitorCore(config); + core.start(); + BatteryMonitorPlugin plugin = new BatteryMonitorPlugin(config); + Matrix.with().getPlugins().add(plugin); + + + + String date = BatteryStatsFeature.getDateString(0); + MMKV mmkv = MMKV.defaultMMKV(); + mmkv.clearAll(); + + BatteryRecorder.MMKVRecorder recorder = new BatteryRecorder.MMKVRecorder(mmkv); + Assert.assertTrue(recorder.read(date, "main").isEmpty()); + + BatteryRecord.EventStatRecord record = new BatteryRecord.EventStatRecord(); + record.id = 22; + record.event = "EVENT"; + + recorder.write(date, record); + Assert.assertTrue(recorder.read(BatteryStatsFeature.getDateString(-1), "main").isEmpty()); + + List records = recorder.read(date, "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(record.id, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals(record.event, ((BatteryRecord.EventStatRecord) records.get(0)).event); + + Assert.assertTrue(recorder.read(BatteryStatsFeature.getDateString(0), "sub1").isEmpty()); + Assert.assertTrue(recorder.read(BatteryStatsFeature.getDateString(0), "sub2").isEmpty()); + + recorder.clean(BatteryStatsFeature.getDateString(-1), "main"); + Assert.assertFalse(recorder.read(date, "main").isEmpty()); + + recorder.clean(date, "sub1"); + recorder.clean(date, "sub2"); + Assert.assertFalse(recorder.read(date, "main").isEmpty()); + records = recorder.read(date, "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(record.id, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals(record.event, ((BatteryRecord.EventStatRecord) records.get(0)).event); + + recorder.clean(date, "main"); + Assert.assertTrue(recorder.read(date, "main").isEmpty()); + + mmkv.clearAll(); + } + + @Test + public void testCleanByDayLimit() { + BatteryMonitorConfig config = new BatteryMonitorConfig.Builder().enable(JiffiesMonitorFeature.class).build(); + BatteryMonitorCore core = new BatteryMonitorCore(config); + core.start(); + BatteryMonitorPlugin plugin = new BatteryMonitorPlugin(config); + Matrix.with().getPlugins().add(plugin); + + MMKV mmkv = MMKV.defaultMMKV(); + mmkv.clearAll(); + BatteryRecorder.MMKVRecorder recorder = new BatteryRecorder.MMKVRecorder(mmkv); + Assert.assertTrue(recorder.getProcSet().isEmpty()); + + int dayLimit = 7; + BatteryRecord.EventStatRecord record = new BatteryRecord.EventStatRecord(); + record.id = 22; + record.event = "KEEP"; + recorder.write(BatteryRecorder.MMKVRecorder.getDateString(0), record); + recorder.write(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit + 1), record); + record.event = "EXPIRED"; + recorder.write(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit), record); + recorder.flush(); + + List records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(0), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals("KEEP", ((BatteryRecord.EventStatRecord) records.get(0)).event); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit + 1), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals("KEEP", ((BatteryRecord.EventStatRecord) records.get(0)).event); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals("EXPIRED", ((BatteryRecord.EventStatRecord) records.get(0)).event); + + recorder.clean(dayLimit + 1); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(0), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals("KEEP", ((BatteryRecord.EventStatRecord) records.get(0)).event); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit + 1), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals("KEEP", ((BatteryRecord.EventStatRecord) records.get(0)).event); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + + recorder.clean(dayLimit); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(0), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals("KEEP", ((BatteryRecord.EventStatRecord) records.get(0)).event); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit + 1), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals("KEEP", ((BatteryRecord.EventStatRecord) records.get(0)).event); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit), "main"); + Assert.assertEquals(0, records.size()); + + recorder.clean(1); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(0), "main"); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals(22, ((BatteryRecord.EventStatRecord) records.get(0)).id); + Assert.assertEquals("KEEP", ((BatteryRecord.EventStatRecord) records.get(0)).event); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit + 1), "main"); + Assert.assertEquals(0, records.size()); + + records = recorder.read(BatteryRecorder.MMKVRecorder.getDateString(-dayLimit), "main"); + Assert.assertEquals(0, records.size()); + } + + @Test + public void testUniversalRecords() throws InterruptedException { + MMKV mmkv = MMKV.defaultMMKV(); + mmkv.clearAll(); + + String date = BatteryStatsFeature.getDateString(0); + String proc = "main"; + BatteryRecorder.MMKVRecorder recorder = new BatteryRecorder.MMKVRecorder(mmkv); + List reads = recorder.read(date, proc); + Assert.assertTrue(reads.isEmpty()); + int count = 0; + + BatteryRecord.ProcStatRecord procStatRecord = new BatteryRecord.ProcStatRecord(); + procStatRecord.pid = Process.myPid(); + procStatRecord.procStat = 22; + recorder.write(date, procStatRecord, proc); + Thread.sleep(10L); + reads = recorder.read(date, proc); + Assert.assertFalse(reads.isEmpty()); + Assert.assertTrue(reads.get(reads.size() - 1) instanceof BatteryRecord.ProcStatRecord); + Assert.assertEquals(procStatRecord.pid, ((BatteryRecord.ProcStatRecord) reads.get(reads.size() - 1)).pid); + Assert.assertEquals(procStatRecord.procStat, ((BatteryRecord.ProcStatRecord) reads.get(reads.size() - 1)).procStat); + + BatteryRecord.AppStatRecord appStat = new BatteryRecord.AppStatRecord(); + appStat.appStat = AppStats.APP_STAT_FOREGROUND; + recorder.write(date, appStat, proc); + Thread.sleep(10L); + reads = recorder.read(date, proc); + Assert.assertFalse(reads.isEmpty()); + Assert.assertTrue(reads.get(reads.size() - 1) instanceof BatteryRecord.AppStatRecord); + Assert.assertEquals(appStat.appStat, ((BatteryRecord.AppStatRecord) reads.get(reads.size() - 1)).appStat); + + BatteryRecord.SceneStatRecord sceneStatRecord = new BatteryRecord.SceneStatRecord(); + sceneStatRecord.scene = "Activity 1"; + recorder.write(date, sceneStatRecord, proc); + Thread.sleep(10L); + reads = recorder.read(date, proc); + Assert.assertFalse(reads.isEmpty()); + Assert.assertTrue(reads.get(reads.size() - 1) instanceof BatteryRecord.SceneStatRecord); + Assert.assertEquals(sceneStatRecord.scene, ((BatteryRecord.SceneStatRecord) reads.get(reads.size() - 1)).scene); + + BatteryRecord.DevStatRecord devStatRecord = new BatteryRecord.DevStatRecord(); + devStatRecord.devStat = AppStats.DEV_STAT_CHARGING; + recorder.write(date, devStatRecord, proc); + Thread.sleep(10L); + reads = recorder.read(date, proc); + Assert.assertFalse(reads.isEmpty()); + Assert.assertTrue(reads.get(reads.size() - 1) instanceof BatteryRecord.DevStatRecord); + Assert.assertEquals(devStatRecord.devStat, ((BatteryRecord.DevStatRecord) reads.get(reads.size() - 1)).devStat); + + BatteryRecord.ReportRecord reportRecord = new BatteryRecord.ReportRecord(); + reportRecord.scope = "xxx"; + reportRecord.threadInfoList = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + BatteryRecord.ReportRecord.ThreadInfo threadInfo = new BatteryRecord.ReportRecord.ThreadInfo(); + threadInfo.stat = "R" + i; + threadInfo.tid = 10000 + i; + threadInfo.name = "ThreadName_" + i; + threadInfo.jiffies = SystemClock.currentThreadTimeMillis() / 10; + reportRecord.threadInfoList.add(0, threadInfo); + } + reportRecord.entryList = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + BatteryRecord.ReportRecord.EntryInfo entryInfo = new BatteryRecord.ReportRecord.EntryInfo(); + entryInfo.name = "Entry Name " + i; + entryInfo.entries = new ArrayMap<>(); + entryInfo.entries.put("Key 1", "Value 1"); + entryInfo.entries.put("Key 2", "Value 2"); + entryInfo.entries.put("Key 3", "Value 3"); + reportRecord.entryList.add(entryInfo); + } + recorder.write(date, reportRecord, proc); + Thread.sleep(10L); + reads = recorder.read(date, proc); + Assert.assertFalse(reads.isEmpty()); + Assert.assertTrue(reads.get(reads.size() - 1) instanceof BatteryRecord.ReportRecord); + Assert.assertEquals(reportRecord.scope, ((BatteryRecord.ReportRecord) reads.get(reads.size() - 1)).scope); + Assert.assertEquals(5, ((BatteryRecord.ReportRecord) reads.get(reads.size() - 1)).threadInfoList.size()); + Assert.assertEquals(4, ((BatteryRecord.ReportRecord) reads.get(reads.size() - 1)).entryList.size()); + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryStatsFeatureTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryStatsFeatureTest.java new file mode 100644 index 000000000..94c021cbf --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/BatteryStatsFeatureTest.java @@ -0,0 +1,193 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.batterycanary.stats; + +import android.app.Application; +import android.content.Context; + +import com.tencent.matrix.Matrix; +import com.tencent.matrix.batterycanary.BatteryCanary; +import com.tencent.matrix.batterycanary.BatteryEventDelegate; +import com.tencent.matrix.batterycanary.BatteryMonitorPlugin; +import com.tencent.matrix.batterycanary.TestUtils; +import com.tencent.matrix.batterycanary.monitor.AppStats; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCallback; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.monitor.feature.AppStatMonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.DeviceStatMonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; +import com.tencent.matrix.batterycanary.utils.Consumer; +import com.tencent.mmkv.MMKV; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + + +@RunWith(AndroidJUnit4.class) +public class BatteryStatsFeatureTest { + static final String TAG = "Matrix.test.BatteryStatsFeatureTest"; + + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + if (!Matrix.isInstalled()) { + Matrix.init(new Matrix.Builder(((Application) mContext.getApplicationContext())).build()); + } + if (!BatteryEventDelegate.isInit()) { + BatteryEventDelegate.init((Application) mContext.getApplicationContext()); + } + String rootDir = MMKV.initialize(mContext); + System.out.println("mmkv root: " + rootDir); + } + + @After + public void shutDown() { + } + + @Test + public void testBatteryStatsWithMonitors() throws InterruptedException { + if (TestUtils.isAssembleTest()) { + return; + } + + final CountDownLatch latch = new CountDownLatch(11); + + MMKV mmkv = MMKV.defaultMMKV(); + mmkv.clearAll(); + BatteryRecorder.MMKVRecorder recorder = new BatteryRecorder.MMKVRecorder(mmkv) { + @Override + public void write(String date, BatteryRecord record) { + super.write(date, record); + latch.countDown(); + } + }; + recorder.clean(BatteryStatsFeature.getDateString(0), "main"); + + BatteryMonitorConfig config = new BatteryMonitorConfig.Builder() + .enable(JiffiesMonitorFeature.class) + .enable(AppStatMonitorFeature.class) + .enable(DeviceStatMonitorFeature.class) + .enableBuiltinForegroundNotify(false) + .enableForegroundMode(false) + .greyJiffiesTime(1) + .enable(BatteryStatsFeature.class) + .setRecorder(recorder) + .setCallback(new BatteryMonitorCallback.BatteryPrinter()) + .build(); + + final BatteryMonitorCore monitor = new BatteryMonitorCore(config); + BatteryMonitorPlugin plugin = new BatteryMonitorPlugin(monitor.getConfig()); + Matrix.with().getPlugins().add(plugin); + monitor.enableForegroundLoopCheck(false); + + BatteryStatsFeature batteryStatsFeature = BatteryCanary.getMonitorFeature(BatteryStatsFeature.class); + Assert.assertNotNull(batteryStatsFeature); + List records = batteryStatsFeature.readRecords(0, "main"); + Assert.assertTrue(records.isEmpty()); + + monitor.start(); + + batteryStatsFeature.onForeground(true); + batteryStatsFeature.statsScene("UI 1"); + batteryStatsFeature.statsScene("UI 2"); + batteryStatsFeature.statsScene("UI 3"); + batteryStatsFeature.onForeground(false); + batteryStatsFeature.statsDevStat(AppStats.DEV_STAT_CHARGING); + batteryStatsFeature.statsDevStat(AppStats.DEV_STAT_SCREEN_OFF); + batteryStatsFeature.statsDevStat(AppStats.DEV_STAT_UN_CHARGING); + batteryStatsFeature.onForeground(true); + + monitor.stop(); + + latch.await(); + BatteryCanary.getMonitorFeature(BatteryStatsFeature.class, new Consumer() { + @Override + public void accept(BatteryStatsFeature batteryStatsFeature) { + List records = batteryStatsFeature.readRecords(0, "main"); + Assert.assertEquals("Records list: " + records,11, records.size()); + } + }); + } + + @Test + public void testStatRecords() { + if (TestUtils.isAssembleTest()) { + return; + } + + MMKV mmkv = MMKV.defaultMMKV(); + mmkv.clearAll(); + + String proc = "main"; + BatteryMonitorConfig config = new BatteryMonitorConfig.Builder() + .enable(BatteryStatsFeature.class) + .setRecorder(new BatteryRecorder.MMKVRecorder(mmkv)) + .build(); + + final BatteryMonitorCore monitor = new BatteryMonitorCore(config); + BatteryMonitorPlugin plugin = new BatteryMonitorPlugin(monitor.getConfig()); + Matrix.with().getPlugins().add(plugin); + + BatteryStatsFeature batteryStatsFeature = BatteryCanary.getMonitorFeature(BatteryStatsFeature.class); + Assert.assertNotNull(batteryStatsFeature); + List records = batteryStatsFeature.readRecords(0, proc); + Assert.assertTrue(records.isEmpty()); + + batteryStatsFeature.setStatsImmediately(true); + monitor.start(); + records = batteryStatsFeature.readRecords(0, proc); + Assert.assertEquals(1, records.size()); + Assert.assertTrue(records.get(0) instanceof BatteryRecord.ProcStatRecord); + + batteryStatsFeature.statsAppStat(22); + records = batteryStatsFeature.readRecords(0, proc); + BatteryRecord record = records.get(records.size() - 1); + Assert.assertTrue(record instanceof BatteryRecord.AppStatRecord); + Assert.assertEquals(22, ((BatteryRecord.AppStatRecord) record).appStat); + + batteryStatsFeature.statsDevStat(5); + records = batteryStatsFeature.readRecords(0, proc); + record = records.get(records.size() - 1); + Assert.assertTrue(record instanceof BatteryRecord.DevStatRecord); + Assert.assertEquals(5, ((BatteryRecord.DevStatRecord) record).devStat); + + batteryStatsFeature.statsScene("HEY_DUDE!!"); + records = batteryStatsFeature.readRecords(0, proc); + record = records.get(records.size() - 1); + Assert.assertTrue(record instanceof BatteryRecord.SceneStatRecord); + Assert.assertEquals("HEY_DUDE!!", ((BatteryRecord.SceneStatRecord) record).scene); + + batteryStatsFeature.statsEvent("!!HEY_DUDE!!"); + records = batteryStatsFeature.readRecords(0, proc); + record = records.get(records.size() - 1); + Assert.assertTrue(record instanceof BatteryRecord.EventStatRecord); + Assert.assertEquals("!!HEY_DUDE!!", ((BatteryRecord.EventStatRecord) record).event); + Assert.assertEquals(0, ((BatteryRecord.EventStatRecord) record).id); + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/HealthStatsTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/HealthStatsTest.java new file mode 100644 index 000000000..f03c1b7c3 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/stats/HealthStatsTest.java @@ -0,0 +1,792 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.batterycanary.stats; + +import android.app.Application; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.os.health.HealthStats; +import android.os.health.SystemHealthManager; +import android.os.health.TimerStat; +import android.os.health.UidHealthStats; + +import com.tencent.matrix.Matrix; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; +import com.tencent.matrix.batterycanary.stats.HealthStatsFeature.HealthStatsSnapshot; +import com.tencent.matrix.batterycanary.utils.PowerProfile; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + + +@RunWith(AndroidJUnit4.class) +public class HealthStatsTest { + static final String TAG = "Matrix.test.HealthStatsTest"; + + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + if (!Matrix.isInstalled()) { + Matrix.init(new Matrix.Builder(((Application) mContext.getApplicationContext())).build()); + } + } + + @After + public void shutDown() { + } + + private BatteryMonitorCore mockMonitor() { + BatteryMonitorConfig config = new BatteryMonitorConfig.Builder() + .enable(CpuStatFeature.class) + .enable(HealthStatsFeature.class) + .enableBuiltinForegroundNotify(false) + .enableForegroundMode(false) + .wakelockTimeout(1000) + .greyJiffiesTime(100) + .foregroundLoopCheckTime(1000) + .build(); + return new BatteryMonitorCore(config); + } + + @Test + public void testRoundDecimalPlace() { + Assert.assertEquals(1.1d, HealthStatsHelper.round(1.12345d, 1), 0.00001d); + Assert.assertEquals(1.2d, HealthStatsHelper.round(1.19945d, 1), 0.00001d); + Assert.assertEquals(1.12d, HealthStatsHelper.round(1.12345d, 2), 0.00001d); + Assert.assertEquals(1.13d, HealthStatsHelper.round(1.12945d, 2), 0.00001d); + Assert.assertEquals(1.1234d, HealthStatsHelper.round(1.12341d, 4), 0.00001d); + Assert.assertEquals(1.1235d, HealthStatsHelper.round(1.12345d, 4), 0.00001d); + Assert.assertEquals(1.1200d, HealthStatsHelper.round(1.12d, 2), 0.00001d); + Assert.assertEquals(1.1200d, HealthStatsHelper.round(1.1245d, 2), 0.00001d); + } + + @Test + public void testGetSenorsHandle() throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + SensorManager sm = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + Assert.assertNotNull(sm); + List sensorList = sm.getSensorList(Sensor.TYPE_ALL); + Assert.assertFalse(sensorList.isEmpty()); + + for (Sensor item : sensorList) { + String name = item.getName(); + int id = item.getId(); + int type = item.getType(); + float power = item.getPower(); + Method method = item.getClass().getDeclaredMethod("getHandle"); + int handle = (int) method.invoke(item); + Assert.assertTrue(handle > 0); + } + } + + @Test + public void testLiterateStats() { + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + Assert.assertEquals("UidHealthStats", healthStats.getDataType()); + + List statsList = new ArrayList<>(); + statsList.add(healthStats); + + int statsKeyCount = healthStats.getStatsKeyCount(); + Assert.assertTrue(statsKeyCount > 0); + + for (int i = 0; i < statsKeyCount; i++) { + int key = healthStats.getStatsKeyAt(i); + if (healthStats.hasStats(key)) { + Map stats = healthStats.getStats(key); + for (HealthStats item : stats.values()) { + Assert.assertTrue(Arrays.asList( + "PidHealthStats", + "ProcessHealthStats", + "PackageHealthStats", + "ServiceHealthStats" + ).contains(item.getDataType())); + + statsList.add(item); + + int subKeyCount = item.getStatsKeyCount(); + if (subKeyCount > 0) { + for (int j = 0; j < subKeyCount; j++) { + int subKey = item.getStatsKeyAt(j); + if (item.hasStats(subKey)) { + statsList.addAll(item.getStats(subKey).values()); + } + } + } + } + } + } + + for (HealthStats item : statsList) { + int count = item.getMeasurementKeyCount(); + for (int i = 0; i < count; i++) { + int key = item.getMeasurementKeyAt(i); + long value = item.getMeasurement(key); + Assert.assertTrue(item.getDataType() + ": " + key + "=" + value, value >= 0); + } + } + for (HealthStats item : statsList) { + int count = item.getMeasurementsKeyCount(); + for (int i = 0; i < count; i++) { + int key = item.getMeasurementsKeyAt(i); + Map values = item.getMeasurements(key); + for (Map.Entry entry : values.entrySet()) { + Assert.assertTrue(item.getDataType() + ": " + entry.getKey() + "=" + entry.getValue(), entry.getValue() >= 0); + } + } + } + + for (HealthStats item : statsList) { + int count = item.getTimerKeyCount(); + for (int i = 0; i < count; i++) { + int key = item.getTimerKeyAt(i); + TimerStat timerStat = item.getTimer(key); + Assert.assertTrue(item.getDataType() + ": " + key + "=" + timerStat.getCount() + "," + timerStat.getTime(), timerStat.getCount() >= 0); + Assert.assertTrue(item.getDataType() + ": " + key + "=" + timerStat.getCount() + "," + timerStat.getTime(), timerStat.getTime() >= 0); + } + } + for (HealthStats item : statsList) { + int count = item.getTimersKeyCount(); + for (int i = 0; i < count; i++) { + int key = item.getTimersKeyAt(i); + Map timerStatMap = item.getTimers(key); + for (Map.Entry entry : timerStatMap.entrySet()) { + Assert.assertTrue(item.getDataType() + ": " + entry.getKey() + "=" + entry.getValue().getCount() + "," + entry.getValue().getTime(), entry.getValue().getCount() >= 0); + Assert.assertTrue(item.getDataType() + ": " + entry.getKey() + "=" + entry.getValue().getCount() + "," + entry.getValue().getTime(), entry.getValue().getTime() >= 0); + } + } + } + } + + @Test + public void getGetArrayItemAvgPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + for (String key : PowerProfile.getPowerItemMap().keySet()) { + Assert.assertEquals(powerProfile.getAveragePower(key), powerProfile.getAveragePowerUni(key), 0d); + } + + for (String key : PowerProfile.getPowerArrayMap().keySet()) { + Assert.assertEquals(powerProfile.getAveragePower(key, 0), powerProfile.getAveragePower(key), 0d); + + double sum = 0; + int num = powerProfile.getNumElements(key); + for (int i = 0; i < num; i++) { + sum += powerProfile.getAveragePower(key, i); + } + Assert.assertEquals(sum / num, powerProfile.getAveragePowerUni(key), 0d); + } + } + + @Test + public void testEstimateCpuPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + double cpuActivePower = powerProfile.getAveragePower("cpu.active"); + Assert.assertTrue(cpuActivePower > 0); + UsageBasedPowerEstimator etmCpuActivePower = new UsageBasedPowerEstimator(cpuActivePower); + } + + @Test + public void testEstimateCpuPowerByHealthStats() throws IOException { + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + if (healthStats.hasStats(UidHealthStats.MEASUREMENT_CPU_POWER_MAMS)) { + double powerMah = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_CPU_POWER_MAMS) / (1000.0 * 60 * 60); + Assert.assertTrue(powerMah >= 0); + } + } + + @Test + public void testEstimateCpuPowerByCpuStats() throws IOException { + CpuStatFeature feature = new CpuStatFeature(); + feature.configure(mockMonitor()); + feature.onTurnOn(); + + Assert.assertTrue(feature.isSupported()); + CpuStatFeature.CpuStateSnapshot cpuStateSnapshot = feature.currentCpuStateSnapshot(); + Assert.assertTrue(cpuStateSnapshot.procCpuCoreStates.size() > 0); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_USER_CPU_TIME_MS); + healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_SYSTEM_CPU_TIME_MS); + long cpuTimeMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_USER_CPU_TIME_MS) + healthStats.getMeasurement(UidHealthStats.MEASUREMENT_SYSTEM_CPU_TIME_MS); + + double powerMahByUid = 0, powerMahByDev = 0; + double activePower = HealthStatsHelper.estimateCpuActivePower(feature.getPowerProfile(), cpuTimeMs); + Assert.assertTrue(activePower >= 0); + powerMahByUid += activePower; + powerMahByDev += activePower; + + + powerMahByUid += HealthStatsHelper.estimateCpuClustersPowerByUidStats(feature.getPowerProfile(), cpuStateSnapshot, cpuTimeMs, false); + Assert.assertTrue(powerMahByUid >= 0); + powerMahByUid += HealthStatsHelper.estimateCpuCoresPowerByUidStats(feature.getPowerProfile(), cpuStateSnapshot, cpuTimeMs, false); + Assert.assertTrue(powerMahByUid >= 0); + + powerMahByDev += HealthStatsHelper.estimateCpuClustersPowerByDevStats(feature.getPowerProfile(), cpuStateSnapshot, cpuTimeMs); + Assert.assertTrue(powerMahByDev >= 0); + powerMahByDev += HealthStatsHelper.estimateCpuCoresPowerByDevStats(feature.getPowerProfile(), cpuStateSnapshot, cpuTimeMs); + Assert.assertTrue(powerMahByDev >= 0); + + double calcCpuPower = HealthStatsHelper.calcCpuPower(feature.getPowerProfile(), healthStats); + Assert.assertEquals(powerMahByDev, calcCpuPower, 1d); + } + + @Test + public void testEstimateMemoryPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + int num = powerProfile.getNumElements(PowerProfile.POWER_MEMORY); + for (int i = 0; i < num; i++) { + Assert.assertTrue(powerProfile.getAveragePower(PowerProfile.POWER_MEMORY, num) > 0); + } + + double calcPower = HealthStatsHelper.calcMemoryPower(powerProfile); + Assert.assertTrue(calcPower >= 0); + } + + @Test + public void testEstimateWakelockPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + Assert.assertTrue(powerProfile.getAveragePower("cpu.idle") > 0); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + double powerMah = 0; + if (healthStats.hasTimers(UidHealthStats.TIMERS_WAKELOCKS_PARTIAL)) { + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_WAKELOCKS_PARTIAL); + long timeMs = 0; + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + double powerMa = powerProfile.getAveragePower("cpu.idle"); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + Assert.assertTrue(powerMah >= 0); + + double calcPower = HealthStatsHelper.calcWakelocksPower(powerProfile, healthStats); + Assert.assertEquals(powerMah, calcPower, 1d); + } + + @Test + public void testEstimateMobileRadioPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_MOBILE_POWER_MAMS)) { + double powerMahByHealthStats = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_MOBILE_POWER_MAMS) / (1000.0 * 60 * 60); + Assert.assertTrue(powerMahByHealthStats >= 0); + } + + double powerMahByRadio = 0; + if (powerProfile.getAveragePower("radio.active") > 0) { + if (healthStats.hasTimer(UidHealthStats.TIMER_MOBILE_RADIO_ACTIVE)) { + long timeMs = healthStats.getTimerTime(UidHealthStats.TIMER_MOBILE_RADIO_ACTIVE); + double powerMa = powerProfile.getAveragePower("radio.active"); + powerMahByRadio += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + } + Assert.assertTrue(powerMahByRadio >= 0); + + double powerMahByTime = 0; + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_MOBILE_IDLE_MS)) { + long timeMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_MOBILE_IDLE_MS); + double powerMa = powerProfile.getAveragePower("modem.controller.idle"); + powerMahByTime += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_MOBILE_RX_MS)) { + long timeMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_MOBILE_RX_MS); + double powerMa = powerProfile.getAveragePower("modem.controller.rx"); + powerMahByTime += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_MOBILE_TX_MS)) { + long timeMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_MOBILE_TX_MS); + double powerMa = powerProfile.getAveragePower("modem.controller.tx"); + powerMahByTime += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + Assert.assertTrue(powerMahByTime >= 0); + + double calcPower = HealthStatsHelper.calcMobilePower(powerProfile, healthStats); + Assert.assertEquals(powerMahByTime, calcPower, 1d); + } + + @Test + public void testEstimateWifiPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + + double wifiIdlePower = powerProfile.getAveragePower("wifi.controller.idle"); + Assert.assertTrue(wifiIdlePower >= 0); + UsageBasedPowerEstimator etmWifiIdlePower = new UsageBasedPowerEstimator(wifiIdlePower); + double wifiRxPower = powerProfile.getAveragePower("wifi.controller.rx"); + Assert.assertTrue(wifiRxPower >= 0); + UsageBasedPowerEstimator etmWifiRxPower = new UsageBasedPowerEstimator(wifiIdlePower); + double wifiTxPower = powerProfile.getAveragePower("wifi.controller.tx"); + Assert.assertTrue(wifiTxPower >= 0); + UsageBasedPowerEstimator etmWifiTxPower = new UsageBasedPowerEstimator(wifiIdlePower); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + // calc from packets + // double power = 0; + // double powerMaPerPacket = 0; + // MatrixLog.i(TAG, "estimate WIFI by packets"); + // { + // final long wifiBps = 1000000; + // final double averageWifiActivePower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_ACTIVE) / 3600; + // powerMaPerPacket = averageWifiActivePower / (((double) wifiBps) / 8 / 2048); + // long packets = 1213737 + 244433; + // power += powerMaPerPacket * packets; + // } + // { + // double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_ON); + // long timeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_RUNNING_MS); + // power += new HealthStatsHelper.UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + // } + // { + // double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_SCAN); + // long timeMs = getTimerTime(healthStats, UidHealthStats.TIMER_WIFI_SCAN); + // power += new HealthStatsHelper.UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + // } + // Assert.fail("power: " + power + ", watt: " + powerMaPerPacket); + + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_WIFI_POWER_MAMS)) { + double powerMahByHealthStats = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_WIFI_POWER_MAMS) / (1000.0 * 60 * 60); + Assert.assertTrue(powerMahByHealthStats >= 0); + } + + double powerMah = 0; + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_WIFI_IDLE_MS)) { + long idleMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_WIFI_IDLE_MS); + powerMah += etmWifiIdlePower.calculatePower(idleMs); + } + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_WIFI_RX_MS)) { + long rxMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_WIFI_RX_MS); + powerMah += etmWifiRxPower.calculatePower(rxMs); + } + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_WIFI_TX_MS)) { + long txMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_WIFI_TX_MS); + powerMah += etmWifiTxPower.calculatePower(txMs); + } + Assert.assertTrue(powerMah >= 0); + + double calcPower = HealthStatsHelper.calcWifiPower(powerProfile, healthStats); + Assert.assertEquals(powerMah, calcPower, 1d); + } + + @Test + public void testEstimateBlueToothPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + Assert.assertTrue(powerProfile.getAveragePower("bluetooth.controller.idle") > 0); + Assert.assertTrue(powerProfile.getAveragePower("bluetooth.controller.rx") > 0); + Assert.assertTrue(powerProfile.getAveragePower("bluetooth.controller.tx") > 0); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_BLUETOOTH_POWER_MAMS)) { + double powerMahByHealthStats = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_BLUETOOTH_POWER_MAMS) / (1000.0 * 60 * 60); + Assert.assertTrue(powerMahByHealthStats >= 0); + } + + double powerMah = 0; + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_BLUETOOTH_IDLE_MS)) { + long timeMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_BLUETOOTH_IDLE_MS); + double powerMa = powerProfile.getAveragePower("bluetooth.controller.idle"); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_BLUETOOTH_RX_MS)) { + long timeMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_BLUETOOTH_RX_MS); + double powerMa = powerProfile.getAveragePower("bluetooth.controller.rx"); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + if (healthStats.hasMeasurement(UidHealthStats.MEASUREMENT_BLUETOOTH_TX_MS)) { + long timeMs = healthStats.getMeasurement(UidHealthStats.MEASUREMENT_BLUETOOTH_TX_MS); + double powerMa = powerProfile.getAveragePower("bluetooth.controller.tx"); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + Assert.assertTrue(powerMah >= 0); + + double calcPower = HealthStatsHelper.calcBlueToothPower(powerProfile, healthStats); + Assert.assertEquals(powerMah, calcPower, 1d); + } + + @Test + public void testEstimateGpsPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + double powerMa = 0; + powerMa = powerProfile.getAveragePower("gps.on"); + if (powerMa <= 0) { + int num = powerProfile.getNumElements("gps.signalqualitybased"); + double sumMa = 0; + for (int i = 0; i < num; i++) { + sumMa += powerProfile.getAveragePower("gps.signalqualitybased", i); + } + powerMa = sumMa / num; + } + + Assert.assertTrue(powerMa > 0); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + double powerMah = 0; + if (healthStats.hasTimer(UidHealthStats.TIMER_GPS_SENSOR)) { + long timeMs = healthStats.getTimerTime(UidHealthStats.TIMER_GPS_SENSOR); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + Assert.assertTrue(powerMah >= 0); + + double calcPower = HealthStatsHelper.calcGpsPower(powerProfile, healthStats); + Assert.assertEquals(powerMah, calcPower, 1d); + } + + @Test + public void testEstimateSensorsPower() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + double powerMah = 0; + if (healthStats.hasTimers(UidHealthStats.TIMERS_SENSORS)) { + SensorManager sm = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + Assert.assertNotNull(sm); + List sensorList = sm.getSensorList(Sensor.TYPE_ALL); + Assert.assertFalse(sensorList.isEmpty()); + + Map sensorMap = new HashMap<>(); + for (Sensor item : sensorList) { + Method method = item.getClass().getDeclaredMethod("getHandle"); + int handle = (int) method.invoke(item); + sensorMap.put(String.valueOf(handle), item); + } + + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_SENSORS); + for (Map.Entry item : timers.entrySet()) { + String handle = item.getKey(); + long timeMs = item.getValue().getTime(); + if (handle.equals("-10000")) { + continue; // skip GPS Sensors + } + Sensor sensor = sensorMap.get(handle); + if (sensor != null) { + powerMah += new UsageBasedPowerEstimator(sensor.getPower()).calculatePower(timeMs); + } + } + } + Assert.assertTrue(powerMah >= 0); + } + + @Test + public void testEstimateMediaAndHwPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + double powerMah = 0; + + // Camera + Assert.assertTrue(powerProfile.getAveragePower("camera.avg") > 0); + if (healthStats.hasTimer(UidHealthStats.TIMER_CAMERA)) { + long timeMs = healthStats.getTimerTime(UidHealthStats.TIMER_CAMERA); + double powerMa = powerProfile.getAveragePower("camera.avg"); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + + double calcPower = HealthStatsHelper.calcCameraPower(powerProfile, healthStats); + Assert.assertEquals(powerMah, calcPower, 1d); + + // Flash Light + Assert.assertTrue(powerProfile.getAveragePower("camera.flashlight") > 0); + if (healthStats.hasTimer(UidHealthStats.TIMER_FLASHLIGHT)) { + long timeMs = healthStats.getTimerTime(UidHealthStats.TIMER_FLASHLIGHT); + double powerMa = powerProfile.getAveragePower("camera.flashlight"); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + + calcPower = HealthStatsHelper.calcFlashLightPower(powerProfile, healthStats); + Assert.assertEquals(powerMah, calcPower, 1d); + + // Media + Assert.assertTrue(powerProfile.getAveragePower("audio") > 0); + Assert.assertTrue(powerProfile.getAveragePower("video") > 0); + if (healthStats.hasTimer(UidHealthStats.TIMER_AUDIO)) { + long timeMs = healthStats.getTimerTime(UidHealthStats.TIMER_AUDIO); + double powerMa = powerProfile.getAveragePower("audio"); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + + calcPower = HealthStatsHelper.calcAudioPower(powerProfile, healthStats); + Assert.assertEquals(powerMah, calcPower, 1d); + + if (healthStats.hasTimer(UidHealthStats.TIMER_VIDEO)) { + long timeMs = healthStats.getTimerTime(UidHealthStats.TIMER_VIDEO); + double powerMa = powerProfile.getAveragePower("video"); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + Assert.assertTrue(powerMah >= 0); + + calcPower = HealthStatsHelper.calcVideoPower(powerProfile, healthStats); + Assert.assertEquals(powerMah, calcPower, 1d); + } + + @Test + public void testEstimateScreenPower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + Assert.assertTrue(powerProfile.getAveragePower("screen.on") > 0); + Assert.assertTrue(powerProfile.getAveragePower("screen.full") > 0); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + double calcPower = HealthStatsHelper.calcScreenPower(powerProfile, healthStats); + Assert.assertTrue(calcPower >= 0); + } + + @Test + public void testEstimateSystemServicePower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + CpuStatFeature feature = new CpuStatFeature(); + feature.configure(mockMonitor()); + feature.onTurnOn(); + + Assert.assertTrue(feature.isSupported()); + CpuStatFeature.CpuStateSnapshot cpuStateSnapshot = feature.currentCpuStateSnapshot(); + Assert.assertTrue(cpuStateSnapshot.procCpuCoreStates.size() > 0); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + long timeMs = 0; + if (healthStats.hasTimers(UidHealthStats.TIMERS_JOBS)) { + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_JOBS); + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + } + if (healthStats.hasTimers(UidHealthStats.TIMERS_SYNCS)) { + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_SYNCS); + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + } + + double calcPower = HealthStatsHelper.calcSystemServicePower(powerProfile, healthStats); + Assert.assertTrue(calcPower >= 0); + } + + @Test + public void testEstimateIdlePower() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + Assert.assertTrue(powerProfile.getAveragePower(PowerProfile.POWER_CPU_SUSPEND) > 0); + Assert.assertTrue(powerProfile.getAveragePower(PowerProfile.POWER_CPU_IDLE) > 0); + + SystemHealthManager manager = (SystemHealthManager) mContext.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + HealthStats healthStats = manager.takeMyUidSnapshot(); + Assert.assertNotNull(healthStats); + + double calcPower = HealthStatsHelper.calcIdlePower(powerProfile, healthStats); + Assert.assertTrue(calcPower >= 0); + } + + @Test + public void testGetCurrSnapshot() { + HealthStatsFeature feature = new HealthStatsFeature(); + feature.configure(mockMonitor()); + feature.onTurnOn(); + + Assert.assertNotNull(feature.currHealthStats()); + + HealthStatsSnapshot bgn = feature.currHealthStatsSnapshot(); + Assert.assertNotNull(bgn); + Assert.assertNotNull(bgn.healthStats); + + HealthStatsSnapshot end = feature.currHealthStatsSnapshot(); + Assert.assertNotNull(end); + Assert.assertNotNull(end.healthStats); + + Delta delta = end.diff(bgn); + Assert.assertNotNull(delta); + Assert.assertNull(delta.dlt.healthStats); + + Assert.assertEquals(delta.dlt.getTotalPower(), end.getTotalPower() - bgn.getTotalPower(), 0.001d); + } + + @Test + public void testHealthStatsAccCollecting() throws InterruptedException { + HealthStatsFeature feature = new HealthStatsFeature(); + feature.configure(mockMonitor()); + feature.onTurnOn(); + + Assert.assertNotNull(feature.currHealthStats()); + + HealthStatsSnapshot bgn = feature.currHealthStatsSnapshot(); + Assert.assertNotNull(bgn); + + Assert.assertNull(bgn.accCollector); + HealthStatsSnapshot.AccCollector accCollector = bgn.startAccCollecting(); + Assert.assertNotNull(accCollector); + Assert.assertSame(bgn.accCollector, accCollector); + Assert.assertSame(bgn, accCollector.last); + + int accCount = 2; + long intervalMs = 2000L; + long accDuringMs = 0L; + for (int i = 0; i < accCount; i++) { + Thread.sleep(intervalMs); + HealthStatsSnapshot curr = feature.currHealthStatsSnapshot(); + Delta delta = bgn.accCollect(curr); + Assert.assertNotNull(delta); + Assert.assertEquals(i + 1, accCollector.count); + Assert.assertEquals(accDuringMs += delta.during, accCollector.duringMs); + } + + Thread.sleep(intervalMs); + HealthStatsSnapshot end = feature.currHealthStatsSnapshot(); + Assert.assertNotNull(end); + + Assert.assertEquals(accCount, accCollector.count); + Delta deltaByAcc = end.diffByAccCollector(bgn); + Assert.assertNotNull(deltaByAcc); + Assert.assertTrue(deltaByAcc instanceof Delta.SimpleDelta); + Assert.assertEquals(accCount + 1, accCollector.count); + + Delta delta = end.diff(bgn); + Assert.assertNotNull(delta); + Assert.assertFalse(delta instanceof Delta.SimpleDelta); + + Assert.assertSame(delta.bgn, deltaByAcc.bgn); + Assert.assertSame(delta.end, deltaByAcc.end); + Assert.assertEquals(delta.during, deltaByAcc.during); + Assert.assertEquals(delta.dlt.isDelta, deltaByAcc.dlt.isDelta); + Assert.assertEquals(delta.dlt.getTotalPower(), deltaByAcc.dlt.getTotalPower(), 0.0001d); + + Assert.assertSame(delta.end, accCollector.last); + Assert.assertEquals(delta.during, accCollector.duringMs); + } + + @Test + public void testCalcAvgPowerPerPacket() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + final long MOBILE_BPS = 200000; + final double MOBILE_POWER = powerProfile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ACTIVE, 120) / 3600; + final double mobilePps = (((double) MOBILE_BPS) / 8 / 2048); + double mobilePowerPerPacket = (MOBILE_POWER / mobilePps) / (60 * 60); + Assert.assertTrue(mobilePowerPerPacket > 0); + + final long wifiBps = 1000000; + final double averageWifiActivePower = powerProfile.getAveragePowerOrDefault(PowerProfile.POWER_WIFI_ACTIVE, 120) / 3600; + double wifiPowerPerPacket = averageWifiActivePower / (((double) wifiBps) / 8 / 2048); + Assert.assertTrue(wifiPowerPerPacket > 0); + + Assert.assertTrue(mobilePowerPerPacket < wifiPowerPerPacket); + } + + + public static class UsageBasedPowerEstimator { + private static final double MILLIS_IN_HOUR = 1000.0 * 60 * 60; + private final double mAveragePowerMahPerMs; + + public UsageBasedPowerEstimator(double averagePowerMilliAmp) { + mAveragePowerMahPerMs = averagePowerMilliAmp / MILLIS_IN_HOUR; + } + + public boolean isSupported() { + return mAveragePowerMahPerMs != 0; + } + + public double calculatePower(long durationMs) { + return mAveragePowerMahPerMs * durationMs; + } + } + +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/BatteryCurrencyInspectorTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/BatteryCurrencyInspectorTest.java new file mode 100644 index 000000000..3a13fb736 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/BatteryCurrencyInspectorTest.java @@ -0,0 +1,53 @@ +package com.tencent.matrix.batterycanary.utils; + +import android.app.Application; +import android.content.Context; + +import com.tencent.matrix.Matrix; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +/** + * @author Kaede + * @since 9/9/2022 + */ +@RunWith(AndroidJUnit4.class) +public class BatteryCurrencyInspectorTest { + static final String TAG = "Matrix.test.BatteryCurrencyInspectorTest"; + + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + if (!Matrix.isInstalled()) { + Matrix.init(new Matrix.Builder(((Application) mContext.getApplicationContext())).build()); + } + } + + @After + public void shutDown() { + } + + @Test + @SuppressWarnings("ConstantConditions") + public void testInspection() { + boolean charging = BatteryCanaryUtil.isDeviceCharging(mContext); + if (charging) { + Assert.assertNull(BatteryCurrencyInspector.isMicroAmpCurr(mContext)); + Assert.assertTrue(BatteryCurrencyInspector.isPositiveInChargeCurr(mContext)); + Assert.assertNull(BatteryCurrencyInspector.isPositiveOutOfChargeCurr(mContext)); + } else { + Assert.assertTrue(BatteryCurrencyInspector.isMicroAmpCurr(mContext)); + Assert.assertNull(BatteryCurrencyInspector.isPositiveInChargeCurr(mContext)); + Assert.assertFalse(BatteryCurrencyInspector.isPositiveOutOfChargeCurr(mContext)); + } + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/BatteryMetricsTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/BatteryMetricsTest.java index 7c9fa89cb..c8e7520a0 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/BatteryMetricsTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/BatteryMetricsTest.java @@ -23,10 +23,12 @@ import android.os.Looper; import android.os.Process; import android.os.SystemClock; +import android.system.Os; import android.text.TextUtils; import android.util.Log; import com.tencent.matrix.batterycanary.TestUtils; +import com.tencent.matrix.batterycanary.monitor.feature.CompositeMonitors; import org.junit.After; import org.junit.Assert; @@ -44,6 +46,7 @@ import java.util.Arrays; import java.util.List; import java.util.Stack; +import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -53,7 +56,6 @@ import static com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil.JIFFY_MILLIS; import static com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil.ONE_HOR; -import static com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil.ONE_MIN; @RunWith(AndroidJUnit4.class) @@ -103,6 +105,88 @@ public void testReadPowerProfile() throws Exception { Assert.assertTrue(PowerProfile.getInstance().isSupported()); } + @Test + public void testReadPowerProfileTest() throws Exception { + Class clazz = Class.forName("com.android.internal.os.PowerProfile"); + Constructor constructor = clazz.getConstructor(Context.class); + Object obj = constructor.newInstance(mContext); + Assert.assertNotNull(obj); + + int id = mContext.getResources().getIdentifier("power_profile_test", "xml", "android"); + Assert.assertTrue(id > 0); + } + + @Test + public void testFindPowerProfileFile() throws Exception { + Callable findBlock = new Callable() { + @Override + public File call() { + String customDirs = Os.getenv("CUST_POLICY_DIRS"); + Assert.assertFalse(TextUtils.isEmpty(customDirs)); + for (String dir : customDirs.split(":")) { + // example: /hw_product/etc/xml/power_profile.xml + File file = new File(dir, "/xml/power_profile.xml"); + if (file.exists() && file.canRead()) { + return file; + } + } + return null; + } + }; + + File file = findBlock.call(); + if (file != null) { + PowerProfile powerProfile = new PowerProfile(mContext); + Assert.fail(PowerProfile.getResType()); + powerProfile.readPowerValuesFromFilePath(mContext, file); + powerProfile.initCpuClusters(); + powerProfile.smoke(); + } + } + + @Test + public void KernelCpuUidFreqTimeReaderCompat() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + int[] clusterSteps = new int[powerProfile.getNumCpuClusters()]; + for (int i = 0; i < clusterSteps.length; i++) { + clusterSteps[i] = powerProfile.getNumSpeedStepsInCpuCluster(i); + } + KernelCpuUidFreqTimeReader reader = new KernelCpuUidFreqTimeReader(Process.myPid(), clusterSteps); + reader.smoke(); + } + + @Test + public void testCpuCoreStepSpeedsSampling() throws IOException, InterruptedException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + List cpuFreqSteps = BatteryCanaryUtil.getCpuFreqSteps(); + int[] cpuCurrentFreq = BatteryCanaryUtil.getCpuCurrentFreq(); + Assert.assertNotNull(cpuFreqSteps); + Assert.assertNotNull(cpuCurrentFreq); + + new Thread(new Runnable() { + @Override + public void run() { + while (true) { + } + } + }).start(); + + CompositeMonitors.CpuFreqSampler sampler = new CompositeMonitors.CpuFreqSampler(BatteryCanaryUtil.getCpuFreqSteps()); + Assert.assertTrue(sampler.isCompat(powerProfile)); + if (!TestUtils.isAssembleTest()) { + while (true) { + sampler.count(BatteryCanaryUtil.getCpuCurrentFreq()); + Thread.sleep(5000L); + } + } + } + @Test public void testGetAppCpuCoreNumByTid() { StringBuilder tips = new StringBuilder("\n"); @@ -259,7 +343,7 @@ public void testReadKernelCpuSpeedState() throws IOException { for (int i = 0; i < readers.length; i++) { KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; Assert.assertNotNull(kernelCpuSpeedReader); - Assert.assertTrue(kernelCpuSpeedReader.readTotoal() > 0); + Assert.assertTrue(kernelCpuSpeedReader.readTotal() > 0); long[] cpuCoreJiffies = kernelCpuSpeedReader.readAbsolute(); Assert.assertEquals(powerProfile.getNumSpeedStepsInCpuCluster(i), cpuCoreJiffies.length); for (int j = 0; j < cpuCoreJiffies.length; j++) { @@ -291,7 +375,7 @@ public void testConfigureCpuLoad() throws IOException { for (int i = 0; i < readers.length; i++) { KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; Assert.assertNotNull(kernelCpuSpeedReader); - long cpuCoreJiffies = kernelCpuSpeedReader.readTotoal(); + long cpuCoreJiffies = kernelCpuSpeedReader.readTotal(); Assert.assertTrue("idx = " + i + ", read = " + Arrays.toString(kernelCpuSpeedReader.readAbsolute()), cpuCoreJiffies > 0); cpuJiffiesBgn += cpuCoreJiffies; } @@ -303,7 +387,7 @@ public void testConfigureCpuLoad() throws IOException { for (int i = 0; i < readers.length; i++) { KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; Assert.assertNotNull(kernelCpuSpeedReader); - long cpuCoreJiffies = kernelCpuSpeedReader.readTotoal(); + long cpuCoreJiffies = kernelCpuSpeedReader.readTotal(); Assert.assertTrue(cpuCoreJiffies > 0); cpuJiffiesEnd += cpuCoreJiffies; } @@ -346,7 +430,7 @@ public void testConfigureCpuLoadWithMultiThread() throws IOException { for (int i = 0; i < readers.length; i++) { KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; Assert.assertNotNull(kernelCpuSpeedReader); - long cpuCoreJiffies = kernelCpuSpeedReader.readTotoal(); + long cpuCoreJiffies = kernelCpuSpeedReader.readTotal(); Assert.assertTrue("idx = " + i + ", read = " + Arrays.toString(kernelCpuSpeedReader.readAbsolute()), cpuCoreJiffies > 0); cpuJiffiesBgn += cpuCoreJiffies; } @@ -366,7 +450,7 @@ public void run() { for (int i = 0; i < readers.length; i++) { KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; Assert.assertNotNull(kernelCpuSpeedReader); - long cpuCoreJiffies = kernelCpuSpeedReader.readTotoal(); + long cpuCoreJiffies = kernelCpuSpeedReader.readTotal(); Assert.assertTrue(cpuCoreJiffies > 0); cpuJiffiesEnd += cpuCoreJiffies; } @@ -513,7 +597,7 @@ public void testConfigureCpuBatterySippingDelta() throws IOException { for (int i = 0; i < readers.length; i++) { KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; Assert.assertNotNull(kernelCpuSpeedReader); - long cpuCoreJiffies = kernelCpuSpeedReader.readTotoal(); + long cpuCoreJiffies = kernelCpuSpeedReader.readTotal(); Assert.assertTrue("idx = " + i + ", read = " + Arrays.toString(kernelCpuSpeedReader.readAbsolute()), cpuCoreJiffies > 0); cpuJiffiesBgn += cpuCoreJiffies; } @@ -532,7 +616,7 @@ public void testConfigureCpuBatterySippingDelta() throws IOException { for (int i = 0; i < readers.length; i++) { KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; Assert.assertNotNull(kernelCpuSpeedReader); - long cpuCoreJiffies = kernelCpuSpeedReader.readTotoal(); + long cpuCoreJiffies = kernelCpuSpeedReader.readTotal(); Assert.assertTrue(cpuCoreJiffies > 0); cpuJiffiesEnd += cpuCoreJiffies; } @@ -809,6 +893,64 @@ public void testJiffiesConsumption2() { } } + @Test + public void testRead() throws IOException { + PowerProfile powerProfile = PowerProfile.init(mContext); + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + + List read = KernelCpuFreqPolicyReader.read(); + long uptimeBgn = SystemClock.uptimeMillis(); + long policyBgn = 0; + long kernelBgn = 0; + long procBgn = ProcStatUtil.of(Process.myPid()).getJiffies(); + + for (KernelCpuFreqPolicyReader item : read) { + policyBgn += item.readTotal(); + } + KernelCpuSpeedReader[] readers = new KernelCpuSpeedReader[BatteryCanaryUtil.getCpuCoreNum()]; + for (int i = 0; i < BatteryCanaryUtil.getCpuCoreNum(); i++) { + final int numSpeedSteps = powerProfile.getNumSpeedStepsInCpuCluster(powerProfile.getClusterByCpuNum(i)); + readers[i] = new KernelCpuSpeedReader(i, numSpeedSteps); + } + for (int i = 0; i < readers.length; i++) { + KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; + Assert.assertNotNull(kernelCpuSpeedReader); + long cpuCoreJiffies = kernelCpuSpeedReader.readTotal(); + Assert.assertTrue("idx = " + i + ", read = " + Arrays.toString(kernelCpuSpeedReader.readAbsolute()), cpuCoreJiffies > 0); + kernelBgn += cpuCoreJiffies; + } + + // No matter full-running or sleep, the delta of cpufreq policy & cpu speed time is always the same? + CpuConsumption.hanoi(20); + // try { + // Thread.sleep(10000L); + // } catch (InterruptedException e) { + // e.printStackTrace(); + // } + + long uptimeEnd = SystemClock.uptimeMillis(); + long policyEnd = 0; + long kernelEnd = 0; + long procEnd = ProcStatUtil.of(Process.myPid()).getJiffies(); + + for (KernelCpuFreqPolicyReader item : read) { + policyEnd += item.readTotal(); + } + for (int i = 0; i < readers.length; i++) { + KernelCpuSpeedReader kernelCpuSpeedReader = readers[i]; + Assert.assertNotNull(kernelCpuSpeedReader); + long cpuCoreJiffies = kernelCpuSpeedReader.readTotal(); + Assert.assertTrue(cpuCoreJiffies > 0); + kernelEnd += cpuCoreJiffies; + } + + Assert.fail("policy: " + (policyEnd - policyBgn) * 10f / (uptimeEnd - uptimeBgn) + + ", kernel: " + (kernelEnd - kernelBgn) * 10f / (uptimeEnd - uptimeBgn) + + ", procStat: " + (procEnd - procBgn) * 10f / (uptimeEnd - uptimeBgn) + ); + } + public static class CpuConsumption { public static void inc(int num) { for (int i = 0; i < num; i++) { diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/CallStackCollectorTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/CallStackCollectorTest.java new file mode 100644 index 000000000..73e2113f6 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/CallStackCollectorTest.java @@ -0,0 +1,101 @@ +package com.tencent.matrix.batterycanary.utils; + +import android.content.Context; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; + +import com.tencent.matrix.batterycanary.TestUtils; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +/** + * @author Kaede + * @since 2021/12/17 + */ +@RunWith(AndroidJUnit4.class) +public class CallStackCollectorTest { + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + } + + @Test + public void testUnwindWithThrowable() { + final RuntimeException[] throwable = new RuntimeException[1]; + Thread testThread = new Thread(new Runnable() { + @Override + public void run() { + throwable[0] = new RuntimeException(); + } + }, "test-thread"); + testThread.start(); + + while (throwable[0] == null) {} + StackTraceElement[] stackTrace = throwable[0].getStackTrace(); + Assert.assertNotNull(stackTrace); + + String mine = Log.getStackTraceString(new Throwable()); + String they = Log.getStackTraceString(throwable[0]); + Assert.assertNotEquals(mine, they); + Assert.assertFalse(mine.contains("at "+ CallStackCollectorTest.class.getName() +"$1.run(")); + Assert.assertTrue(they.contains("at "+ CallStackCollectorTest.class.getName() +"$1.run(")); + + if (!TestUtils.isAssembleTest()) { + throw throwable[0]; + } + } + + + @RunWith(AndroidJUnit4.class) + public static class Benchmark { + + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + } + + @After + public void shutDown() { + } + + @Test + @SuppressWarnings("ThrowableNotThrown") + public void testProcStatBenchmark() { + if (TestUtils.isAssembleTest()) return; + + long delta1, delta2; + long bgnMillis, endMillis; + + int loopCount = 100000; + + bgnMillis = SystemClock.uptimeMillis(); + for (int i = 0; i < loopCount; i++) { + new Throwable().getStackTrace(); + } + endMillis = SystemClock.uptimeMillis(); + delta1 = endMillis - bgnMillis; + + bgnMillis = SystemClock.uptimeMillis(); + for (int i = 0; i < loopCount; i++) { + Thread.currentThread().getStackTrace(); + // Looper.getMainLooper().getThread().getStackTrace(); + } + endMillis = SystemClock.uptimeMillis(); + delta2 = endMillis - bgnMillis; + + Assert.fail("TIME CONSUMED: " + delta1 + " vs " + delta2); + } + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/CanaryUtilsTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/CanaryUtilsTest.java index a3c43e5a0..a8539be08 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/CanaryUtilsTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/CanaryUtilsTest.java @@ -24,13 +24,12 @@ import android.app.Service; import android.content.Context; import android.content.Intent; +import android.os.BatteryManager; import android.os.Handler; import android.os.IBinder; import android.os.Looper; - -import androidx.core.app.NotificationCompat; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; +import android.os.PowerManager; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; @@ -48,11 +47,17 @@ import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import androidx.core.app.NotificationCompat; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + import static android.content.Context.ACTIVITY_SERVICE; @@ -130,6 +135,29 @@ public void testGetPkgName() { Assert.assertEquals(mContext.getPackageName(), pkg); } + @Test + public void testGetCpuCoreNum() throws IOException { + long nanos1 = SystemClock.elapsedRealtimeNanos(); + + int coreNum1 = BatteryCanaryUtil.getCpuCoreNumImmediately(); + long nanos2 = SystemClock.elapsedRealtimeNanos(); + + int coreNum2 = Runtime.getRuntime().availableProcessors(); + long nanos3 = SystemClock.elapsedRealtimeNanos(); + + PowerProfile powerProfile = PowerProfile.init(mContext); + int coreNum3 = powerProfile.getCpuCoreNum(); + long nanos4 = SystemClock.elapsedRealtimeNanos(); + + + Assert.assertEquals(coreNum1, coreNum2); + Assert.assertEquals(coreNum2, coreNum3); + + if (!TestUtils.isAssembleTest()) { + Assert.fail((nanos2 - nanos1) + " vs " + (nanos3 - nanos2) + " vs " + (nanos4 - nanos3)); + } + } + @Test public void testGetThrowableStack() { if (TestUtils.isAssembleTest()) return; @@ -166,6 +194,22 @@ public void testGetCpuFreq() throws InterruptedException { } } + @Test + public void testGetCpuFreqSteps() throws InterruptedException { + List last = null; + for (int i = 0; i < 5; i++) { + List freqSteps = BatteryCanaryUtil.getCpuFreqSteps(); + Assert.assertNotNull(freqSteps); + if (last != null) { + for (int j = 0, freqStepsSize = freqSteps.size(); j < freqStepsSize; j++) { + Assert.assertArrayEquals(last.get(j), freqSteps.get(j)); + } + } + last = freqSteps; + Thread.sleep(100L); + } + } + @Test public void testGetBatteryTemps() throws InterruptedException { for (int i = 0; i < 5; i++) { @@ -208,6 +252,66 @@ public void testCheckDeviceOnPowerSaveMode() { Assert.assertFalse(result); } + @Test + public void testGetBatteryPercentage() { + int pct = BatteryCanaryUtil.getBatteryPercentageImmediately(mContext); + Assert.assertTrue(pct > 0); + } + + @Test + public void testGetBatteryCapacity() throws Exception { + PowerProfile powerProfile = PowerProfile.init(mContext); + int capacity0 = BatteryCanaryUtil.getBatteryCapacityImmediately(mContext); + Assert.assertTrue(capacity0 > 0); + + Assert.assertNotNull(powerProfile); + Assert.assertTrue(powerProfile.isSupported()); + double capacity1 = powerProfile.getBatteryCapacity(); + Assert.assertEquals(capacity0, capacity1, 1000); + + Class profileClass = Class.forName("com.android.internal.os.PowerProfile"); + Object profileObject = profileClass.getConstructor(Context.class).newInstance(mContext); + Method method = profileClass.getMethod("getAveragePower", String.class); + double capacity2 = (double) method.invoke(profileObject, "battery.capacity"); + method = profileClass.getMethod("getBatteryCapacity"); + double capacity3 = (double) method.invoke(profileObject); + + Assert.assertEquals(capacity1, capacity2, 1d); + Assert.assertEquals(capacity2, capacity3, 1d); + + BatteryManager batteryManager = (BatteryManager) mContext.getSystemService(Context.BATTERY_SERVICE); + int chargeCounter = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER); + int capacity = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); + + double capacity4 = ((chargeCounter / (float) capacity) * 100) / 1000d; + Assert.assertEquals(capacity3, capacity4, 1000d); + } + + @Test + public void testGetBatteryCurrent() { + BatteryManager batteryManager = (BatteryManager) mContext.getSystemService(Context.BATTERY_SERVICE); + long avgCurrent = 0, currentNow = 0; + avgCurrent = batteryManager.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE); + currentNow = batteryManager.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW); + Log.d(TAG, "BATTERY_PROPERTY_CURRENT_AVERAGE = " + avgCurrent + "microA"); + Log.d(TAG, "BATTERY_PROPERTY_CURRENT_NOW = " + currentNow + "microA"); + Assert.assertNotEquals(0, avgCurrent); + Assert.assertNotEquals(0, currentNow); + + long value = BatteryCanaryUtil.getBatteryCurrencyImmediately(mContext); + Assert.assertNotEquals(0, value); + } + + @Test + public void testCheckIfLowBattery() { + Intent intent = BatteryCanaryUtil.getBatteryStickyIntent(mContext); + Assert.assertNotNull(intent); + int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + + boolean lowBattery = BatteryCanaryUtil.isLowBattery(mContext); + Assert.assertEquals(level <= 2, lowBattery); + } + @Test public void testCheckAppForeGroundService() { boolean hasRunningService = false; @@ -296,6 +400,45 @@ public void testExpireRef() throws InterruptedException { } } + @Test + public void testGetThermalStatus() throws InterruptedException { + int status = BatteryCanaryUtil.getThermalStatImmediately(mContext); + Assert.assertTrue(status >= 0); + + final PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + final AtomicBoolean hasNotify = new AtomicBoolean(); + powerManager.addThermalStatusListener(new PowerManager.OnThermalStatusChangedListener() { + @Override + public void onThermalStatusChanged(int status) { + float room = BatteryCanaryUtil.getThermalHeadroomImmediately(mContext, 10); + Assert.assertTrue(Float.isNaN(room) || room > 0); + synchronized (hasNotify) { + hasNotify.notify(); + hasNotify.set(true); + } + } + }); + + if (!hasNotify.get()) { + synchronized (hasNotify) { + hasNotify.wait(10000); + } + } + float room = BatteryCanaryUtil.getThermalHeadroomImmediately(mContext, 10); + Assert.assertTrue(Float.isNaN(room) || room > 0); + } + + @Test + public void testGetBatteryChargingStatus() { + Intent intent = BatteryCanaryUtil.getBatteryStickyIntent(mContext); + Assert.assertNotNull(intent); + Assert.assertTrue(intent.getIntExtra("max_charging_current", -1) > 0); + Assert.assertTrue(intent.getIntExtra("max_charging_voltage", -1) > 0); + + int watt = BatteryCanaryUtil.getChargingWattImmediately(mContext); + Assert.assertTrue(watt > 0); + } + private static boolean diceWithBase(int base) { double dice = Math.random(); if (base >= 1 && dice < (1 / ((double) base))) { diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/ConnectivityManagerHookerTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/ConnectivityManagerHookerTest.java new file mode 100644 index 000000000..fba7caac2 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/ConnectivityManagerHookerTest.java @@ -0,0 +1,181 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.batterycanary.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.SystemClock; + +import com.tencent.matrix.batterycanary.TestUtils; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import static com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil.ONE_HOR; + + +@RunWith(AndroidJUnit4.class) +public class ConnectivityManagerHookerTest { + static final String TAG = "Matrix.test.ConnectivityManagerHookerTest"; + + Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + } + + @After + public void shutDown() { + } + + @Test + public void testScanning() throws Exception { + if (TestUtils.isAssembleTest()) return; + + final AtomicInteger scanInc = new AtomicInteger(); + final AtomicInteger getScanInc = new AtomicInteger(); + SystemServiceBinderHooker hooker = new SystemServiceBinderHooker(Context.CONNECTIVITY_SERVICE, "android.net.IConnectivityManager", new SystemServiceBinderHooker.HookCallback() { + @Override + public void onServiceMethodInvoke(Method method, Object[] args) { + Assert.assertNotNull(method); + } + + @Nullable + @Override + public Object onServiceMethodIntercept(Object receiver, Method method, Object[] args) throws Throwable { + return null; + } + }); + + hooker.doHook(); + + ConnectivityManager manager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + Assert.assertNotNull(manager); + + List>> pairs = new ArrayList<>(); + for (Network item : manager.getAllNetworks()) { + NetworkInfo networkInfo = manager.getNetworkInfo(item); + if (networkInfo != null && (networkInfo.isConnected() || networkInfo.isConnectedOrConnecting())) { + pairs.add(Arrays.asList( + new Pair<>(networkInfo.getType(), String.valueOf(networkInfo.getTypeName())), + new Pair<>(networkInfo.getSubtype(), String.valueOf(networkInfo.getSubtypeName())) + )); + int txBwKBps = 0, rxBwKBps = 0; + NetworkCapabilities capabilities = manager.getNetworkCapabilities(item); + if (capabilities != null) { + txBwKBps = capabilities.getLinkUpstreamBandwidthKbps() / 8; + rxBwKBps = capabilities.getLinkDownstreamBandwidthKbps() / 8; + } + } + } + + Assert.assertFalse(pairs.isEmpty()); + + ConnectivityManager.NetworkCallback callback = new ConnectivityManager.NetworkCallback() { + long mLastMs = 0; + + @Override + public void onAvailable(@NonNull Network network) { + super.onAvailable(network); + } + + @Override + public void onLosing(@NonNull Network network, int maxMsToLive) { + super.onLosing(network, maxMsToLive); + } + + @Override + public void onLost(@NonNull Network network) { + super.onLost(network); + } + + @Override + public void onUnavailable() { + super.onUnavailable(); + } + + @SuppressLint({"WrongConstant", "RestrictedApi"}) + @Override + public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { + super.onCapabilitiesChanged(network, networkCapabilities); + try { + int strength = 0; + int[] capabilities = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + strength = networkCapabilities.getSignalStrength(); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + capabilities = networkCapabilities.getCapabilities(); + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + } + } catch (Throwable ignored) { + } + + // Dual-link check: both wifi & cellar transported networks + { + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + } + } + } catch (Throwable e) { + } + } + + @Override + public void onLinkPropertiesChanged(@NonNull Network network, @NonNull LinkProperties linkProperties) { + super.onLinkPropertiesChanged(network, linkProperties); + } + + @Override + public void onBlockedStatusChanged(@NonNull Network network, boolean blocked) { + super.onBlockedStatusChanged(network, blocked); + } + }; + + manager.registerDefaultNetworkCallback(callback); + hooker.doUnHook(); + } +} + diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/KernelCpuFreqPolicyReader.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/KernelCpuFreqPolicyReader.java new file mode 100644 index 000000000..20b3ec3d5 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/KernelCpuFreqPolicyReader.java @@ -0,0 +1,62 @@ +package com.tencent.matrix.batterycanary.utils; + + +import android.text.TextUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +public class KernelCpuFreqPolicyReader { + private static final String TAG = "KernelCpuSpeedReader"; + + public static List read() { + List list = new ArrayList<>(); + File file = new File("/sys/devices/system/cpu/cpufreq/"); + File[] files = file.listFiles(); + if (files != null) { + for (File item : files) { + if (item.isDirectory() && item.getName().startsWith("policy") && item.canRead()) { + list.add(new KernelCpuFreqPolicyReader(item.getName())); + } + } + } + return list; + } + + private final String mPolicy; + private final String mProcFile; + + public KernelCpuFreqPolicyReader(String policy) { + mPolicy = policy; + mProcFile = "/sys/devices/system/cpu/cpufreq/" + policy + "/stats/time_in_state"; + } + + public long readTotal() throws IOException { + long sum = 0; + for (long item : readAbsolute()) { + sum += item; + } + return sum; + } + + public List readAbsolute() throws IOException { + List speedTimeJiffies = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(mProcFile))) { + TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(' '); + String line; + while ((line = reader.readLine()) != null) { + splitter.setString(line); + splitter.next(); + speedTimeJiffies.add(Long.parseLong(splitter.next())); + } + } catch (Throwable e) { + throw new IOException("Failed to read cpu-freq time: " + e.getMessage(), e); + } + return speedTimeJiffies; + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/PowerManagerHookerTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/PowerManagerHookerTest.java index 9f7efb31e..a4f73c639 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/PowerManagerHookerTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/PowerManagerHookerTest.java @@ -16,10 +16,14 @@ package com.tencent.matrix.batterycanary.utils; +import android.annotation.SuppressLint; import android.content.Context; import android.os.IBinder; import android.os.PowerManager; import android.os.WorkSource; +import android.text.TextUtils; +import android.util.Log; + import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -31,6 +35,7 @@ import org.junit.runner.RunWith; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -89,6 +94,8 @@ public void onReleaseWakeLock(IBinder token, int flags) { PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); wakLockRef.set(wakeLock); + String wakeLockTag = getWakeLockTag(wakeLock); + Assert.assertEquals(TAG, wakeLockTag); Assert.assertNotNull(wakeLock); wakeLock.acquire(); @@ -101,6 +108,51 @@ public void onReleaseWakeLock(IBinder token, int flags) { Assert.assertTrue(hasRelease.get()); } + private static String getWakeLockTag(PowerManager.WakeLock wakeLock) { + String tag = null; + try { + @SuppressWarnings("JavaReflectionMemberAccess") + @SuppressLint({"SoonBlockedPrivateApi"}) + Method method = wakeLock.getClass().getDeclaredMethod("getTag"); + method.setAccessible(true); + tag = (String) method.invoke(wakeLock); + } catch (Throwable e) { + Log.e(TAG, "getTag err: " + e); + } + if (TextUtils.isEmpty(tag)) { + try { + @SuppressWarnings("JavaReflectionMemberAccess") + @SuppressLint("DiscouragedPrivateApi") + Field field = wakeLock.getClass().getDeclaredField("mTag"); + field.setAccessible(true); + tag = (String) field.get(wakeLock); + } catch (Throwable e) { + Log.e(TAG, "mTag err: " + e); + } + } + if (TextUtils.isEmpty(tag)) { + try { + @SuppressWarnings("JavaReflectionMemberAccess") + @SuppressLint({"SoonBlockedPrivateApi"}) + Field field = wakeLock.getClass().getDeclaredField("mTraceName"); + field.setAccessible(true); + tag = (String) field.get(wakeLock); + if (tag != null && tag.startsWith("WakeLock (") && tag.endsWith(")")) { + int idxBgn = tag.indexOf("WakeLock (") + "WakeLock (".length(); + int idxEnd = tag.lastIndexOf(")"); + if (idxBgn < idxEnd) { + tag = tag.substring(idxBgn, idxEnd); + } + } + } catch (Throwable e) { + Log.e(TAG, "mTraceName err: " + e); + } + } + + Log.i(TAG, "getWakeLockTag: " + tag); + return tag; + } + @Ignore @Test public void testAcquireWakeupTimeout() throws Exception { diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/ProcStatUtilsTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/ProcStatUtilsTest.java index 47047667b..0f5deae15 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/ProcStatUtilsTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/ProcStatUtilsTest.java @@ -24,12 +24,9 @@ import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; -import android.util.Pair; import android.util.SparseArray; import com.tencent.matrix.batterycanary.TestUtils; -import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; -import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature; import com.tencent.matrix.util.MatrixLog; import org.junit.After; @@ -41,6 +38,7 @@ import java.io.BufferedReader; import java.io.File; +import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -51,10 +49,15 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import androidx.arch.core.util.Function; +import androidx.core.util.Consumer; +import androidx.core.util.Pair; +import androidx.core.util.Supplier; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; @@ -94,6 +97,165 @@ public void testGetCurrentMethodName() { Assert.assertEquals("testGetCurrentMethodName", ste[2].getMethodName()); } + @Test + public void testGetRootProcStat() { + List names = Arrays.asList( + "asound", + "ath_pktlog", + "bldrlog", + "buddyinfo", + "bus", + "cgroups", + "cld", + "cmdline", + "config.gz", + "consoles", + "crypto", + "debugdriver", + "driver", + "dynamic_debug", + "device-tree", + "devices", + "diskstats", + "execdomains", + "fs", + "fts", + "fb", + "filesystems", + "interrupts", + "iomem", + "ioports", + "irq", + "kallsyms", + "key-users", + "keys", + "kmsg", + "kpagecount", + "kpageflags", + "loadavg", + "locks", + "misc", + "modules", + "net", + "pressure", + "pagetypeinfo", + "partitions", + "sched_debug", + "slabinfo", + "sysrq-trigger", + "schedstat", + "softirqs", + "stat", + "swaps", + "sysrq-trigger", + "scsi", + "sys", + "tty", + "timer_list", + "uid", + "uid_concurrent_active_time", + "uid_concurrent_policy_time", + "uid_cputime", + "uid_io", + "uid_procstat", + "uid_time_in_state", + "uptime", + "version", + "vmallocinfo", + "vmstat", + "zoneinfo" + ); + + final Function inWhiteList = new Function() { + @Override + public Boolean apply(File input) { + for (String item : Arrays.asList( + "/proc/sys/kernel/perf_", + "/proc/sys/kernel/random/" + )) { + if (input.getAbsolutePath().startsWith(item)) { + return true; + } + } + return false; + } + }; + + final Consumer fileConsumer = new Consumer() { + @Override + public void accept(File file) { + if (file.canRead()) { + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null && files.length > 0) { + for (File item : files) { + this.accept(item); + } + } + } else { + try (InputStream is = new FileInputStream(file)) { + Collection cat = IOUtil.readLines(is); + if (!inWhiteList.apply(file)) { + Assert.assertNull("Should be null-content: " + file.getAbsolutePath(), cat); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + }; + for (String item : names) { + File file = new File("proc/" + item); + Assert.assertTrue("shoud exits: " + item, file.exists()); + fileConsumer.accept(file); + } + } + + @Test + public void testGetReadableRootProcStat() { + List names = Arrays.asList( + "cpuinfo", + "meminfo", + "mounts", + "sys/kernel" + ); + + final Function>, Pair>> func = new Function>, Pair>>() { + @SuppressWarnings("ConstantConditions") + @Override + public Pair> apply(Pair> input) { + if (input.first.canRead()) { + if (input.first.isDirectory()) { + File[] files = input.first.listFiles(); + if (files != null && files.length > 0) { + for (File item : files) { + this.apply(new Pair<>(item, input.second)); + } + } + } else { + try (InputStream is = new FileInputStream(input.first)) { + Collection cat = IOUtil.readLines(is); + Assert.assertNotNull("Should not be empty: " + input.first.getAbsolutePath(), cat); + input.second.add(cat.toArray(new String[0])); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + return input; + } + }; + + for (String item : names) { + File file = new File("proc/" + item); + Assert.assertTrue("shoud exits: " + item, file.exists()); + Assert.assertTrue("shoud readable: " + item, file.canRead()); + Pair> ret = func.apply(new Pair>(file, new ArrayList())); + Assert.assertNotEquals("shoud not empty: " + item, 0, ret.second.size()); + } + } + @Test public void testGetThreadJiffies() { ProcStatUtil.ProcStat stat = ProcStatUtil.current(); @@ -188,6 +350,18 @@ public void testGetCpuLoad() { public void testGetProcStat() { String cat = BatteryCanaryUtil.cat("/proc/stat"); Assert.assertTrue(TextUtils.isEmpty(cat)); + + File file = new File("/proc"); + File[] files = file.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isFile(); + } + }); + for (File item : files) { + cat = BatteryCanaryUtil.cat("/proc/stat"); + Assert.assertTrue(TextUtils.isEmpty(cat)); + } } /** diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/TemperatureUtilsTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/TemperatureUtilsTest.java index f0770112b..2945dc677 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/TemperatureUtilsTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/TemperatureUtilsTest.java @@ -69,6 +69,22 @@ public void setUp() { public void shutDown() { } + @Test + public void getHardwareManager() { + HardwarePropertiesManager manager = (HardwarePropertiesManager) mContext.getSystemService(Context.HARDWARE_PROPERTIES_SERVICE); + Assert.assertNotNull(manager); + try { + manager.getCpuUsages(); + Assert.fail("Should be permission denied"); + } catch (SecurityException ignored) { + } + try { + manager.getFanSpeeds(); + Assert.fail("Should be permission denied"); + } catch (SecurityException ignored) { + } + } + @Test public void testGetCpuTemp() { HardwarePropertiesManager manager = (HardwarePropertiesManager) mContext.getSystemService(Context.HARDWARE_PROPERTIES_SERVICE); diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/TimeBreakerTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/TimeBreakerTest.java index 7b15e76a7..14798ba73 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/TimeBreakerTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/TimeBreakerTest.java @@ -20,15 +20,21 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Objects; @@ -278,6 +284,54 @@ public TimeBreaker.Stamp stamp(String key) { } } + @Test + public void testPortionFromJson() throws JSONException { + final List stampList = new ArrayList<>(); + JSONArray json = new JSONArray("[\n" + + " {\n" + + " \"key\": \"1\",\n" + + " \"upTime\": 13269846,\n" + + " \"statMillis\": \"05:25:05\"\n" + + " },\n" + + " {\n" + + " \"key\": \"1\",\n" + + " \"upTime\": 13269845,\n" + + " \"statMillis\": \"05:25:05\"\n" + + " }\n" + + "]"); + + for (int i = 0; i < json.length(); i++) { + JSONObject jsonObject = json.getJSONObject(i); + long upTime = jsonObject.getLong("upTime"); + Timestamp timestamp = Timestamp.valueOf(new SimpleDateFormat("yyyy-MM-dd ") + .format(new Date()) + .concat(jsonObject.getString("statMillis"))); + long statMillis = timestamp.getTime(); + TimeBreaker.Stamp stamp = new TimeBreaker.Stamp(jsonObject.getString("key"), upTime, statMillis); + stampList.add(stamp); + } + + Assert.assertFalse(stampList.isEmpty()); + + long windowMs = BatteryCanaryUtil.ONE_HOR; + final String currStat = "1"; + final long currUpTimeMs = 16978289; + final long currStatMs = Timestamp.valueOf(new SimpleDateFormat("yyyy-MM-dd ") + .format(new Date()) + .concat("06:26:54")).getTime(); + TimeBreaker.TimePortions portions = TimeBreaker.configurePortions( + stampList, + windowMs, + 10L, + new TimeBreaker.Stamp.Stamper() { + @Override + public TimeBreaker.Stamp stamp(String key) { + return new TimeBreaker.Stamp(currStat, currUpTimeMs, currStatMs); + } + }); + Assert.assertTrue(portions.isValid()); + } + @Test public void testConcurrentBenchmark() throws InterruptedException { final List stampList = new ArrayList<>(); diff --git a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/WifiManagerHookerTest.java b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/WifiManagerHookerTest.java index c1ecc28d1..02a36cadf 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/WifiManagerHookerTest.java +++ b/matrix/matrix-android/matrix-battery-canary/src/androidTest/java/com/tencent/matrix/batterycanary/utils/WifiManagerHookerTest.java @@ -36,7 +36,7 @@ @RunWith(AndroidJUnit4.class) public class WifiManagerHookerTest { - static final String TAG = "Matrix.test.BleManagerHookerTest"; + static final String TAG = "Matrix.test.WifiManagerHookerTest"; Context mContext; diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/AndroidManifest.xml b/matrix/matrix-android/matrix-battery-canary/src/main/AndroidManifest.xml index cf79d9abe..a7cd0a922 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/AndroidManifest.xml +++ b/matrix/matrix-android/matrix-battery-canary/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ package="com.tencent.matrix.batterycanary"> + diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/BatteryEventDelegate.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/BatteryEventDelegate.java index 240e3256e..e55051a26 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/BatteryEventDelegate.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/BatteryEventDelegate.java @@ -6,17 +6,16 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.Build; import android.os.Handler; import android.os.Looper; +import android.os.PowerManager; import android.os.SystemClock; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.annotation.StringDef; -import androidx.annotation.UiThread; -import androidx.annotation.VisibleForTesting; +import com.tencent.matrix.batterycanary.monitor.AppStats; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.stats.BatteryStatsFeature; import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.util.MatrixLog; @@ -26,12 +25,24 @@ import java.util.LinkedList; import java.util.List; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.annotation.StringDef; +import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; + +import static com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil.ONE_MIN; + /** * @author Kaede * @since 2021/1/11 */ public final class BatteryEventDelegate { public static final String TAG = "Matrix.battery.LifeCycle"; + private static final int BATTERY_POWER_GRADUATION = 5; + private static final int BATTERY_TEMPERATURE_GRADUATION = 15; @SuppressLint("StaticFieldLeak") static volatile BatteryEventDelegate sInstance; @@ -78,6 +89,9 @@ public static BatteryEventDelegate getInstance() { final Handler mUiHandler = new Handler(Looper.getMainLooper()); final BackgroundTask mAppLowEnergyTask = new BackgroundTask(); boolean sIsForeground = true; + long mLastBatteryPowerPct; + long mLastBatteryTemp; + @Nullable BatteryMonitorCore mCore; @@ -87,6 +101,8 @@ public static BatteryEventDelegate getInstance() { throw new IllegalStateException("Context should not be null"); } mContext = context; + mLastBatteryPowerPct = BatteryCanaryUtil.getBatteryPercentageImmediately(context); + mLastBatteryTemp = BatteryCanaryUtil.getBatteryTemperature(context); } public BatteryEventDelegate attach(BatteryMonitorCore core) { @@ -111,26 +127,151 @@ public void onForeground(boolean isForeground) { @RestrictTo(RestrictTo.Scope.LIBRARY) public void startListening() { IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_POWER_CONNECTED); filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + filter.addAction(Intent.ACTION_BATTERY_OKAY); + filter.addAction(Intent.ACTION_BATTERY_LOW); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + } + } + mContext.registerReceiver(new BroadcastReceiver() { + long mLastBatteryChangedHandleMs = -1; @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(Context context, final Intent intent) { String action = intent.getAction(); if (action != null) { - switch (action) { - case Intent.ACTION_SCREEN_ON: - case Intent.ACTION_SCREEN_OFF: - case Intent.ACTION_POWER_CONNECTED: - case Intent.ACTION_POWER_DISCONNECTED: + if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { + // 1. Check battery power & temperature changed + // Received 'ACTION_BATTERY_CHANGED' frequently, + // should be frequency-controlled & handled with worker thread + if (mCore != null) { + boolean limited = false; + long currMs = System.currentTimeMillis(); + if (mLastBatteryChangedHandleMs > 0 && currMs - mLastBatteryChangedHandleMs < ONE_MIN) { + limited = true; + } + if (!limited) { + mLastBatteryChangedHandleMs = currMs; + mCore.getHandler().post(new Runnable() { + @Override + public void run() { + // Full charged + int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + if (status == BatteryManager.BATTERY_STATUS_FULL) { + onBatteryFullCharged(); + return; + } + + // Power percentage + int currPct = BatteryCanaryUtil.getBatteryPercentage(mContext); + if (currPct >= 0 && currPct <= 1000) { + if (Math.abs(currPct - mLastBatteryPowerPct) >= BATTERY_POWER_GRADUATION) { + mLastBatteryPowerPct = currPct; + if (mCore != null) { + BatteryStatsFeature feat = mCore.getMonitorFeature(BatteryStatsFeature.class); + if (feat != null) { + feat.statsBatteryEvent(currPct); + } + } + onBatteryPowerChanged(currPct); + } + } + + // Battery temperature + try { + int currTemp = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1); + if (currTemp >= 0 && currPct <= 1000) { + if (Math.abs(currTemp - mLastBatteryTemp) >= BATTERY_TEMPERATURE_GRADUATION) { + mLastBatteryTemp = currTemp; + if (mCore != null) { + BatteryStatsFeature feat = mCore.getMonitorFeature(BatteryStatsFeature.class); + if (feat != null) { + feat.statsBatteryTempEvent(currTemp); + } + } + onBatteryTemperatureChanged(currTemp); + } + } + } catch (Exception e) { + MatrixLog.w(TAG, "get EXTRA_TEMPERATURE failed: " + e.getMessage()); + } + } + }); + } + } + + } else { + int devStat = -1; + boolean notifyStateChanged = false, notifyBatteryLowChanged = false; + switch (action) { + case Intent.ACTION_SCREEN_ON: + devStat = AppStats.DEV_STAT_SCREEN_ON; + notifyStateChanged = true; + break; + case Intent.ACTION_SCREEN_OFF: + devStat = AppStats.DEV_STAT_SCREEN_OFF; + notifyStateChanged = true; + break; + case Intent.ACTION_POWER_CONNECTED: + devStat = AppStats.DEV_STAT_CHARGING; + notifyStateChanged = true; + break; + case Intent.ACTION_POWER_DISCONNECTED: + devStat = AppStats.DEV_STAT_UN_CHARGING; + notifyStateChanged = true; + break; + case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED: + devStat = BatteryCanaryUtil.isDeviceOnIdleMode(context) ? AppStats.DEV_STAT_DOZE_MODE_ON : AppStats.DEV_STAT_DOZE_MODE_OFF; + notifyStateChanged = true; + break; + case PowerManager.ACTION_POWER_SAVE_MODE_CHANGED: + devStat = BatteryCanaryUtil.isDeviceOnPowerSave(context) ? AppStats.DEV_STAT_SAVE_POWER_MODE_ON : AppStats.DEV_STAT_SAVE_POWER_MODE_OFF; + notifyStateChanged = true; + break; + case Intent.ACTION_BATTERY_OKAY: + case Intent.ACTION_BATTERY_LOW: + notifyStateChanged = true; + notifyBatteryLowChanged = true; + break; + default: + break; + } + + // 2. Stat device status changed + if (devStat != -1) { + if (mCore != null) { + BatteryStatsFeature feat = mCore.getMonitorFeature(BatteryStatsFeature.class); + if (feat != null) { + feat.statsDevStat(devStat); + } + } + } + + // 3. Notify state changed + if (notifyStateChanged) { onSateChangedEvent(intent); - break; + } + if (notifyBatteryLowChanged) { + if (mCore != null) { + BatteryStatsFeature feat = mCore.getMonitorFeature(BatteryStatsFeature.class); + if (feat != null) { + feat.statsBatteryEvent(action.equals(Intent.ACTION_BATTERY_LOW)); + } + } + onBatteryChangeEvent(intent); + } } } - } }, filter); } @@ -184,13 +325,72 @@ public void run() { } } + private void onBatteryChangeEvent(final Intent intent) { + if (Looper.myLooper() == Looper.getMainLooper()) { + dispatchBatteryStateChangedEvent(intent); + } else { + mUiHandler.post(new Runnable() { + @Override + public void run() { + dispatchBatteryStateChangedEvent(intent); + } + }); + } + } + + private void onBatteryPowerChanged(final int pct) { + if (Looper.myLooper() == Looper.getMainLooper()) { + dispatchBatteryPowerChanged(pct); + } else { + mUiHandler.post(new Runnable() { + @Override + public void run() { + dispatchBatteryPowerChanged(pct); + } + }); + } + } + + private void onBatteryFullCharged() { + if (Looper.myLooper() == Looper.getMainLooper()) { + dispatchBatteryFullCharged(); + } else { + mUiHandler.post(new Runnable() { + @Override + public void run() { + dispatchBatteryFullCharged(); + } + }); + } + } + + private void onBatteryTemperatureChanged(final int temp) { + if (Looper.myLooper() == Looper.getMainLooper()) { + dispatchBatteryTemperatureChanged(temp); + } else { + mUiHandler.post(new Runnable() { + @Override + public void run() { + dispatchBatteryTemperatureChanged(temp); + } + }); + } + } + @VisibleForTesting void dispatchSateChangedEvent(Intent intent) { MatrixLog.i(TAG, "onSateChanged >> " + intent.getAction()); synchronized (mListenerList) { + BatteryState batteryState = currentState(); for (Listener item : mListenerList) { - if (item.onStateChanged(intent.getAction())) { - removeListener(item); + if (item instanceof Listener.ExListener) { + if (((Listener.ExListener) item).onStateChanged(batteryState, intent.getAction())) { + removeListener(item); + } + } else { + if (item.onStateChanged(intent.getAction())) { + removeListener(item); + } } } } @@ -198,10 +398,77 @@ void dispatchSateChangedEvent(Intent intent) { @VisibleForTesting void dispatchAppLowEnergyEvent(long duringMillis) { + MatrixLog.i(TAG, "onAppLowEnergy >> " + (duringMillis / ONE_MIN) + "min"); + synchronized (mListenerList) { + BatteryState batteryState = currentState(); + for (Listener item : mListenerList) { + if (item.onAppLowEnergy(batteryState, duringMillis)) { + removeListener(item); + } + } + } + } + + @VisibleForTesting + void dispatchBatteryStateChangedEvent(Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_BATTERY_OKAY.equals(action) || Intent.ACTION_BATTERY_LOW.equals(action)) { + MatrixLog.i(TAG, "onBatteryStateChanged >> " + action); + synchronized (mListenerList) { + BatteryState batteryState = currentState(); + for (Listener item : mListenerList) { + if (item instanceof Listener.ExListener) { + if (((Listener.ExListener) item).onBatteryStateChanged(batteryState, Intent.ACTION_BATTERY_LOW.equals(action))) { + removeListener(item); + } + } + } + } + } else { + throw new IllegalStateException("Illegal battery state: " + action); + } + } + + @VisibleForTesting + void dispatchBatteryPowerChanged(int pct) { + MatrixLog.i(TAG, "onBatteryPowerChanged >> " + pct + "%"); synchronized (mListenerList) { + BatteryState batteryState = currentState(); for (Listener item : mListenerList) { - if (item.onAppLowEnergy(currentState(), duringMillis)) { - return; + if (item instanceof Listener.ExListener) { + if (((Listener.ExListener) item).onBatteryPowerChanged(batteryState, pct)) { + removeListener(item); + } + } + } + } + } + + @VisibleForTesting + void dispatchBatteryFullCharged() { + MatrixLog.i(TAG, "dispatchBatteryFullCharged"); + synchronized (mListenerList) { + BatteryState batteryState = currentState(); + for (Listener item : mListenerList) { + if (item instanceof Listener.ExListener) { + if (((Listener.ExListener) item).onBatteryFullCharged(batteryState)) { + removeListener(item); + } + } + } + } + } + + @VisibleForTesting + void dispatchBatteryTemperatureChanged(int temperature) { + MatrixLog.i(TAG, "onBatteryTemperatureChanged >> " + (temperature / 10f) + "°C"); + synchronized (mListenerList) { + BatteryState batteryState = currentState(); + for (Listener item : mListenerList) { + if (item instanceof Listener.ExListener) { + if (((Listener.ExListener) item).onBatteryTemperatureChanged(batteryState, temperature)) { + removeListener(item); + } } } } @@ -270,10 +537,29 @@ public boolean isScreenOn() { return BatteryCanaryUtil.isDeviceScreenOn(mContext); } - public boolean isPowerSaveMode() { + public boolean isSysDozeMode() { + return BatteryCanaryUtil.isDeviceOnIdleMode(mContext); + } + + public boolean isAppStandbyMode() { return BatteryCanaryUtil.isDeviceOnPowerSave(mContext); } + @SuppressWarnings("unused") + public boolean isLowBattery() { + return BatteryCanaryUtil.isLowBattery(mContext); + } + + @SuppressWarnings("unused") + public int getBatteryPercentage() { + return BatteryCanaryUtil.getBatteryPercentage(mContext); + } + + @SuppressWarnings("unused") + public int getBatteryCapacity() { + return BatteryCanaryUtil.getBatteryCapacity(mContext); + } + public long getBackgroundTimeMillis() { if (isForeground()) return 0L; if (sBackgroundBgnMillis <= 0L) return 0L; @@ -286,27 +572,36 @@ public String toString() { "fg=" + isForeground() + ", charge=" + isCharging() + ", screen=" + isScreenOn() + - ", doze=" + isPowerSaveMode() + + ", sysDoze=" + isSysDozeMode() + + ", appStandby=" + isAppStandbyMode() + ", bgMillis=" + getBackgroundTimeMillis() + '}'; } } + public interface Listener { + @RequiresApi(api = Build.VERSION_CODES.M) @StringDef(value = { Intent.ACTION_SCREEN_ON, Intent.ACTION_SCREEN_OFF, Intent.ACTION_POWER_CONNECTED, - Intent.ACTION_POWER_DISCONNECTED + Intent.ACTION_POWER_DISCONNECTED, + Intent.ACTION_BATTERY_OKAY, + Intent.ACTION_BATTERY_LOW, + PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED, + PowerManager.ACTION_POWER_SAVE_MODE_CHANGED, }) @Retention(RetentionPolicy.SOURCE) @interface BatteryEventDef { } /** + * @see ExListener#onStateChanged(BatteryState, String) * @return return true if your listening is done, thus we remove your listener */ @UiThread + @Deprecated boolean onStateChanged(@BatteryEventDef String event); /** @@ -314,5 +609,95 @@ public interface Listener { */ @UiThread boolean onAppLowEnergy(BatteryState batteryState, long backgroundMillis); + + interface ExListener extends Listener { + + @UiThread + boolean onStateChanged(BatteryState batteryState, @BatteryEventDef String event); + + /** + * On battery temperature changed. + * + * @param batteryState {@link BatteryState} + * @param temperature See {@link BatteryManager#EXTRA_TEMPERATURE}, °C * 10 + * @return return true if your listening is done, thus we remove your listener + */ + @UiThread + boolean onBatteryTemperatureChanged(BatteryState batteryState, int temperature); + + /** + * On battery power changed. + * + * @param batteryState {@link BatteryState} + * @param levelPct Battery capacity level 0 - 100 + * @return return true if your listening is done, thus we remove your listener + */ + @UiThread + boolean onBatteryPowerChanged(BatteryState batteryState, int levelPct); + + /** + * On battery power full charged. + * + * @param batteryState {@link BatteryState} + * @return return true if your listening is done, thus we remove your listener + */ + @UiThread + boolean onBatteryFullCharged(BatteryState batteryState); + + /** + * On battery power low or ok. + * + * @param batteryState {@link BatteryState} + * @param isLowBattery {@link Intent#ACTION_BATTERY_LOW}, {@link Intent#ACTION_BATTERY_OKAY} + * @return return true if your listening is done, thus we remove your listener + */ + @UiThread + boolean onBatteryStateChanged(BatteryState batteryState, boolean isLowBattery); + } + + @SuppressWarnings("unused") + class DefaultListenerImpl implements ExListener { + + final boolean mKeepAlive; + + public DefaultListenerImpl(boolean keepAlive) { + mKeepAlive = keepAlive; + } + + @Override + public boolean onStateChanged(String event) { + throw new RuntimeException("Use #onStateChanged(BatteryState, String) instead"); + } + + @Override + public boolean onAppLowEnergy(BatteryState batteryState, long backgroundMillis) { + return !mKeepAlive; + } + + @Override + public boolean onStateChanged(BatteryState batteryState, String event) { + return !mKeepAlive; + } + + @Override + public boolean onBatteryTemperatureChanged(BatteryState batteryState, int temperature) { + return !mKeepAlive; + } + + @Override + public boolean onBatteryPowerChanged(BatteryState batteryState, int levelPct) { + return !mKeepAlive; + } + + @Override + public boolean onBatteryFullCharged(BatteryState batteryState) { + return !mKeepAlive; + } + + @Override + public boolean onBatteryStateChanged(BatteryState batteryState, boolean isLowBattery) { + return !mKeepAlive; + } + } } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/BatteryMonitorPlugin.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/BatteryMonitorPlugin.java index c74c9bea8..ca4ae885b 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/BatteryMonitorPlugin.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/BatteryMonitorPlugin.java @@ -2,10 +2,10 @@ import android.app.Application; -import com.tencent.matrix.AppActiveMatrixDelegate; import com.tencent.matrix.Matrix; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner; import com.tencent.matrix.plugin.Plugin; import com.tencent.matrix.plugin.PluginListener; import com.tencent.matrix.util.MatrixLog; @@ -31,7 +31,7 @@ public BatteryMonitorCore core() { public void init(Application app, PluginListener listener) { super.init(app, listener); if (!mDelegate.getConfig().isBuiltinForegroundNotifyEnabled) { - AppActiveMatrixDelegate.INSTANCE.removeListener(this); + ProcessUILifecycleOwner.INSTANCE.removeListener(this); } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/AppStats.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/AppStats.java index 51511cf5c..0abb8c96c 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/AppStats.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/AppStats.java @@ -24,6 +24,7 @@ public class AppStats { @IntDef(value = { APP_STAT_FOREGROUND, APP_STAT_FOREGROUND_SERVICE, + APP_STAT_FLOAT_WINDOW, APP_STAT_BACKGROUND }) @Retention(RetentionPolicy.SOURCE) @@ -33,8 +34,12 @@ public class AppStats { @IntDef(value = { DEV_STAT_CHARGING, DEV_STAT_UN_CHARGING, + DEV_STAT_SCREEN_ON, DEV_STAT_SCREEN_OFF, - DEV_STAT_SAVE_POWER_MODE + DEV_STAT_SAVE_POWER_MODE_ON, + DEV_STAT_SAVE_POWER_MODE_OFF, + DEV_STAT_DOZE_MODE_ON, + DEV_STAT_DOZE_MODE_OFF, }) @Retention(RetentionPolicy.SOURCE) public @interface DevStatusDef { @@ -42,16 +47,22 @@ public class AppStats { public static final int APP_STAT_FOREGROUND = 1; public static final int APP_STAT_FOREGROUND_SERVICE = 3; + public static final int APP_STAT_FLOAT_WINDOW = 4; public static final int APP_STAT_BACKGROUND = 2; public static final int DEV_STAT_CHARGING = 1; public static final int DEV_STAT_UN_CHARGING = 2; public static final int DEV_STAT_SCREEN_OFF = 3; - public static final int DEV_STAT_SAVE_POWER_MODE = 4; + public static final int DEV_STAT_SAVE_POWER_MODE_ON = 4; + public static final int DEV_STAT_SCREEN_ON = 5; + public static final int DEV_STAT_SAVE_POWER_MODE_OFF = 6; + public static final int DEV_STAT_DOZE_MODE_ON = 7; + public static final int DEV_STAT_DOZE_MODE_OFF = 8; public int appFgRatio; public int appBgRatio; public int appFgSrvRatio; + public int appFloatRatio; public int devChargingRatio; public int devUnChargingRatio; @@ -96,16 +107,24 @@ public boolean isCharging() { @AppStatusDef public int getAppStat() { + if (mForegroundOverride != null) { + if (mForegroundOverride.get()) { + return APP_STAT_FOREGROUND; + } + } + // FIXME: return the max one might be better if (appFgRatio >= 50) return APP_STAT_FOREGROUND; if (appFgSrvRatio >= 50) return APP_STAT_FOREGROUND_SERVICE; + if (appFloatRatio >= 50) return APP_STAT_FLOAT_WINDOW; return APP_STAT_BACKGROUND; } @DevStatusDef public int getDevStat() { + // FIXME: return the max one might be better if (devChargingRatio >= 50) return DEV_STAT_CHARGING; if (devSceneOffRatio >= 50) return DEV_STAT_SCREEN_OFF; - if (devLowEnergyRatio >= 50) return DEV_STAT_SAVE_POWER_MODE; + if (devLowEnergyRatio >= 50) return DEV_STAT_SAVE_POWER_MODE_ON; return DEV_STAT_UN_CHARGING; } @@ -120,6 +139,7 @@ public String toString() { "appFgRatio=" + appFgRatio + ", appBgRatio=" + appBgRatio + ", appFgSrvRatio=" + appFgSrvRatio + + ", appFloatRatio=" + appFloatRatio + ", devChargingRatio=" + devChargingRatio + ", devUnChargingRatio=" + devUnChargingRatio + ", devSceneOffRatio=" + devSceneOffRatio + @@ -160,6 +180,7 @@ public static AppStats current(long millisFromNow) { stats.appFgRatio = appStats.fgRatio.get().intValue(); stats.appBgRatio = appStats.bgRatio.get().intValue(); stats.appFgSrvRatio = appStats.fgSrvRatio.get().intValue(); + stats.appFloatRatio = appStats.floatRatio.get().intValue(); TimeBreaker.TimePortions portions = appStatFeat.currentSceneSnapshot(duringMillis); TimeBreaker.TimePortions.Portion top1 = portions.top1(); diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorCallback.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorCallback.java index 9dacc7fda..4eb351a0d 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorCallback.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorCallback.java @@ -1,29 +1,23 @@ package com.tencent.matrix.batterycanary.monitor; import android.content.ComponentName; -import android.os.HandlerThread; -import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; -import android.util.LongSparseArray; import com.tencent.matrix.batterycanary.monitor.feature.AbsTaskMonitorFeature.TaskJiffiesSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.AlarmMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.AlarmMonitorFeature.AlarmSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.AppStatMonitorFeature; -import com.tencent.matrix.batterycanary.monitor.feature.BlueToothMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.BlueToothMonitorFeature.BlueToothSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.CompositeMonitors; import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature; import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature.CpuStateSnapshot; -import com.tencent.matrix.batterycanary.monitor.feature.DeviceStatMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.DeviceStatMonitorFeature.BatteryTmpSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.DeviceStatMonitorFeature.CpuFreqSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.InternalMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot.ThreadJiffiesEntry; -import com.tencent.matrix.batterycanary.monitor.feature.LocationMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.LocationMonitorFeature.LocationSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.LooperTaskMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; @@ -33,13 +27,11 @@ import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Sampler; import com.tencent.matrix.batterycanary.monitor.feature.NotificationMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.NotificationMonitorFeature.BadNotification; -import com.tencent.matrix.batterycanary.monitor.feature.TrafficMonitorFeature; -import com.tencent.matrix.batterycanary.monitor.feature.TrafficMonitorFeature.RadioStatSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.WakeLockMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.WakeLockMonitorFeature.WakeLockSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.WakeLockMonitorFeature.WakeLockTrace.WakeLockRecord; -import com.tencent.matrix.batterycanary.monitor.feature.WifiMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.WifiMonitorFeature.WifiSnapshot; +import com.tencent.matrix.batterycanary.stats.BatteryStatsFeature; import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.Consumer; import com.tencent.matrix.batterycanary.utils.PowerProfile; @@ -81,58 +73,11 @@ class BatteryPrinter implements BatteryMonitorCallback { protected long mTraceBgnMillis; protected boolean mIsForeground; - // TODO: Remove deprecated fields - @Deprecated - protected Printer mPrinter = new Printer(); - @Deprecated - protected final LongSparseArray> tasks = new LongSparseArray<>(); - @Deprecated - protected AlarmMonitorFeature mAlarmFeat; - @Deprecated - protected AppStatMonitorFeature mAppStatFeat; - @Deprecated - protected BlueToothMonitorFeature mBlueToothFeat; - @Deprecated - protected DeviceStatMonitorFeature mDevStatFeat; - @Deprecated - protected JiffiesMonitorFeature mJiffiesFeat; - @Deprecated - protected LocationMonitorFeature mLocationFeat; - @Deprecated - protected TrafficMonitorFeature mTrafficFeat; - @Deprecated - protected WakeLockMonitorFeature mWakeLockFeat; - @Deprecated - protected WifiMonitorFeature mWifiMonitorFeat; - @Deprecated - protected CpuStatFeature mCpuStatFeat; - - @Deprecated - protected AlarmSnapshot mLastAlarmSnapshot; - @Deprecated - protected BlueToothSnapshot mLastBlueToothSnapshot; - @Deprecated - protected BatteryTmpSnapshot mLastBatteryTmpSnapshot; - @Deprecated - protected CpuFreqSnapshot mLastCpuFreqSnapshot; - @Deprecated - protected JiffiesSnapshot mLastJiffiesSnapshot; - @Deprecated - protected LocationSnapshot mLastLocationSnapshot; - @Deprecated - protected RadioStatSnapshot mLastTrafficSnapshot; - @Deprecated - protected WakeLockSnapshot mLastWakeWakeLockSnapshot; - @Deprecated - protected WifiSnapshot mLastWifiSnapshot; - @Deprecated - protected CpuStateSnapshot mLastCpuStateSnapshot; - @SuppressWarnings("UnusedReturnValue") @VisibleForTesting public BatteryPrinter attach(BatteryMonitorCore monitorCore) { mMonitor = monitorCore; - mCompositeMonitors = new CompositeMonitors(monitorCore); + mCompositeMonitors = new CompositeMonitors(monitorCore, CompositeMonitors.SCOPE_CANARY); mCompositeMonitors.metricAll(); return this; } @@ -146,74 +91,16 @@ protected boolean isForegroundReport() { return mIsForeground; } - @Deprecated - protected AppStats getAppStats() { - if (mCompositeMonitors.getAppStats() != null) { - return mCompositeMonitors.getAppStats(); - } - return AppStats.current(); - } - @CallSuper @Override public void onTraceBegin() { mTraceBgnMillis = SystemClock.uptimeMillis(); mCompositeMonitors.clear(); mCompositeMonitors.start(); - - // TODO: Remove deprecated statements - // Configure begin snapshots - mAlarmFeat = mMonitor.getMonitorFeature(AlarmMonitorFeature.class); - if (mAlarmFeat != null) { - mLastAlarmSnapshot = mAlarmFeat.currentAlarms(); - } - - mAppStatFeat = mMonitor.getMonitorFeature(AppStatMonitorFeature.class); - - mBlueToothFeat = mMonitor.getMonitorFeature(BlueToothMonitorFeature.class); - if (mBlueToothFeat != null) { - mLastBlueToothSnapshot = mBlueToothFeat.currentSnapshot(); - } - - mDevStatFeat = mMonitor.getMonitorFeature(DeviceStatMonitorFeature.class); - if (mDevStatFeat != null) { - mLastCpuFreqSnapshot = mDevStatFeat.currentCpuFreq(); - mLastBatteryTmpSnapshot = mDevStatFeat.currentBatteryTemperature(mMonitor.getContext()); - } - - mJiffiesFeat = mMonitor.getMonitorFeature(JiffiesMonitorFeature.class); - if (mJiffiesFeat != null) { - mLastJiffiesSnapshot = mJiffiesFeat.currentJiffiesSnapshot(); - } - - mLocationFeat = mMonitor.getMonitorFeature(LocationMonitorFeature.class); - if (mLocationFeat != null) { - mLastLocationSnapshot = mLocationFeat.currentSnapshot(); - } - - mTrafficFeat = mMonitor.getMonitorFeature(TrafficMonitorFeature.class); - if (mTrafficFeat != null) { - mLastTrafficSnapshot = mTrafficFeat.currentRadioSnapshot(mMonitor.getContext()); - } - - mWakeLockFeat = mMonitor.getMonitorFeature(WakeLockMonitorFeature.class); - if (mWakeLockFeat != null) { - mLastWakeWakeLockSnapshot = mWakeLockFeat.currentWakeLocks(); - } - - mWifiMonitorFeat = mMonitor.getMonitorFeature(WifiMonitorFeature.class); - if (mWifiMonitorFeat != null) { - mLastWifiSnapshot = mWifiMonitorFeat.currentSnapshot(); - } - - mCpuStatFeat = mMonitor.getMonitorFeature(CpuStatFeature.class); - if (mCpuStatFeat != null && mCpuStatFeat.isSupported()) { - mLastCpuStateSnapshot = mCpuStatFeat.currentCpuStateSnapshot(); - } } @Override - public void onTraceEnd(boolean isForeground) { + public void onTraceEnd(final boolean isForeground) { mIsForeground = isForeground; long duringMillis = SystemClock.uptimeMillis() - mTraceBgnMillis; if (mTraceBgnMillis <= 0L || duringMillis <= 0L) { @@ -221,12 +108,18 @@ public void onTraceEnd(boolean isForeground) { return; } mCompositeMonitors.finish(); + mCompositeMonitors.getAppStats(new Consumer() { + @Override + public void accept(AppStats appStats) { + appStats.setForeground(isForeground); + } + }); onCanaryDump(mCompositeMonitors); } @Override public void onReportInternalJiffies(Delta delta) { - CompositeMonitors monitors = new CompositeMonitors(mMonitor); + CompositeMonitors monitors = new CompositeMonitors(mMonitor, CompositeMonitors.SCOPE_INTERNAL); monitors.setAppStats(AppStats.current(delta.during)); monitors.putDelta(InternalMonitorFeature.InternalSnapshot.class, delta); onCanaryReport(monitors); @@ -234,11 +127,6 @@ public void onReportInternalJiffies(Delta delta) { @Override public void onTaskTrace(Thread thread, List sortList) { - if (thread instanceof HandlerThread) { - synchronized (tasks) { - tasks.put(((HandlerThread) thread).getThreadId(), sortList); - } - } } @Override @@ -321,7 +209,7 @@ public void onWatchingThreads(ListEntry threadJiff .append("(").append(thread.getState()).append(")") .append(threadName).append("(").append(thread.getId()).append(")") .append("\n"); - String stack = BatteryCanaryUtil.stackTraceToString(elements); + String stack = mMonitor.getConfig().callStackCollector.collect(elements); // thread stacks for (StackTraceElement item : elements) { printer.append("| ").append(item).append("\n"); @@ -391,46 +279,15 @@ public void accept(JiffiesMonitorFeature feature) { } protected Dumper createDumper() { - return new Dumper() { - @Override - protected void onWritingSections(CompositeMonitors monitors, Printer printer) { - super.onWritingSections(monitors, printer); - monitors.getAppStats(new Consumer() { - @Override - public void accept(AppStats appStats) { - BatteryPrinter.this.onWritingSections(appStats); - } - }); - } - - @Override - protected boolean onWritingSectionContent(@NonNull Delta sessionDelta, CompositeMonitors monitors, Printer printer) { - if (!super.onWritingSectionContent(sessionDelta, monitors, printer)) { - AppStats appStats = monitors.getAppStats(); - if (appStats != null) { - return BatteryPrinter.this.onWritingSectionContent(sessionDelta, appStats, printer); - } - } - return false; - } - }; + return new Dumper(); } protected Printer createPrinter() { - // TODO: Remove deprecated statements - mPrinter = new Printer(); - return mPrinter; + return new Printer(); } @CallSuper protected void onCanaryDump(final CompositeMonitors monitors) { - monitors.getAppStats(new Consumer() { - @Override - public void accept(AppStats appStats) { - onCanaryDump(appStats); - } - }); - Dumper dumper = createDumper(); Printer printer = createPrinter(); printer.writeTitle(); @@ -440,227 +297,16 @@ public void accept(AppStats appStats) { checkBadThreads(monitors); onCanaryReport(monitors); - - synchronized (tasks) { - tasks.clear(); - } - } - - protected void onPreDumping(CompositeMonitors monitors) { - checkBadThreads(monitors); } - @Deprecated @CallSuper - protected void onCanaryDump(AppStats appStats) { - // mPrinter.clear(); - // - // // title - // mPrinter.writeTitle(); - // - // // sections - // onWritingJiffiesSection(appStats); - // onWritingSections(appStats); - // onWritingAppStatSection(appStats); - // - // // end - // mPrinter.writeEnding(); - // mPrinter.dump(); - // synchronized (tasks) { - // tasks.clear(); - // } - } - - // @Deprecated - // @CallSuper - // protected void onWritingJiffiesSection(final AppStats appStats) { - // mCompositeMonitors.getDelta(JiffiesSnapshot.class, new Consumer>() { - // @Override - // public void accept(final Delta delta) { - // final long minute = appStats.getMinute(); - // for (final ThreadJiffiesEntry threadJiffies : delta.dlt.threadEntries.getList()) { - // if (!threadJiffies.stat.toUpperCase().contains("R")) { - // continue; - // } - // mCompositeMonitors.getFeature(JiffiesMonitorFeature.class, new Consumer() { - // @Override - // public void accept(JiffiesMonitorFeature feature) { - // // Watching thread state when thread is: - // // 1. still running (status 'R') - // // 2. runing time > 10min - // // 3. avgJiffies > THRESHOLD - // long avgJiffies = threadJiffies.get() / minute; - // if (appStats.isForeground()) { - // if (minute > 10 && avgJiffies > getMonitor().getConfig().fgThreadWatchingLimit) { - // MatrixLog.i(TAG, "threadWatchDog fg set, name = " + delta.dlt.name - // + ", pid = " + delta.dlt.pid - // + ", tid = " + threadJiffies.tid); - // feature.watchBackThreadSate(true, delta.dlt.pid, threadJiffies.tid); - // } - // } else { - // if (minute > 10 && avgJiffies > getMonitor().getConfig().bgThreadWatchingLimit) { - // MatrixLog.i(TAG, "threadWatchDog bg set, name = " + delta.dlt.name - // + ", pid = " + delta.dlt.pid - // + ", tid = " + threadJiffies.tid); - // feature.watchBackThreadSate(false, delta.dlt.pid, threadJiffies.tid); - // } - // } - // } - // }); - // } - // onReportJiffies(delta); - // onWritingSectionContent(delta, appStats, mPrinter); - // } - // }); - // } - - // @Deprecated - // @CallSuper - // protected void onWritingAppStatSection(final AppStats appStats) { - // createSection("app_stats", new Consumer() { - // @Override - // public void accept(final Printer printer) { - // printer.createSubSection("stat_time"); - // printer.writeLine("time", appStats.getMinute() + "(min)"); - // printer.writeLine("fg", String.valueOf(appStats.appFgRatio)); - // printer.writeLine("bg", String.valueOf(appStats.appBgRatio)); - // printer.writeLine("fgSrv", String.valueOf(appStats.appFgSrvRatio)); - // printer.writeLine("devCharging", String.valueOf(appStats.devChargingRatio)); - // printer.writeLine("devScreenOff", String.valueOf(appStats.devSceneOffRatio)); - // if (!TextUtils.isEmpty(appStats.sceneTop1)) { - // printer.writeLine("sceneTop1", appStats.sceneTop1 + "/" + appStats.sceneTop1Ratio); - // } - // if (!TextUtils.isEmpty(appStats.sceneTop2)) { - // printer.writeLine("sceneTop2", appStats.sceneTop2 + "/" + appStats.sceneTop2Ratio); - // } - // mCompositeMonitors.getFeature(AppStatMonitorFeature.class, new Consumer() { - // @Override - // public void accept(AppStatMonitorFeature feature) { - // AppStatMonitorFeature.AppStatSnapshot currSnapshot = feature.currentAppStatSnapshot(); - // printer.createSubSection("run_time"); - // printer.writeLine("time", currSnapshot.uptime.get() / ONE_MIN + "(min)"); - // printer.writeLine("fg", String.valueOf(currSnapshot.fgRatio.get())); - // printer.writeLine("bg", String.valueOf(currSnapshot.bgRatio.get())); - // printer.writeLine("fgSrv", String.valueOf(currSnapshot.fgSrvRatio.get())); - // } - // }); - // } - // }); - // } - - @Deprecated - @CallSuper - protected void onWritingSections(final AppStats appStats) { - } - - // @Deprecated - // @CallSuper - // protected void onWritingSections() { - // } - - @Deprecated - @CallSuper - protected boolean onWritingSectionContent(@NonNull Delta sessionDelta, AppStats appStats, final Printer printer) { - return false; - } - - @Deprecated - protected void createSection(String sectionName, Consumer printerConsumer) { - mPrinter.createSection(sectionName); - printerConsumer.accept(mPrinter); - } - - protected void onCanaryReport(CompositeMonitors monitors) { - monitors.getDelta(AlarmSnapshot.class, new Consumer>() { - @Override - public void accept(Delta delta) { - onReportAlarm(delta); - } - }); - monitors.getDelta(BlueToothSnapshot.class, new Consumer>() { - @Override - public void accept(Delta delta) { - onReportBlueTooth(delta); - } - }); - monitors.getDelta(CpuFreqSnapshot.class, new Consumer>() { - @Override - public void accept(Delta delta) { - onReportCpuFreq(delta); - } - }); - monitors.getDelta(CpuStateSnapshot.class, new Consumer>() { - @Override - public void accept(Delta delta) { - onReportCpuStats(delta); - } - }); - monitors.getDelta(JiffiesSnapshot.class, new Consumer>() { + protected void onCanaryReport(final CompositeMonitors monitors) { + monitors.getFeature(BatteryStatsFeature.class, new Consumer() { @Override - public void accept(Delta delta) { - onReportJiffies(delta); + public void accept(BatteryStatsFeature batteryStatsFeature) { + batteryStatsFeature.statsMonitors(monitors); } }); - monitors.getDelta(BatteryTmpSnapshot.class, new Consumer>() { - @Override - public void accept(Delta delta) { - onReportTemperature(delta); - } - }); - monitors.getDelta(WakeLockSnapshot.class, new Consumer>() { - @Override - public void accept(Delta delta) { - onReportWakeLock(delta); - } - }); - monitors.getDelta(WifiSnapshot.class, new Consumer>() { - @Override - public void accept(Delta delta) { - onReportWifi(delta); - } - }); - monitors.getDelta(LocationSnapshot.class, new Consumer>() { - @Override - public void accept(Delta delta) { - onReportLocation(delta); - } - }); - } - - @Deprecated - protected void onReportAlarm(@NonNull Delta delta) { - } - - @Deprecated - protected void onReportBlueTooth(@NonNull Delta delta) { - } - - @Deprecated - protected void onReportCpuFreq(@NonNull Delta delta) { - } - - @Deprecated - protected void onReportCpuStats(@NonNull Delta delta) { - } - - @Deprecated - protected void onReportJiffies(@NonNull Delta delta) { - } - - @Deprecated - protected void onReportTemperature(@NonNull Delta delta) { - } - - @Deprecated - protected void onReportWakeLock(@NonNull Delta delta) { - } - - @Deprecated - protected void onReportWifi(@NonNull Delta delta) { - } - - @Deprecated - protected void onReportLocation(@NonNull Delta delta) { } @@ -761,7 +407,7 @@ public void accept(Delta delta) { } } - protected boolean onWritingSectionContent(@NonNull Delta sessionDelta, CompositeMonitors monitors, final Printer printer) { + protected boolean onWritingSectionContent(@NonNull Delta sessionDelta, final CompositeMonitors monitors, final Printer printer) { if (monitors.getMonitor() == null || monitors.getAppStats() == null) { return false; } @@ -772,8 +418,8 @@ protected boolean onWritingSectionContent(@NonNull Delta sessionDelta, Compos Delta delta = (Delta) sessionDelta; // header long minute = Math.max(1, delta.during / ONE_MIN); - long avgJiffies = delta.dlt.totalJiffies.get() / minute; - printer.append("| ").append("pid=").append(Process.myPid()) + long avgJiffies = monitors.computeAvgJiffies(delta.dlt.totalJiffies.get()); + printer.append("| ").append("cpu=").append(monitors.getCpuLoad()).append("/").append(monitors.getNorCpuLoad()) .tab().tab().append("fg=").append(BatteryCanaryUtil.convertAppStat(appStats.getAppStat())) .tab().tab().append("during(min)=").append(minute) .tab().tab().append("diff(jiffies)=").append(delta.dlt.totalJiffies.get()) @@ -785,22 +431,31 @@ protected boolean onWritingSectionContent(@NonNull Delta sessionDelta, Compos printer.writeLine("desc", "(status)name(tid)\tavg/total"); printer.writeLine("inc_thread_num", String.valueOf(delta.dlt.threadNum.get())); printer.writeLine("cur_thread_num", String.valueOf(delta.end.threadNum.get())); - for (ThreadJiffiesEntry threadJiffies : delta.dlt.threadEntries.getList().subList(0, Math.min(delta.dlt.threadEntries.getList().size(), 8))) { + int toppingCount = 8; + long remainJffies = 0; + for (int i = 0; i < delta.dlt.threadEntries.getList().size(); i++) { + ThreadJiffiesEntry threadJiffies = delta.dlt.threadEntries.getList().get(i); long entryJffies = threadJiffies.get(); - printer.append("| -> (").append(threadJiffies.isNewAdded ? "+" : "~").append("/").append(threadJiffies.stat).append(")") - .append(threadJiffies.name).append("(").append(threadJiffies.tid).append(")\t") - .append(entryJffies / minute).append("/").append(entryJffies).append("\tjiffies") - .append("\n"); - - // List threadTasks = tasks.get(threadJiffies.tid); - // if (null != threadTasks && !threadTasks.isEmpty()) { - // for (LooperTaskMonitorFeature.TaskTraceInfo task : threadTasks.subList(0, Math.min(3, threadTasks.size()))) { - // printer.append("|\t\t").append(task).append("\n"); - // } - // } + if (i < toppingCount) { + printer.append("| -> (").append(threadJiffies.isNewAdded ? "+" : "~").append("/").append(threadJiffies.stat).append(")") + .append(threadJiffies.name).append("(").append(threadJiffies.tid).append(")\t") + .append(monitors.computeAvgJiffies(entryJffies)).append("/").append(entryJffies).append("\tjiffies") + .append("\n"); + } else { + remainJffies += entryJffies; + } } printer.append("|\t\t......\n"); + if (remainJffies > 0) { + printer.append("| -> R/R)") + .append("REMAINS").append("(").append(delta.dlt.threadEntries.getList().size() - toppingCount).append(")\t") + .append(monitors.computeAvgJiffies(remainJffies) / minute).append("/").append(remainJffies).append("\tjiffies") + .append("\n"); + } if (avgJiffies > 1000L || !delta.isValid()) { + // Proc CPU Load = avgJiffies / 6000L + // 1000L -> 16.6% + // 1200L -> 20.0% printer.append("| ").append(avgJiffies > 1000L ? " #overHeat" : "").append(!delta.isValid() ? " #invalid" : "").append("\n"); } return true; @@ -852,7 +507,7 @@ protected boolean onWritingSectionContent(@NonNull Delta sessionDelta, Compos return true; } - // - Dump BlueTooth + // - Dump Wifi if (sessionDelta.dlt instanceof WifiSnapshot) { //noinspection unchecked Delta delta = (Delta) sessionDelta; @@ -863,7 +518,7 @@ protected boolean onWritingSectionContent(@NonNull Delta sessionDelta, Compos return true; } - // - Dump BlueTooth + // - Dump GPS if (sessionDelta.dlt instanceof LocationSnapshot) { //noinspection unchecked Delta delta = (Delta) sessionDelta; @@ -899,45 +554,33 @@ public void accept(Sampler.Result result) { if (sessionDelta.dlt instanceof CpuStateSnapshot) { //noinspection unchecked final Delta delta = (Delta) sessionDelta; + final long minute = Math.max(1, delta.during / ONE_MIN); // Cpu Usage - printer.createSubSection("cpu_load"); + printer.createSubSection("dev_cpu_load"); printer.writeLine(delta.during + "(mls)\t" + (delta.during / ONE_MIN) + "(min)"); final CpuStatFeature cpuStatFeature = monitors.getFeature(CpuStatFeature.class); if (cpuStatFeature != null) { - monitors.getDelta(JiffiesSnapshot.class, new Consumer>() { - @Override - public void accept(Delta jiffiesDelta) { - long appJiffiesDelta = jiffiesDelta.dlt.totalJiffies.get(); - long cpuJiffiesDelta = delta.dlt.totalCpuJiffies(); - float cpuLoad = (float) appJiffiesDelta / cpuJiffiesDelta; - float cpuLoadAvg = cpuLoad * cpuStatFeature.getPowerProfile().getCpuCoreNum(); - printer.writeLine("usage", (int) (cpuLoadAvg * 100) + "%"); - } - }); + printer.writeLine("usage", monitors.getDevCpuLoad() + "%"); } for (int i = 0; i < delta.dlt.cpuCoreStates.size(); i++) { ListEntry> listEntry = delta.dlt.cpuCoreStates.get(i); printer.writeLine("cpu" + i, Arrays.toString(listEntry.getList().toArray())); } - // BatterySip - if (cpuStatFeature != null) { + // Cpu BatterySip + if (cpuStatFeature != null && cpuStatFeature.isSupported()) { printer.createSubSection("cpu_sip"); // Cpu battery sip - CPU State final PowerProfile powerProfile = cpuStatFeature.getPowerProfile(); - printer.writeLine("inc_cpu_sip", String.format(Locale.US, "%.2f(mAh)", delta.dlt.configureCpuSip(powerProfile))); + printer.writeLine("inc_cpu_sip", String.format(Locale.US, "%.2f(mAh)/min", delta.dlt.configureCpuSip(powerProfile) / minute)); printer.writeLine("cur_cpu_sip", String.format(Locale.US, "%.2f(mAh)", delta.end.configureCpuSip(powerProfile))); // Cpu battery sip - Proc State monitors.getDelta(JiffiesSnapshot.class, new Consumer>() { @Override public void accept(Delta jiffiesDelta) { - double procSipDelta = delta.dlt.configureProcSip(powerProfile, jiffiesDelta.dlt.totalJiffies.get()); + double procSipBgn = delta.bgn.configureProcSip(powerProfile, jiffiesDelta.bgn.totalJiffies.get()); double procSipEnd = delta.end.configureProcSip(powerProfile, jiffiesDelta.end.totalJiffies.get()); - printer.writeLine("inc_prc_sip", String.format(Locale.US, "%.2f(mAh)", procSipDelta)); + printer.writeLine("inc_prc_sip", String.format(Locale.US, "%.2f(mAh)/min", (procSipEnd - procSipBgn) / minute)); printer.writeLine("cur_prc_sip", String.format(Locale.US, "%.2f(mAh)", procSipEnd)); - if (Double.isNaN(procSipDelta)) { - double procSipBgn = delta.bgn.configureProcSip(powerProfile, jiffiesDelta.bgn.totalJiffies.get()); - printer.writeLine("inc_prc_sipr", String.format(Locale.US, "%.2f(mAh)", procSipEnd - procSipBgn)); - } } }); } @@ -981,6 +624,7 @@ protected void onWritingAppStatSection(CompositeMonitors monitors, final Printer printer.writeLine("fg", String.valueOf(appStats.appFgRatio)); printer.writeLine("bg", String.valueOf(appStats.appBgRatio)); printer.writeLine("fgSrv", String.valueOf(appStats.appFgSrvRatio)); + printer.writeLine("float", String.valueOf(appStats.appFloatRatio)); printer.writeLine("devCharging", String.valueOf(appStats.devChargingRatio)); printer.writeLine("devScreenOff", String.valueOf(appStats.devSceneOffRatio)); if (!TextUtils.isEmpty(appStats.sceneTop1)) { @@ -998,6 +642,7 @@ public void accept(AppStatMonitorFeature feature) { printer.writeLine("fg", String.valueOf(currSnapshot.fgRatio.get())); printer.writeLine("bg", String.valueOf(currSnapshot.bgRatio.get())); printer.writeLine("fgSrv", String.valueOf(currSnapshot.fgSrvRatio.get())); + printer.writeLine("float", String.valueOf(currSnapshot.floatRatio.get())); } }); } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorConfig.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorConfig.java index bf47b93e0..46fecb191 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorConfig.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorConfig.java @@ -1,11 +1,16 @@ package com.tencent.matrix.batterycanary.monitor; import android.app.ActivityManager; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import android.os.HandlerThread; import com.tencent.matrix.batterycanary.BuildConfig; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature.UidCpuStateSnapshot.IpcCpuStat.RemoteStat; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.UidJiffiesSnapshot.IpcJiffies.IpcProcessJiffies; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature; +import com.tencent.matrix.batterycanary.stats.BatteryRecorder; +import com.tencent.matrix.batterycanary.stats.BatteryStats; +import com.tencent.matrix.batterycanary.utils.CallStackCollector; +import com.tencent.matrix.batterycanary.utils.Function; import java.util.ArrayList; import java.util.Collections; @@ -13,6 +18,10 @@ import java.util.List; import java.util.concurrent.Callable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; + /** * @author Kaede * @since 2020/10/27 @@ -28,6 +37,8 @@ public class BatteryMonitorConfig { public static final int AMS_HOOK_FLAG_BT = 0b00000001; + @Nullable + public HandlerThread canaryThread = null; @NonNull public BatteryMonitorCallback callback = new BatteryMonitorCallback.BatteryPrinter(); @Nullable @@ -49,6 +60,7 @@ public class BatteryMonitorConfig { public boolean isStatPidProc = BuildConfig.DEBUG; public boolean isInspectiffiesError = BuildConfig.DEBUG; public boolean isAmsHookEnabled = BuildConfig.DEBUG; + public boolean isSkipNewAddedPidTid = false; public int amsHookEnableFlag = 0; public boolean isAggressiveMode = BuildConfig.DEBUG; public boolean isUseThreadClock = BuildConfig.DEBUG; @@ -57,6 +69,12 @@ public class BatteryMonitorConfig { public List looperWatchList = Collections.emptyList(); public List threadWatchList = Collections.emptyList(); public final List features = new ArrayList<>(3); + public BatteryRecorder batteryRecorder; + public BatteryStats batteryStats; + public CallStackCollector callStackCollector; + public Function, IpcProcessJiffies> ipcJiffiesCollector; + public Function, RemoteStat> ipcCpuStatCollector; + public boolean isTuningPowers = BuildConfig.DEBUG; private BatteryMonitorConfig() { } @@ -65,29 +83,7 @@ private BatteryMonitorConfig() { @Override public String toString() { return "BatteryMonitorConfig{" - + "wakelockTimeout=" + wakelockTimeout - + ", wakelockWarnCount=" + wakelockWarnCount - + ", greyTime=" + greyTime - + ", foregroundLoopCheckTime=" + foregroundLoopCheckTime - + ", backgroundLoopCheckTime=" + backgroundLoopCheckTime - + ", overHeatCount=" + overHeatCount - + ", foregroundServiceLeakLimit=" + foregroundServiceLeakLimit - + ", fgThreadWatchingLimit=" + fgThreadWatchingLimit - + ", bgThreadWatchingLimit=" + bgThreadWatchingLimit - + ", isForegroundModeEnabled=" + isForegroundModeEnabled - + ", isBackgroundModeEnabled=" + isBackgroundModeEnabled - + ", isBuiltinForegroundNotifyEnabled=" + isBuiltinForegroundNotifyEnabled - + ", isStatAsSample=" + isStatAsSample - + ", isStatPidProc=" + isStatPidProc - + ", isInspectiffiesError=" + isInspectiffiesError - + ", isAmsHookEnabled=" + isAmsHookEnabled - + ", isAggressiveMode=" + isAggressiveMode - + ", isUseThreadClock=" + isUseThreadClock - + ", tagWhiteList=" + tagWhiteList - + ", tagBlackList=" + tagBlackList - + ", looperWatchList=" + looperWatchList - + ", threadWatchList=" + threadWatchList - + ", features=" + features + + "features=" + features + '}'; } @@ -97,6 +93,11 @@ public String toString() { public static class Builder { private final BatteryMonitorConfig config = new BatteryMonitorConfig(); + public Builder setCanaryThread(HandlerThread thread) { + config.canaryThread = thread; + return this; + } + public Builder setCallback(BatteryMonitorCallback callback) { config.callback = callback; return this; @@ -260,6 +261,36 @@ public Builder setOverHeatCount(int count) { return this; } + public Builder setRecorder(BatteryRecorder recorder) { + config.batteryRecorder = recorder; + return this; + } + + public Builder setStats(BatteryStats stats) { + config.batteryStats = stats; + return this; + } + + public Builder setCollector(CallStackCollector collector) { + config.callStackCollector = collector; + return this; + } + + public Builder setCollector(Function, IpcProcessJiffies> collector) { + config.ipcJiffiesCollector = collector; + return this; + } + + public Builder enableTuningPowers(boolean enable) { + config.isTuningPowers = enable; + return this; + } + + public Builder skipNewAddedPidTid(boolean skip) { + config.isSkipNewAddedPidTid = skip; + return this; + } + public BatteryMonitorConfig build() { Collections.sort(config.features, new Comparator() { @Override @@ -267,6 +298,13 @@ public int compare(MonitorFeature o1, MonitorFeature o2) { return Integer.compare(o2.weight(), o1.weight()); } }); + + if (config.batteryStats == null) { + config.batteryStats = new BatteryStats.BatteryStatsImpl(); + } + if (config.callStackCollector == null) { + config.callStackCollector = new CallStackCollector(); + } return config; } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorCore.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorCore.java index f357a9099..9e6506b37 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorCore.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/BatteryMonitorCore.java @@ -4,15 +4,14 @@ import android.content.ComponentName; import android.content.Context; import android.os.Handler; +import android.os.HandlerThread; import android.os.Message; -import com.tencent.matrix.AppActiveMatrixDelegate; import com.tencent.matrix.Matrix; import com.tencent.matrix.batterycanary.BatteryEventDelegate; import com.tencent.matrix.batterycanary.monitor.feature.AbsTaskMonitorFeature.TaskJiffiesSnapshot; import com.tencent.matrix.batterycanary.monitor.feature.AlarmMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.AppStatMonitorFeature; -import com.tencent.matrix.batterycanary.monitor.feature.InternalMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot.ThreadJiffiesEntry; import com.tencent.matrix.batterycanary.monitor.feature.LooperTaskMonitorFeature; @@ -24,6 +23,7 @@ import com.tencent.matrix.batterycanary.monitor.feature.WakeLockMonitorFeature; import com.tencent.matrix.batterycanary.monitor.feature.WakeLockMonitorFeature.WakeLockTrace.WakeLockRecord; import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner; import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; @@ -33,7 +33,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; public class BatteryMonitorCore implements LooperTaskMonitorFeature.LooperTaskListener, @@ -95,9 +94,9 @@ public void run() { private final BatteryMonitorConfig mConfig; @NonNull private final Handler mHandler; + @NonNull private final Handler mCanaryHandler; @Nullable private ForegroundLoopCheckTask mFgLooperTask; @Nullable private BackgroundLoopCheckTask mBgLooperTask; - @Nullable private TaskJiffiesSnapshot mLastInternalSnapshot; @NonNull Callable mSupplier = new Callable() { @@ -108,7 +107,7 @@ public String call() { }; private volatile boolean mTurnOn = false; - private boolean mAppForeground = AppActiveMatrixDelegate.INSTANCE.isAppForeground(); + private boolean mAppForeground = ProcessUILifecycleOwner.INSTANCE.isProcessForeground(); private boolean mForegroundModeEnabled; private boolean mBackgroundModeEnabled; private final long mMonitorDelayMillis; @@ -123,7 +122,16 @@ public BatteryMonitorCore(BatteryMonitorConfig config) { mSupplier = config.onSceneSupplier; } - mHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper(), this); + if (config.canaryThread != null) { + HandlerThread thread = config.canaryThread; + mHandler = new Handler(thread.getLooper(), this); // For BatteryMonitorCore only + mCanaryHandler = new Handler(thread.getLooper(), this); // For BatteryCanary + } else { + HandlerThread thread = MatrixHandlerThread.getDefaultHandlerThread(); + mHandler = new Handler(thread.getLooper(), this); // For BatteryMonitorCore only + mCanaryHandler = mHandler; // For BatteryCanary as legacy logic + } + enableForegroundLoopCheck(config.isForegroundModeEnabled); enableBackgroundLoopCheck(config.isBackgroundModeEnabled); mMonitorDelayMillis = config.greyTime; @@ -208,16 +216,6 @@ public void stop() { } } - /** - * Removed to {@link InternalMonitorFeature#configureMonitorConsuming()} - */ - @WorkerThread - @Nullable - @Deprecated - public TaskJiffiesSnapshot configureMonitorConsuming() { - return null; - } - public void onForeground(boolean isForeground) { if (!Matrix.isInstalled()) { MatrixLog.e(TAG, "Matrix was not installed yet, just ignore the event"); @@ -277,7 +275,7 @@ public void onForeground(boolean isForeground) { @NonNull public Handler getHandler() { - return mHandler; + return mCanaryHandler; } public Context getContext() { @@ -304,7 +302,7 @@ public int getCurrentBatteryTemperature(Context context) { return tmp; } catch (Throwable e) { MatrixLog.printErrStackTrace(TAG, e, "#currentBatteryTemperature error"); - return 0; + return -1; } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/AbsTaskMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/AbsTaskMonitorFeature.java index 5536cada5..91bb65579 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/AbsTaskMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/AbsTaskMonitorFeature.java @@ -104,7 +104,7 @@ public void onTurnOff() { } } - public List> currentJiffies() { + public List> currentJiffies(long windowMsFromNow) { // Report unfinished task: // Maybe cause duplicated reporting // for (TaskJiffiesSnapshot bgn : mTaskJiffiesTrace.values()) { @@ -116,7 +116,17 @@ public List> currentJiffies() { ArrayList> list; synchronized (mDeltaList) { - list = new ArrayList<>(mDeltaList); + if (windowMsFromNow <= 0) { + list = new ArrayList<>(mDeltaList); + } else { + list = new ArrayList<>(); + long bgnMillis = SystemClock.uptimeMillis() - windowMsFromNow; + for (Delta item : mDeltaList) { + if (item.bgn.time >= bgnMillis) { + list.add(item); + } + } + } } // Sorting by jiffies dec @@ -408,7 +418,7 @@ protected void onCoolingDown() { // task jiffies list overheat if (mDeltaList.size() > mOverHeatCount) { MatrixLog.w(TAG, "cooling task jiffies list, before = " + mDeltaList.size()); - List> deltas = currentJiffies(); + List> deltas = currentJiffies(0); clearFinishedJiffies(); MatrixLog.w(TAG, "cooling task jiffies list, after = " + mDeltaList.size()); diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/AppStatMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/AppStatMonitorFeature.java index b67f2b57d..93012fa30 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/AppStatMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/AppStatMonitorFeature.java @@ -4,33 +4,43 @@ import android.content.ComponentName; import android.content.Context; import android.os.Looper; -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; import android.text.TextUtils; import com.tencent.matrix.batterycanary.BatteryEventDelegate; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.stats.BatteryRecord; +import com.tencent.matrix.batterycanary.stats.BatteryStatsFeature; import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.TimeBreaker; +import com.tencent.matrix.lifecycle.IStateObserver; +import com.tencent.matrix.lifecycle.owners.ForegroundServiceLifecycleOwner; +import com.tencent.matrix.lifecycle.owners.OverlayWindowLifecycleOwner; import com.tencent.matrix.util.MatrixLog; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import static com.tencent.matrix.batterycanary.monitor.AppStats.APP_STAT_BACKGROUND; +import static com.tencent.matrix.batterycanary.monitor.AppStats.APP_STAT_FLOAT_WINDOW; +import static com.tencent.matrix.batterycanary.monitor.AppStats.APP_STAT_FOREGROUND; +import static com.tencent.matrix.batterycanary.monitor.AppStats.APP_STAT_FOREGROUND_SERVICE; + /** * @author Kaede * @since 2020/12/8 */ public final class AppStatMonitorFeature extends AbsMonitorFeature { - private static final String TAG = "Matrix.battery.AppStatMonitorFeature"; public interface AppStatListener { void onForegroundServiceLeak(boolean isMyself, int appImportance, int globalAppImportance, ComponentName componentName, long millis); - void onAppSateLeak(boolean isMyself, int appImportance, ComponentName componentName, long millis); } + private static final String TAG = "Matrix.battery.AppStatMonitorFeature"; /** * Less important than {@link ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE} */ @@ -62,6 +72,62 @@ public void run() { } }; + private final IStateObserver mFgSrvObserver = new IStateObserver() { + @Override + public void on() { + MatrixLog.i(TAG, "fgSrv >> on"); + boolean foreground = mCore.isForeground(); + int appStat = BatteryCanaryUtil.getAppStatImmediately(mCore.getContext(), foreground); + if (appStat != APP_STAT_FOREGROUND) { + MatrixLog.i(TAG, "statAppStat: " + APP_STAT_FOREGROUND_SERVICE); + onStatAppStat(APP_STAT_FOREGROUND_SERVICE); + } else { + MatrixLog.i(TAG, "skip statAppStat, fg = " + foreground + ", currAppStat = " + appStat); + } + } + + @Override + public void off() { + MatrixLog.i(TAG, "fgSrv >> off"); + boolean foreground = mCore.isForeground(); + int appStat = BatteryCanaryUtil.getAppStatImmediately(mCore.getContext(), foreground); + if (appStat != APP_STAT_FOREGROUND && appStat != APP_STAT_FOREGROUND_SERVICE && appStat != APP_STAT_FLOAT_WINDOW) { + MatrixLog.i(TAG, "statAppStat: " + APP_STAT_BACKGROUND); + onStatAppStat(APP_STAT_BACKGROUND); + } else { + MatrixLog.i(TAG, "skip statAppStat, fg = " + foreground + ", currAppStat = " + appStat); + } + } + }; + + private final IStateObserver mFloatViewObserver = new IStateObserver() { + @Override + public void on() { + MatrixLog.i(TAG, "floatView >> on"); + boolean foreground = mCore.isForeground(); + int appStat = BatteryCanaryUtil.getAppStatImmediately(mCore.getContext(), foreground); + if (appStat != APP_STAT_FOREGROUND && appStat != APP_STAT_FOREGROUND_SERVICE) { + MatrixLog.i(TAG, "statAppStat: " + APP_STAT_FLOAT_WINDOW); + onStatAppStat(APP_STAT_FLOAT_WINDOW); + } else { + MatrixLog.i(TAG, "skip statAppStat, fg = " + foreground + ", currAppStat = " + appStat); + } + } + + @Override + public void off() { + MatrixLog.i(TAG, "floatView >> off"); + boolean foreground = mCore.isForeground(); + int appStat = BatteryCanaryUtil.getAppStatImmediately(mCore.getContext(), foreground); + if (appStat != APP_STAT_FOREGROUND && appStat != APP_STAT_FOREGROUND_SERVICE && appStat != APP_STAT_FLOAT_WINDOW) { + MatrixLog.i(TAG, "statAppStat: " + APP_STAT_BACKGROUND); + onStatAppStat(APP_STAT_BACKGROUND); + } else { + MatrixLog.i(TAG, "skip statAppStat, fg = " + foreground + ", currAppStat = " + appStat); + } + } + }; + @Override protected String getTag() { return TAG; @@ -76,7 +142,7 @@ public void configure(BatteryMonitorCore monitor) { @Override public void onTurnOn() { super.onTurnOn(); - TimeBreaker.Stamp firstStamp = new TimeBreaker.Stamp("1"); + TimeBreaker.Stamp firstStamp = new TimeBreaker.Stamp(String.valueOf(APP_STAT_FOREGROUND)); TimeBreaker.Stamp firstSceneStamp = new TimeBreaker.Stamp(mCore.getScene()); synchronized (TAG) { mStampList = new ArrayList<>(); @@ -84,11 +150,16 @@ public void onTurnOn() { mSceneStampList = new ArrayList<>(); mSceneStampList.add(0, firstSceneStamp); } + + ForegroundServiceLifecycleOwner.INSTANCE.observeForever(mFgSrvObserver); + OverlayWindowLifecycleOwner.INSTANCE.observeForever(mFloatViewObserver); } @Override public void onTurnOff() { super.onTurnOff(); + ForegroundServiceLifecycleOwner.INSTANCE.removeObserver(mFgSrvObserver); + OverlayWindowLifecycleOwner.INSTANCE.removeObserver(mFloatViewObserver); synchronized (TAG) { mStampList.clear(); mSceneStampList.clear(); @@ -100,13 +171,7 @@ public void onForeground(boolean isForeground) { super.onForeground(isForeground); int appStat = BatteryCanaryUtil.getAppStatImmediately(mCore.getContext(), isForeground); BatteryCanaryUtil.getProxy().updateAppStat(appStat); - synchronized (TAG) { - if (mStampList != Collections.EMPTY_LIST) { - MatrixLog.i(BatteryEventDelegate.TAG, "onStat >> " + BatteryCanaryUtil.convertAppStat(appStat)); - mStampList.add(0, new TimeBreaker.Stamp(String.valueOf(appStat))); - checkOverHeat(); - } - } + onStatAppStat(appStat); MatrixLog.i(TAG, "updateAppImportance when app " + (isForeground ? "foreground" : "background")); updateAppImportance(); @@ -163,7 +228,25 @@ public void onBackgroundCheck(long duringMillis) { // checkBackgroundAppState(duringMillis); } + public void onStatAppStat(int appStat) { + synchronized (TAG) { + if (mStampList != Collections.EMPTY_LIST) { + MatrixLog.i(BatteryEventDelegate.TAG, "onStat >> " + BatteryCanaryUtil.convertAppStat(appStat)); + mStampList.add(0, new TimeBreaker.Stamp(String.valueOf(appStat))); + checkOverHeat(); + } + } + } + + @SuppressWarnings("unused") public void onStatScene(@NonNull String scene) { + BatteryStatsFeature statsFeature = mCore.getMonitorFeature(BatteryStatsFeature.class); + if (statsFeature != null) { + BatteryRecord.SceneStatRecord statRecord = new BatteryRecord.SceneStatRecord(); + statRecord.scene = scene; + statsFeature.writeRecord(statRecord); + } + synchronized (TAG) { if (mSceneStampList != Collections.EMPTY_LIST) { mSceneStampList.add(0, new TimeBreaker.Stamp(scene)); @@ -294,9 +377,10 @@ public TimeBreaker.Stamp stamp(String key) { AppStatSnapshot snapshot = new AppStatSnapshot(); snapshot.setValid(timePortions.isValid()); snapshot.uptime = Snapshot.Entry.DigitEntry.of(timePortions.totalUptime); - snapshot.fgRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio("1")); - snapshot.bgRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio("2")); - snapshot.fgSrvRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio("3")); + snapshot.fgRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio(String.valueOf(APP_STAT_FOREGROUND))); + snapshot.bgRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio(String.valueOf(APP_STAT_BACKGROUND))); + snapshot.fgSrvRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio(String.valueOf(APP_STAT_FOREGROUND_SERVICE))); + snapshot.floatRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio(String.valueOf(APP_STAT_FLOAT_WINDOW))); return snapshot; } catch (Throwable e) { @@ -350,6 +434,7 @@ public static final class AppStatSnapshot extends Snapshot { public Entry.DigitEntry fgRatio = Entry.DigitEntry.of(0L); public Entry.DigitEntry bgRatio = Entry.DigitEntry.of(0L); public Entry.DigitEntry fgSrvRatio = Entry.DigitEntry.of(0L); + public Entry.DigitEntry floatRatio = Entry.DigitEntry.of(0L); AppStatSnapshot() { } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/BlueToothMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/BlueToothMonitorFeature.java index aa64463a1..42fb05d89 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/BlueToothMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/BlueToothMonitorFeature.java @@ -4,13 +4,12 @@ import android.os.Build; import android.text.TextUtils; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.BluetoothManagerServiceHooker; import com.tencent.matrix.util.MatrixLog; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import static com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig.AMS_HOOK_FLAG_BT; public final class BlueToothMonitorFeature extends AbsMonitorFeature { @@ -36,7 +35,7 @@ public void onTurnOn() { mListener = new BluetoothManagerServiceHooker.IListener() { @Override public void onRegisterScanner() { - String stack = shouldTracing() ? BatteryCanaryUtil.stackTraceToString(new Throwable().getStackTrace()) : ""; + String stack = shouldTracing() ? mCore.getConfig().callStackCollector.collectCurr() : ""; MatrixLog.i(TAG, "#onRegisterScanner, stack = " + stack); mTracing.setStack(stack); mTracing.onRegisterScanner(); @@ -44,7 +43,7 @@ public void onRegisterScanner() { @Override public void onStartDiscovery() { - String stack = shouldTracing() ? BatteryCanaryUtil.stackTraceToString(new Throwable().getStackTrace()) : ""; + String stack = shouldTracing() ? mCore.getConfig().callStackCollector.collectCurr() : ""; MatrixLog.i(TAG, "#onStartDiscovery, stack = " + stack); mTracing.setStack(stack); mTracing.onStartDiscovery(); diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/CompositeMonitors.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/CompositeMonitors.java index 9d282a6f0..6b31f12d6 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/CompositeMonitors.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/CompositeMonitors.java @@ -1,26 +1,42 @@ package com.tencent.matrix.batterycanary.monitor.feature; +import android.annotation.SuppressLint; +import android.os.Build; +import android.os.Bundle; import android.os.SystemClock; +import android.text.TextUtils; import com.tencent.matrix.batterycanary.monitor.AppStats; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.monitor.feature.AbsTaskMonitorFeature.TaskJiffiesSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot.ThreadJiffiesEntry; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.DigitEntry; +import com.tencent.matrix.batterycanary.stats.HealthStatsFeature; +import com.tencent.matrix.batterycanary.stats.HealthStatsFeature.HealthStatsSnapshot; +import com.tencent.matrix.batterycanary.stats.HealthStatsHelper; import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.Consumer; +import com.tencent.matrix.batterycanary.utils.Function; +import com.tencent.matrix.batterycanary.utils.PowerProfile; +import com.tencent.matrix.batterycanary.utils.RadioStatUtil; import com.tencent.matrix.util.MatrixLog; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Callable; import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; /** * @author Kaede @@ -29,6 +45,13 @@ public class CompositeMonitors { private static final String TAG = "Matrix.battery.CompositeMonitors"; + public static final String SCOPE_UNKNOWN = "unknown"; + public static final String SCOPE_CANARY = "canary"; + public static final String SCOPE_INTERNAL = "internal"; + public static final String SCOPE_OVERHEAT = "overheat"; + public static final String SCOPE_TOP_SHELL = "topShell"; + public static final String SCOPE_TOP_INDICATOR = "topIndicator"; + // Differing protected final List>> mMetrics = new ArrayList<>(); protected final Map>, Snapshot> mBgnSnapshots = new HashMap<>(); @@ -39,25 +62,64 @@ public class CompositeMonitors { protected final Map>, Snapshot.Sampler> mSamplers = new HashMap<>(); protected final Map>, Snapshot.Sampler.Result> mSampleResults = new HashMap<>(); + // Task Tracing + protected final Map, List>> mTaskDeltas = new HashMap<>(); + protected final Map, Delta>>> mTaskDeltasCollect = new HashMap<>(); + + // Extra Info + protected final Bundle mExtras = new Bundle(); + + // Call Stacks + protected final Map mStacks = new HashMap<>(); + @Nullable protected BatteryMonitorCore mMonitor; @Nullable protected AppStats mAppStats; + @Nullable + protected CpuFreqSampler mCpuFreqSampler; + @Nullable + protected BpsSampler mBpsSampler; + protected long mBgnMillis = SystemClock.uptimeMillis(); + protected String mScope; public CompositeMonitors(@Nullable BatteryMonitorCore core) { mMonitor = core; + mScope = SCOPE_UNKNOWN; + } + + public CompositeMonitors(@Nullable BatteryMonitorCore core, String scope) { + mMonitor = core; + mScope = scope; } + public String getScope() { + return mScope; + } + + @CallSuper public void clear() { + MatrixLog.i(TAG, hashCode() + " #clear: " + mScope); mBgnSnapshots.clear(); mDeltas.clear(); mSamplers.clear(); mSampleResults.clear(); + mTaskDeltas.clear(); + mTaskDeltasCollect.clear(); + mExtras.clear(); + mStacks.clear(); + mCpuFreqSampler = null; } public CompositeMonitors fork() { - CompositeMonitors that = new CompositeMonitors(mMonitor); + return fork(new CompositeMonitors(mMonitor, mScope)); + } + + @CallSuper + protected CompositeMonitors fork(CompositeMonitors that) { + MatrixLog.i(TAG, hashCode() + " #fork: " + mScope); + that.clear(); that.mBgnMillis = this.mBgnMillis; that.mAppStats = this.mAppStats; @@ -65,9 +127,16 @@ public CompositeMonitors fork() { that.mBgnSnapshots.putAll(mBgnSnapshots); that.mDeltas.putAll(mDeltas); - that.mSampleRegs.putAll(mSampleRegs); - that.mSamplers.putAll(mSamplers); - that.mSampleResults.putAll(mSampleResults); + // Sampler can not be cloned. + // that.mSampleRegs.putAll(mSampleRegs); + // that.mSamplers.putAll(mSamplers); + // that.mSampleResults.putAll(mSampleResults); + + that.mTaskDeltas.putAll(this.mTaskDeltas); + that.mTaskDeltasCollect.putAll(this.mTaskDeltasCollect); + that.mExtras.putAll(this.mExtras); + that.mStacks.putAll(this.mStacks); + that.mCpuFreqSampler = this.mCpuFreqSampler; return that; } @@ -119,22 +188,113 @@ public int getCpuLoad() { MatrixLog.w(TAG, "AppStats should not be null to get CpuLoad"); return -1; } + long appJiffiesDelta; + Delta uidJiffies = getDelta(JiffiesMonitorFeature.UidJiffiesSnapshot.class); + if (uidJiffies != null) { + appJiffiesDelta = uidJiffies.dlt.totalUidJiffies.get(); + } else { + Delta pidJiffies = getDelta(JiffiesSnapshot.class); + if (pidJiffies == null) { + MatrixLog.w(TAG, JiffiesSnapshot.class + " should be metrics to get CpuLoad"); + return -1; + } + appJiffiesDelta = pidJiffies.dlt.totalJiffies.get(); + } + + long cpuUptimeDelta = mAppStats.duringMillis; + float cpuLoad = cpuUptimeDelta > 0 ? (float) (appJiffiesDelta * 10) / cpuUptimeDelta : 0; + return (int) (cpuLoad * 100); + } + + public int getNorCpuLoad() { + int cpuLoad = getCpuLoad(); + if (cpuLoad == -1) { + MatrixLog.w(TAG, "cpu is invalid"); + return -1; + } + MonitorFeature.Snapshot.Sampler.Result result = getSamplingResult(DeviceStatMonitorFeature.CpuFreqSnapshot.class); + if (result == null) { + MatrixLog.w(TAG, "cpufreq is null"); + return -1; + } + List cpuFreqSteps = BatteryCanaryUtil.getCpuFreqSteps(); + if (cpuFreqSteps.size() != BatteryCanaryUtil.getCpuCoreNum()) { + MatrixLog.w(TAG, "cpuCore is invalid: " + cpuFreqSteps.size() + " vs " + BatteryCanaryUtil.getCpuCoreNum()); + } + long sumMax = 0; + for (int[] steps : cpuFreqSteps) { + int max = 0; + for (int item : steps) { + if (item > max) { + max = item; + } + } + sumMax += max; + } + if (sumMax <= 0) { + MatrixLog.w(TAG, "cpufreq sum is invalid: " + sumMax); + return -1; + } + if (result.sampleAvg >= sumMax) { + // avgFreq should not greater than maxFreq + MatrixLog.w(TAG, "NorCpuLoad err: sampling = " + result); + for (int[] item : cpuFreqSteps) { + MatrixLog.w(TAG, "NorCpuLoad err: freqs = " + Arrays.toString(item)); + } + } + return (int) (cpuLoad * result.sampleAvg / sumMax); + } - Delta appJiffies = getDelta(JiffiesMonitorFeature.JiffiesSnapshot.class); - Delta cpuJiffies = getDelta(CpuStatFeature.CpuStateSnapshot.class); - if (appJiffies == null) { - MatrixLog.w(TAG, JiffiesMonitorFeature.JiffiesSnapshot.class + " should be metrics to get CpuLoad"); + /** + * Work in progress + */ + public int getDevCpuLoad() { + if (mAppStats == null) { + MatrixLog.w(TAG, "AppStats should not be null to get CpuLoad"); return -1; } + Delta cpuJiffies = getDelta(CpuStatFeature.CpuStateSnapshot.class); if (cpuJiffies == null) { - MatrixLog.w(TAG, CpuStatFeature.CpuStateSnapshot.class + "should be metrics to get CpuLoad"); + MatrixLog.w(TAG, "Configure CpuLoad by uptime"); + return -1; + } + + long cpuJiffiesDelta = cpuJiffies.dlt.totalCpuJiffies(); + long devJiffiesDelta = mAppStats.duringMillis; + float cpuLoad = devJiffiesDelta > 0 ? (float) (cpuJiffiesDelta * 10) / devJiffiesDelta : 0; + return (int) (cpuLoad * 100); + } + + public long computeAvgJiffies(long jiffies) { + if (mAppStats == null) { + MatrixLog.w(TAG, "AppStats should not be null to computeAvgJiffies"); return -1; } + return computeAvgJiffies(jiffies, mAppStats.duringMillis); + } - final long appJiffiesDelta = appJiffies.dlt.totalJiffies.get(); - final long cpuJiffiesDelta = cpuJiffies.dlt.totalCpuJiffies(); - final float cpuLoad = cpuJiffiesDelta > 0 ? (float) appJiffiesDelta / cpuJiffiesDelta : 0; - return (int) (cpuLoad * BatteryCanaryUtil.getCpuCoreNum() * 100); + public static long computeAvgJiffies(long jiffies, long millis) { + if (millis <= 0) { + throw new IllegalArgumentException("Illegal millis: " + millis); + } + return (long) (jiffies / (millis / 60000f)); + } + + public > boolean isOverHeat(Class snapshotClass) { + AppStats appStats = getAppStats(); + Delta delta = getDelta(snapshotClass); + if (appStats == null || delta == null) { + return false; + } + if (snapshotClass == JiffiesSnapshot.class) { + //noinspection unchecked + Delta jiffiesDelta = (Delta) delta; + long minute = appStats.getMinute(); + long avgJiffies = jiffiesDelta.dlt.totalJiffies.get() / minute; + return minute >= 5 && avgJiffies >= 1000; + } + // Override by child + return false; } @Nullable @@ -179,7 +339,7 @@ public void getSamplingResult(Class> snapshotClass, Consum @CallSuper public CompositeMonitors metricAll() { - metric(JiffiesMonitorFeature.JiffiesSnapshot.class); + metric(JiffiesSnapshot.class); metric(AlarmMonitorFeature.AlarmSnapshot.class); metric(WakeLockMonitorFeature.WakeLockSnapshot.class); metric(CpuStatFeature.CpuStateSnapshot.class); @@ -195,6 +355,16 @@ public CompositeMonitors metricAll() { return this; } + public CompositeMonitors metricCpuLoad() { + if (!mMetrics.contains(JiffiesSnapshot.class)) { + metric(JiffiesSnapshot.class); + } + if (!mMetrics.contains(CpuStatFeature.CpuStateSnapshot.class)) { + metric(CpuStatFeature.CpuStateSnapshot.class); + } + return this; + } + public CompositeMonitors metric(Class> snapshotClass) { if (!mMetrics.contains(snapshotClass)) { mMetrics.add(snapshotClass); @@ -211,16 +381,8 @@ public CompositeMonitors sample(Class> snapshotClass, long return this; } - @Deprecated - public void configureAllSnapshot() { - start(); - } - @Deprecated - public void configureDeltas() { - finish(); - } - public void start() { + MatrixLog.i(TAG, hashCode() + " #start: " + mScope); mAppStats = null; mBgnMillis = SystemClock.uptimeMillis(); configureBgnSnapshots(); @@ -228,14 +390,29 @@ public void start() { } public void finish() { + MatrixLog.i(TAG, hashCode() + " #finish: " + mScope); configureEndDeltas(); + collectStacks(); configureSampleResults(); mAppStats = AppStats.current(SystemClock.uptimeMillis() - mBgnMillis); + + // For further procedures + polishEstimatedPower(); } protected void configureBgnSnapshots() { for (Class> item : mMetrics) { - statCurrSnapshot(item); + Snapshot currSnapshot = statCurrSnapshot(item); + if (currSnapshot != null) { + mBgnSnapshots.put(item, currSnapshot); + + // Start acc collecting for HealthStatsSnapshot + if (currSnapshot instanceof HealthStatsSnapshot) { + if (mSampleRegs.containsKey(HealthStatsSnapshot.class)) { + ((HealthStatsSnapshot) currSnapshot).startAccCollecting(); + } + } + } } } @@ -247,12 +424,50 @@ protected void configureEndDeltas() { Class> snapshotClass = item.getKey(); Snapshot currSnapshot = statCurrSnapshot(snapshotClass); if (currSnapshot != null && currSnapshot.getClass() == lastSnapshot.getClass()) { - putDelta(snapshotClass, currSnapshot.diff(lastSnapshot)); + Delta delta; + if (lastSnapshot instanceof HealthStatsSnapshot && ((HealthStatsSnapshot) lastSnapshot).accCollector != null) { + delta = ((HealthStatsSnapshot) currSnapshot).diffByAccCollector((HealthStatsSnapshot) lastSnapshot); + } else { + delta = currSnapshot.diff(lastSnapshot); + } + putDelta(snapshotClass, delta); } } } } + protected void collectStacks() { + if (mMonitor == null) { + return; + } + // Figure out thread' stack if need + if (SCOPE_CANARY.equals(getScope())) { + // 待机功耗监控 + AppStats appStats = getAppStats(); + if (appStats != null && !appStats.isForeground()) { + getDelta(JiffiesSnapshot.class, new Consumer>() { + @Override + public void accept(Delta delta) { + long minute = Math.max(1, delta.during / BatteryCanaryUtil.ONE_MIN); + if (minute < 5) { + return; + } + for (ThreadJiffiesEntry threadEntry : delta.dlt.threadEntries.getList()) { + long topThreadAvgJiffies = threadEntry.get() / minute; + if (topThreadAvgJiffies < 3000L) { + break; + } + String stack = mMonitor.getConfig().callStackCollector.collect(threadEntry.tid); + if (!TextUtils.isEmpty(stack)) { + mStacks.put(String.valueOf(threadEntry.tid), stack); + } + } + } + }); + } + } + } + @CallSuper protected Snapshot statCurrSnapshot(Class> snapshotClass) { Snapshot snapshot = null; @@ -260,7 +475,6 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas AlarmMonitorFeature feature = getFeature(AlarmMonitorFeature.class); if (feature != null) { snapshot = feature.currentAlarms(); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } @@ -268,7 +482,6 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas BlueToothMonitorFeature feature = getFeature(BlueToothMonitorFeature.class); if (feature != null) { snapshot = feature.currentSnapshot(); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } @@ -276,7 +489,6 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas DeviceStatMonitorFeature feature = getFeature(DeviceStatMonitorFeature.class); if (feature != null) { snapshot = feature.currentCpuFreq(); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } @@ -284,23 +496,26 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas DeviceStatMonitorFeature feature = getFeature(DeviceStatMonitorFeature.class); if (feature != null && mMonitor != null) { snapshot = feature.currentBatteryTemperature(mMonitor.getContext()); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } - if (snapshotClass == JiffiesMonitorFeature.JiffiesSnapshot.class) { + if (snapshotClass == JiffiesSnapshot.class) { JiffiesMonitorFeature feature = getFeature(JiffiesMonitorFeature.class); if (feature != null) { snapshot = feature.currentJiffiesSnapshot(); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } + if (snapshotClass == JiffiesMonitorFeature.UidJiffiesSnapshot.class) { + JiffiesMonitorFeature feat = getFeature(JiffiesMonitorFeature.class); + if (feat != null) { + return feat.currentUidJiffiesSnapshot(); + } + } if (snapshotClass == LocationMonitorFeature.LocationSnapshot.class) { LocationMonitorFeature feature = getFeature(LocationMonitorFeature.class); if (feature != null) { snapshot = feature.currentSnapshot(); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } @@ -308,7 +523,6 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas TrafficMonitorFeature feature = getFeature(TrafficMonitorFeature.class); if (feature != null && mMonitor != null) { snapshot = feature.currentRadioSnapshot(mMonitor.getContext()); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } @@ -316,7 +530,6 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas WakeLockMonitorFeature feature = getFeature(WakeLockMonitorFeature.class); if (feature != null) { snapshot = feature.currentWakeLocks(); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } @@ -324,7 +537,6 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas WifiMonitorFeature feature = getFeature(WifiMonitorFeature.class); if (feature != null) { snapshot = feature.currentSnapshot(); - mBgnSnapshots.put(snapshotClass, snapshot); } return snapshot; } @@ -332,7 +544,13 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas CpuStatFeature feature = getFeature(CpuStatFeature.class); if (feature != null && feature.isSupported()) { snapshot = feature.currentCpuStateSnapshot(); - mBgnSnapshots.put(snapshotClass, snapshot); + } + return snapshot; + } + if (snapshotClass == CpuStatFeature.UidCpuStateSnapshot.class) { + CpuStatFeature feature = getFeature(CpuStatFeature.class); + if (feature != null && feature.isSupported()) { + snapshot = feature.currentUidCpuStateSnapshot(); } return snapshot; } @@ -340,7 +558,13 @@ protected Snapshot statCurrSnapshot(Class> snapshotClas AppStatMonitorFeature feature = getFeature(AppStatMonitorFeature.class); if (feature != null) { snapshot = feature.currentAppStatSnapshot(); - mBgnSnapshots.put(snapshotClass, snapshot); + } + return snapshot; + } + if (snapshotClass == HealthStatsSnapshot.class) { + HealthStatsFeature feature = getFeature(HealthStatsFeature.class); + if (feature != null) { + snapshot = feature.currHealthStatsSnapshot(); } return snapshot; } @@ -359,6 +583,7 @@ protected void configureSamplers() { protected void configureSampleResults() { for (Map.Entry>, Snapshot.Sampler> item : mSamplers.entrySet()) { + MatrixLog.i(TAG, hashCode() + " " + item.getValue().getTag() + " #pause: " + mScope); item.getValue().pause(); Snapshot.Sampler.Result result = item.getValue().getResult(); if (result != null) { @@ -373,18 +598,41 @@ protected Snapshot.Sampler statSampler(Class> snapshotClas if (snapshotClass == DeviceStatMonitorFeature.CpuFreqSnapshot.class) { final DeviceStatMonitorFeature feature = getFeature(DeviceStatMonitorFeature.class); if (feature != null && mMonitor != null) { - sampler = new Snapshot.Sampler(mMonitor.getHandler(), new Callable() { + final CpuStatFeature cpuStatsFeat = getFeature(CpuStatFeature.class); + if (cpuStatsFeat != null) { + if (cpuStatsFeat.isSupported()) { + mCpuFreqSampler = new CpuFreqSampler(BatteryCanaryUtil.getCpuFreqSteps()); + } + } + sampler = new Snapshot.Sampler("cpufreq", mMonitor.getHandler(), new Function() { @Override - public Number call() { - DeviceStatMonitorFeature.CpuFreqSnapshot snapshot = feature.currentCpuFreq(); - List> list = snapshot.cpuFreqs.getList(); - Collections.sort(list, new Comparator>() { - @Override - public int compare(DigitEntry o1, DigitEntry o2) { - return o1.get().compareTo(o2.get()); + public Number apply(Snapshot.Sampler sampler) { + int[] cpuFreqs = BatteryCanaryUtil.getCpuCurrentFreq(); + if (cpuStatsFeat != null && cpuStatsFeat.isSupported()) { + if (mCpuFreqSampler != null && mCpuFreqSampler.isCompat(cpuStatsFeat.getPowerProfile())) { + mCpuFreqSampler.count(cpuFreqs); } - }); - return list.isEmpty() ? 0 : list.get(list.size() - 1).get(); + } + DeviceStatMonitorFeature.CpuFreqSnapshot snapshot = feature.currentCpuFreq(cpuFreqs); + List> list = snapshot.cpuFreqs.getList(); + MatrixLog.i(TAG, CompositeMonitors.this.hashCode() + " #onSampling: " + mScope); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + ", val = " + list); + if (list.isEmpty()) { + return Snapshot.Sampler.INVALID; + } + // Better to use sum of all cpufreqs, rather than just use the max value? + // Collections.sort(list, new Comparator>() { + // @Override + // public int compare(DigitEntry o1, DigitEntry o2) { + // return o1.get().compareTo(o2.get()); + // } + // }); + // return list.isEmpty() ? 0 : list.get(list.size() - 1).get(); + long sum = 0; + for (DigitEntry item : list) { + sum += item.get(); + } + return sum; } }); mSamplers.put(snapshotClass, sampler); @@ -394,11 +642,202 @@ public int compare(DigitEntry o1, DigitEntry o2) { if (snapshotClass == DeviceStatMonitorFeature.BatteryTmpSnapshot.class) { final DeviceStatMonitorFeature feature = getFeature(DeviceStatMonitorFeature.class); if (feature != null && mMonitor != null) { - sampler = new Snapshot.Sampler(mMonitor.getHandler(), new Callable() { + sampler = new Snapshot.Sampler("batt-temp", mMonitor.getHandler(), new Function() { @Override - public Number call() { + public Number apply(Snapshot.Sampler sampler) { DeviceStatMonitorFeature.BatteryTmpSnapshot snapshot = feature.currentBatteryTemperature(mMonitor.getContext()); - return snapshot.temp.get(); + Integer value = snapshot.temp.get(); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + ", val = " + value); + if (value == -1) { + return Snapshot.Sampler.INVALID; + } + return value; + } + }); + mSamplers.put(snapshotClass, sampler); + } + return sampler; + } + if (snapshotClass == DeviceStatMonitorFeature.ThermalStatSnapshot.class) { + final DeviceStatMonitorFeature feature = getFeature(DeviceStatMonitorFeature.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && feature != null && mMonitor != null) { + sampler = new Snapshot.Sampler("thermal-stat", mMonitor.getHandler(), new Function() { + @Override + public Number apply(Snapshot.Sampler sampler) { + int value = BatteryCanaryUtil.getThermalStat(mMonitor.getContext()); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + ", val = " + value); + if (value == -1) { + return Snapshot.Sampler.INVALID; + } + return value; + } + }); + mSamplers.put(snapshotClass, sampler); + } + return sampler; + } + if (snapshotClass == DeviceStatMonitorFeature.ThermalHeadroomSnapshot.class) { + final DeviceStatMonitorFeature feature = getFeature(DeviceStatMonitorFeature.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && feature != null && mMonitor != null) { + final Long interval = mSampleRegs.get(snapshotClass); + if (interval != null && interval >= 1000L) { + sampler = new Snapshot.Sampler("thermal-headroom", mMonitor.getHandler(), new Function() { + @Override + public Number apply(Snapshot.Sampler sampler) { + float value = BatteryCanaryUtil.getThermalHeadroom(mMonitor.getContext(), (int) (interval / 1000L)); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + ", val = " + value); + if (value == -1f) { + return Snapshot.Sampler.INVALID; + } + return value; + } + }); + mSamplers.put(snapshotClass, sampler); + } + } + return sampler; + } + if (snapshotClass == DeviceStatMonitorFeature.ChargeWattageSnapshot.class) { + final DeviceStatMonitorFeature feature = getFeature(DeviceStatMonitorFeature.class); + if (feature != null && mMonitor != null) { + sampler = new Snapshot.Sampler("batt-watt", mMonitor.getHandler(), new Function() { + @Override + public Number apply(Snapshot.Sampler sampler) { + int value = BatteryCanaryUtil.getChargingWatt(mMonitor.getContext()); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + ", val = " + value); + if (value == -1) { + return Snapshot.Sampler.INVALID; + } + return value; + } + }); + mSamplers.put(snapshotClass, sampler); + } + return sampler; + } + if (snapshotClass == CpuStatFeature.CpuStateSnapshot.class) { + final CpuStatFeature feature = getFeature(CpuStatFeature.class); + if (feature != null && feature.isSupported() && mMonitor != null) { + sampler = new Snapshot.Sampler("cpu-stat", mMonitor.getHandler(), new Function() { + @Override + public Number apply(Snapshot.Sampler sampler) { + CpuStatFeature.CpuStateSnapshot snapshot = feature.currentCpuStateSnapshot(); + for (int i = 0; i < snapshot.cpuCoreStates.size(); i++) { + Snapshot.Entry.ListEntry> item = snapshot.cpuCoreStates.get(i); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + " cpuCore" + i + ", val = " + item.getList()); + } + for (int i = 0; i < snapshot.procCpuCoreStates.size(); i++) { + Snapshot.Entry.ListEntry> item = snapshot.procCpuCoreStates.get(i); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + " procCpuCluster" + i + ", val = " + item.getList()); + } + return 0; + } + }); + mSamplers.put(snapshotClass, sampler); + } + return sampler; + } + if (snapshotClass == JiffiesMonitorFeature.UidJiffiesSnapshot.class) { + final JiffiesMonitorFeature feature = getFeature(JiffiesMonitorFeature.class); + if (feature != null && mMonitor != null) { + sampler = new Snapshot.Sampler("uid-jiffies", mMonitor.getHandler(), new Function() { + JiffiesMonitorFeature.UidJiffiesSnapshot mLastSnapshot; + @Override + public Number apply(Snapshot.Sampler sampler) { + JiffiesMonitorFeature.UidJiffiesSnapshot curr = feature.currentUidJiffiesSnapshot(); + if (mLastSnapshot != null) { + Delta delta = curr.diff(mLastSnapshot); + long minute = Math.max(1, delta.during / BatteryCanaryUtil.ONE_MIN); + long avgUidJiffies = computeAvgJiffies(delta.dlt.totalUidJiffies.get(), delta.during); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + " avgUidJiffies, val = " + avgUidJiffies + ", minute = " + minute); + for (Delta item : delta.dlt.pidDeltaJiffiesList) { + long avgPidJiffies = computeAvgJiffies(item.dlt.totalJiffies.get(), delta.during); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + " avgPidJiffies, val = " + avgPidJiffies + ", minute = " + minute + ", name = " + item.dlt.name); + } + mLastSnapshot = curr; + return avgUidJiffies; + } else { + mLastSnapshot = curr; + } + return 0; + } + }); + mSamplers.put(snapshotClass, sampler); + } + return sampler; + } + if (snapshotClass == TrafficMonitorFeature.RadioStatSnapshot.class) { + final TrafficMonitorFeature feature = getFeature(TrafficMonitorFeature.class); + if (feature != null && mMonitor != null) { + sampler = new MonitorFeature.Snapshot.Sampler("traffic", mMonitor.getHandler(), new Function() { + @Override + public Number apply(Snapshot.Sampler sampler) { + TrafficMonitorFeature.RadioStatSnapshot snapshot = feature.currentRadioSnapshot(mMonitor.getContext()); + if (snapshot != null) { + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + " wifiRx, val = " + snapshot.wifiRxBytes); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + " wifiTx, val = " + snapshot.wifiTxBytes); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + " mobileRx, val = " + snapshot.mobileRxBytes); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + " mobileTx, val = " + snapshot.mobileTxBytes); + } + return 0; + } + }); + mSamplers.put(snapshotClass, sampler); + } + return sampler; + } + if (snapshotClass == TrafficMonitorFeature.RadioBpsSnapshot.class) { + final TrafficMonitorFeature feature = getFeature(TrafficMonitorFeature.class); + if (feature != null && mMonitor != null) { + mBpsSampler = new BpsSampler(); + sampler = new MonitorFeature.Snapshot.Sampler("trafficBps", mMonitor.getHandler(), new Function() { + @Override + public Number apply(Snapshot.Sampler sampler) { + TrafficMonitorFeature.RadioBpsSnapshot snapshot = feature.currentRadioBpsSnapshot(mMonitor.getContext()); + if (snapshot != null) { + mBpsSampler.count(snapshot); + } + return 0; + } + }); + mSamplers.put(snapshotClass, sampler); + } + return sampler; + } + if (snapshotClass == DeviceStatMonitorFeature.BatteryCurrentSnapshot.class) { + final DeviceStatMonitorFeature feature = getFeature(DeviceStatMonitorFeature.class); + if (feature != null && mMonitor != null) { + sampler = new MonitorFeature.Snapshot.Sampler("batt-curr", mMonitor.getHandler(), new Function() { + @Override + public Number apply(Snapshot.Sampler sampler) { + if (BatteryCanaryUtil.isDeviceCharging(mMonitor.getContext())) { + // battery currency is meaningless when charging + return Snapshot.Sampler.INVALID; + } + long value = BatteryCanaryUtil.getBatteryCurrencyImmediately(mMonitor.getContext()); + MatrixLog.i(TAG, "onSampling " + sampler.mCount + " " + sampler.mTag + ", val = " + value); + if (value == -1L) { + return Snapshot.Sampler.INVALID; + } + return value; + } + }); + mSamplers.put(snapshotClass, sampler); + } + return sampler; + } + if (snapshotClass == HealthStatsSnapshot.class) { + final HealthStatsFeature feature = getFeature(HealthStatsFeature.class); + if (feature != null && mMonitor != null) { + sampler = new MonitorFeature.Snapshot.Sampler("health-stats", mMonitor.getHandler(), new Function() { + @Override + public Number apply(Snapshot.Sampler sampler) { + Snapshot snapshot = mBgnSnapshots.get(HealthStatsSnapshot.class); + if (snapshot instanceof HealthStatsSnapshot) { + MatrixLog.i(TAG, "onAcc " + sampler.mCount + " " + sampler.mTag); + ((HealthStatsSnapshot) snapshot).accCollect(feature.currHealthStatsSnapshot()); + } + return Snapshot.Sampler.INVALID; } }); mSamplers.put(snapshotClass, sampler); @@ -408,7 +847,124 @@ public Number call() { return null; } + protected void configureTaskDeltas(final Class featClass) { + if (mAppStats != null) { + AbsTaskMonitorFeature taskFeat = getFeature(featClass); + if (taskFeat != null) { + List> deltas = taskFeat.currentJiffies(mAppStats.duringMillis); + // No longer clear here + // Clear at BG Scope or OverHeat + // taskFeat.clearFinishedJiffies(); + putTaskDeltas(featClass, deltas); + } + } + } + + protected void collectTaskDeltas() { + if (!mTaskDeltas.isEmpty()) { + for (Map.Entry, List>> entry : mTaskDeltas.entrySet()) { + Class key = entry.getKey(); + for (Delta taskDelta : entry.getValue()) { + // FIXME: better windowMillis cfg of Task and AppStats + if (taskDelta.bgn.time >= mBgnMillis) { + List, Delta>> pairList = mTaskDeltasCollect.get(taskDelta.dlt.name); + if (pairList == null) { + pairList = new ArrayList<>(); + mTaskDeltasCollect.put(taskDelta.dlt.name, pairList); + } + pairList.add(new Pair, Delta>(key, taskDelta)); + } + } + } + } + } + + public void putTaskDeltas(Class key, List> deltas) { + mTaskDeltas.put(key, deltas); + } + + public List> getTaskDeltas(Class key) { + List> deltas = mTaskDeltas.get(key); + if (deltas == null) { + return Collections.emptyList(); + } + return deltas; + } + + public void getTaskDeltas(Class key, Consumer>> block) { + List> deltas = mTaskDeltas.get(key); + if (deltas != null) { + block.accept(deltas); + } + } + + public Map, Delta>>> getCollectedTaskDeltas() { + if (mTaskDeltasCollect.size() <= 1) { + return mTaskDeltasCollect; + } + // Sorting by jiffies sum + return BatteryCanaryUtil.sortMapByValue(mTaskDeltasCollect, new Comparator, Delta>>>>() { + @SuppressWarnings("ConstantConditions") + @Override + public int compare(Map.Entry, Delta>>> o1, Map.Entry, Delta>>> o2) { + long sumLeft = 0, sumRight = 0; + for (Pair, Delta> item : o1.getValue()) { + sumLeft += item.second.dlt.jiffies.get(); + } + for (Pair, Delta> item : o2.getValue()) { + sumRight += item.second.dlt.jiffies.get(); + } + long minus = sumLeft - sumRight; + if (minus == 0) return 0; + if (minus > 0) return -1; + return 1; + } + }); + } + + public void getCollectedTaskDeltas(Consumer, Delta>>>> block) { + block.accept(getCollectedTaskDeltas()); + } + + public void getAllPidDeltaList(Consumer>> block) { + List> deltaList = getAllPidDeltaList(); + if (deltaList != null) { + block.accept(deltaList); + } + } + + public List> getAllPidDeltaList() { + Delta delta = getDelta(JiffiesMonitorFeature.UidJiffiesSnapshot.class); + if (delta == null) { + Delta pidDelta = getDelta(JiffiesSnapshot.class); + if (pidDelta != null) { + return Collections.singletonList(pidDelta); + } + return Collections.emptyList(); + } + return delta.dlt.pidDeltaJiffiesList; + } + + public Map getStacks() { + return mStacks; + } + + public Bundle getExtras() { + return mExtras; + } + + @Nullable + public CpuFreqSampler getCpuFreqSampler() { + return mCpuFreqSampler; + } + + @Nullable + public BpsSampler getBpsSampler() { + return mBpsSampler; + } + @Override + @NonNull public String toString() { return "CompositeMonitors{" + "\n" + "Metrics=" + mMetrics + "\n" + @@ -417,7 +973,483 @@ public String toString() { ", SampleRegs=" + mSampleRegs + "\n" + ", Samplers=" + mSamplers + "\n" + ", SampleResults=" + mSampleResults + "\n" + + ", TaskDeltas=" + mTaskDeltas + "\n" + ", AppStats=" + mAppStats + "\n" + + ", Stacks=" + mStacks + "\n" + + ", Extras =" + mExtras + "\n" + '}'; } + + public static class CpuFreqSampler { + public int[] cpuCurrentFreq; + public final List cpuFreqSteps; + public final List cpuFreqCounters; + + public CpuFreqSampler(List cpuFreqSteps) { + this.cpuFreqSteps = cpuFreqSteps; + this.cpuFreqCounters = new ArrayList<>(cpuFreqSteps.size()); + for (int[] item : cpuFreqSteps) { + this.cpuFreqCounters.add(new int[item.length]); + } + } + + public boolean isCompat(PowerProfile powerProfile) { + if (cpuFreqSteps.size() == powerProfile.getCpuCoreNum()) { + for (int i = 0; i < cpuFreqSteps.size(); i++) { + int clusterByCpuNum = powerProfile.getClusterByCpuNum(i); + int steps = powerProfile.getNumSpeedStepsInCpuCluster(clusterByCpuNum); + if (cpuFreqSteps.get(i).length != steps) { + return false; + } + } + return true; + } + return false; + } + + public void count(int[] cpuCurrentFreq) { + this.cpuCurrentFreq = cpuCurrentFreq; + for (int i = 0; i < cpuCurrentFreq.length; i++) { + int speed = cpuCurrentFreq[i]; + int[] steps = cpuFreqSteps.get(i); + if (speed < steps[0]) { + cpuFreqCounters.get(i)[0]++; + continue; + } + boolean found = false; + for (int j = 0; j < steps.length; j++) { + if (speed <= steps[j]) { + cpuFreqCounters.get(i)[j]++; + found = true; + break; + } + } + if (!found) { + if (speed > steps[steps.length - 1]) { + cpuFreqCounters.get(i)[steps.length - 1]++; + } + } + } + } + } + + public static class BpsSampler { + public int count; + public long wifiRxBps; + public long wifiTxBps; + public long mobileRxBps; + public long mobileTxBps; + + public void count(TrafficMonitorFeature.RadioBpsSnapshot snapshot) { + count++; + wifiRxBps += snapshot.wifiRxBps.get(); + wifiTxBps += snapshot.wifiTxBps.get(); + mobileRxBps += snapshot.mobileRxBps.get(); + mobileTxBps += snapshot.mobileTxBps.get(); + } + + public double getAverage(long input) { + if (count != 0) { + return input * 1f / count; + } + return 0; + } + } + + + protected void polishEstimatedPower() { + getDelta(HealthStatsFeature.HealthStatsSnapshot.class, new Consumer>() { + @Override + public void accept(Delta healthStatsDelta) { + tuningPowers(healthStatsDelta.dlt); + { + // Reset cpuPower + double power = 0; + Object powers = healthStatsDelta.dlt.extras.get("JiffyUid"); + if (powers instanceof Map) { + // Take power-cpu-uidDiff or 0 as default + Object val = ((Map) powers).get("power-cpu-uidDiff"); + if (val instanceof Double) { + power = (double) val; + } + } + healthStatsDelta.dlt.cpuPower = DigitEntry.of(power); + } + { + // Reset mobilePower if exists + if (healthStatsDelta.dlt.mobilePower.get() <= 0) { + double power = 0; + // Take power-mobile-statByte + Object val = healthStatsDelta.dlt.extras.get("power-mobile-statByte"); + if (val instanceof Double) { + power = (double) val; + } + healthStatsDelta.dlt.mobilePower = DigitEntry.of(power); + } + } + { + // Reset wifiPower if exists + if (healthStatsDelta.dlt.wifiPower.get() <= 0) { + double power = 0; + // Take power-wifi-statByte + Object val = healthStatsDelta.dlt.extras.get("power-wifi-statByte"); + if (val instanceof Double) { + power = (double) val; + } + healthStatsDelta.dlt.wifiPower = DigitEntry.of(power); + } + } + } + }); + } + + protected void tuningPowers(final HealthStatsFeature.HealthStatsSnapshot snapshot) { + if (!snapshot.isDelta) { + throw new IllegalStateException("Only support delta snapshot"); + } + BatteryMonitorCore monitor = getMonitor(); + if (monitor == null) { + return; + } + snapshot.extras = new HashMap<>(); + final boolean tunning = monitor.getConfig().isTuningPowers; + final Tuner tuner = new Tuner(); + + getFeature(CpuStatFeature.class, new Consumer() { + @Override + public void accept(CpuStatFeature feat) { + if (feat.isSupported()) { + final PowerProfile powerProfile = feat.getPowerProfile(); + + // 1. Tune CPU + getDelta(CpuStatFeature.CpuStateSnapshot.class, new Consumer>() { + @Override + public void accept(final Delta cpuStatDelta) { + // 1.1 CpuTimeMs + if (tunning) { + getDelta(HealthStatsSnapshot.class, new Consumer>() { + @Override + public void accept(final Delta healthStats) { + healthStats.dlt.extras.put("TimeUid", tuner.tuningCpuPowers(powerProfile, CompositeMonitors.this, new Tuner.CpuTime() { + @Override + public long getBgnMs(String procSuffix) { + return getCpuTimeMs(procSuffix, healthStats.bgn); + } + + @Override + public long getEndMs(String procSuffix) { + return getCpuTimeMs(procSuffix, healthStats.end); + } + + @Override + public long getDltMs(String procSuffix) { + return getCpuTimeMs(procSuffix, healthStats.dlt); + } + + private long getCpuTimeMs(String procSuffix, HealthStatsSnapshot healthStatsSnapshot) { + if (procSuffix == null) { + return healthStatsSnapshot.cpuUsrTimeMs.get() + + healthStatsSnapshot.cpuSysTimeMs.get(); + } else { + if (mMonitor != null) { + String procName = mMonitor.getContext().getPackageName(); + if ("main".equals(procSuffix)) { + procName = mMonitor.getContext().getPackageName() + ":" + procSuffix; + } + DigitEntry usrTime = healthStatsSnapshot.procStatsCpuUsrTimeMs.get(procName); + DigitEntry sysTime = healthStatsSnapshot.procStatsCpuSysTimeMs.get(procName); + return (usrTime == null ? 0 : usrTime.get()) + (sysTime == null ? 0 : sysTime.get()); + } + } + return 0L; + } + })); + } + }); + } + + // 1.2 CpuTimeJiffies + getDelta(JiffiesMonitorFeature.UidJiffiesSnapshot.class, new Consumer>() { + @Override + public void accept(final Delta delta) { + snapshot.extras.put("JiffyUid", tuner.tuningCpuPowers(powerProfile, CompositeMonitors.this, new Tuner.CpuTime() { + @Override + public long getBgnMs(String procSuffix) { + if (procSuffix == null) { + // All + return delta.bgn.totalUidJiffies.get() * 10L; + } else { + for (JiffiesSnapshot item : delta.bgn.pidCurrJiffiesList) { + if (item.name.equals(procSuffix)) { + return item.totalJiffies.get() * 10L; + } + } + } + return 0L; + } + @Override + public long getEndMs(String procSuffix) { + if (procSuffix == null) { + // All + return delta.end.totalUidJiffies.get() * 10L; + } else { + for (JiffiesSnapshot item : delta.end.pidCurrJiffiesList) { + if (item.name.equals(procSuffix)) { + return item.totalJiffies.get() * 10L; + } + } + } + return 0L; + } + @Override + public long getDltMs(String procSuffix) { + if (procSuffix == null) { + // All + return delta.dlt.totalUidJiffies.get() * 10L; + } else { + for (Delta item : delta.dlt.pidDeltaJiffiesList) { + if (item.dlt.name.equals(procSuffix)) { + return item.dlt.totalJiffies.get() * 10L; + } + } + } + return 0L; + } + })); + } + }); + } + }); + + // 2. Tune Network + { + double mobileRxBps = 0, mobileTxBps = 0; + double wifiRxBps = 0, wifiTxBps = 0; + BpsSampler bpsSampler = getBpsSampler(); + if (bpsSampler != null) { + mobileRxBps = bpsSampler.getAverage(bpsSampler.mobileRxBps); + mobileTxBps = bpsSampler.getAverage(bpsSampler.mobileTxBps); + wifiRxBps = bpsSampler.getAverage(bpsSampler.wifiRxBps); + wifiTxBps = bpsSampler.getAverage(bpsSampler.wifiTxBps); + } else { + if (mMonitor != null) { + RadioStatUtil.RadioBps bpsStat = RadioStatUtil.getCurrentBps(mMonitor.getContext()); + if (bpsStat != null) { + mobileRxBps = bpsStat.mobileRxBps; + mobileTxBps = bpsStat.mobileTxBps; + wifiRxBps = bpsStat.wifiRxBps; + wifiTxBps = bpsStat.wifiTxBps; + } + } + } + + final double finalMobileRxBps = mobileRxBps; + final double finalMobileTxBps = mobileTxBps; + final double finalWifiRxBps = wifiRxBps; + final double finalWifiTxBps = wifiTxBps; + + // 2.1 HealthStats + getDelta(HealthStatsSnapshot.class, new Consumer>() { + @SuppressLint("VisibleForTests") + @Override + public void accept(Delta delta) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + // mobile + { + double power = 0; + if (delta.bgn.healthStats != null && delta.end.healthStats != null) { + double powerBgn = HealthStatsHelper.calcMobilePowerByRadioActive(powerProfile, delta.bgn.healthStats); + double powerEnd = HealthStatsHelper.calcMobilePowerByRadioActive(powerProfile, delta.end.healthStats); + power = powerEnd - powerBgn; + } + snapshot.extras.put("power-mobile-radio", power); + } + { + double power = 0; + if (delta.bgn.healthStats != null && delta.end.healthStats != null) { + double powerBgn = HealthStatsHelper.calcMobilePowerByController(powerProfile, delta.bgn.healthStats); + double powerEnd = HealthStatsHelper.calcMobilePowerByController(powerProfile, delta.end.healthStats); + power = powerEnd - powerBgn; + } + snapshot.extras.put("power-mobile-controller", power); + } + { + double power = 0; + if (delta.bgn.healthStats != null && delta.end.healthStats != null) { + double powerBgn = HealthStatsHelper.calcMobilePowerByPackets(powerProfile, delta.bgn.healthStats, finalMobileRxBps, finalMobileTxBps); + double powerEnd = HealthStatsHelper.calcMobilePowerByPackets(powerProfile, delta.end.healthStats, finalMobileRxBps, finalMobileTxBps); + power = powerEnd - powerBgn; + } + snapshot.extras.put("power-mobile-packet", power); + } + // wifi + { + double power = 0; + if (delta.bgn.healthStats != null && delta.end.healthStats != null) { + double powerBgn = HealthStatsHelper.calcWifiPowerByController(powerProfile, delta.bgn.healthStats); + double powerEnd = HealthStatsHelper.calcWifiPowerByController(powerProfile, delta.end.healthStats); + power = powerEnd - powerBgn; + } + snapshot.extras.put("power-wifi-controller", power); + } + { + double power = 0; + if (delta.bgn.healthStats != null && delta.end.healthStats != null) { + double powerBgn = HealthStatsHelper.calcWifiPowerByPackets(powerProfile, delta.bgn.healthStats, finalWifiRxBps, finalWifiTxBps); + double powerEnd = HealthStatsHelper.calcWifiPowerByPackets(powerProfile, delta.end.healthStats, finalWifiRxBps, finalWifiTxBps); + power = powerEnd - powerBgn; + } + snapshot.extras.put("power-wifi-packet", power); + } + } + } + }); + + // 2.2 RadioStat + getDelta(TrafficMonitorFeature.RadioStatSnapshot.class, new Consumer>() { + @SuppressLint("VisibleForTests") + @Override + public void accept(Delta delta) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mMonitor != null) { + BpsSampler bpsSampler = getBpsSampler(); + if (bpsSampler != null) { + // mobile + double power = HealthStatsHelper.calcMobilePowerByNetworkStatBytes(powerProfile, delta.dlt, finalMobileRxBps, finalMobileTxBps); + snapshot.extras.put("power-mobile-statByte", power); + power = HealthStatsHelper.calcMobilePowerByNetworkStatPackets(powerProfile, delta.dlt, finalMobileRxBps, finalMobileTxBps); + snapshot.extras.put("power-mobile-statPacket", power); + // wifi + power = HealthStatsHelper.calcWifiPowerByNetworkStatBytes(powerProfile, delta.dlt, finalWifiRxBps, finalWifiTxBps); + snapshot.extras.put("power-wifi-statByte", power); + power = HealthStatsHelper.calcWifiPowerByNetworkStatPackets(powerProfile, delta.dlt, finalWifiRxBps, finalWifiTxBps); + snapshot.extras.put("power-wifi-statPacket", power); + } + } + } + }); + } + } + } + }); + } + + + @SuppressLint("RestrictedApi") + protected static class Tuner { + interface CpuTime { + long getBgnMs(String procSuffix); + long getEndMs(String procSuffix); + long getDltMs(String procSuffix); + } + + public Map tuningCpuPowers(final PowerProfile powerProfile, final CompositeMonitors monitors, final CpuTime cpuTime) { + final Map dict = new LinkedHashMap<>(); + monitors.getDelta(CpuStatFeature.UidCpuStateSnapshot.class, new Consumer>() { + @SuppressLint("VisibleForTests") + @Override + public void accept(Delta uidCpuStatDelta) { + BatteryMonitorCore monitor = monitors.getMonitor(); + if (monitor == null) { + return; + } + + // Calc by DIff + { + // UID Diff + double cpuPower = 0; + boolean scaled = false; + for (Delta cpuStateDelta : uidCpuStatDelta.dlt.pidDeltaCpuSateList) { + CpuStatFeature.CpuStateSnapshot cpuStatsSnapshot = cpuStateDelta.dlt; + long cpuTimeMs = cpuTime.getDltMs(cpuStatsSnapshot.name); + cpuPower += HealthStatsHelper.estimateCpuActivePower(powerProfile, cpuTimeMs) + + HealthStatsHelper.estimateCpuClustersPowerByUidStats(powerProfile, cpuStatsSnapshot, cpuTimeMs, scaled) + + HealthStatsHelper.estimateCpuCoresPowerByUidStats(powerProfile, cpuStatsSnapshot, cpuTimeMs, scaled); + } + dict.put("power-cpu-uidDiff", cpuPower); + } + + boolean tunning = monitor.getConfig().isTuningPowers; + if (!tunning) { + return; + } + + { + // UID Diff Scaled + double cpuPower = 0; + boolean scaled = true; + for (Delta cpuStateDelta : uidCpuStatDelta.dlt.pidDeltaCpuSateList) { + CpuStatFeature.CpuStateSnapshot cpuStatsSnapshot = cpuStateDelta.dlt; + long cpuTimeMs = cpuTime.getDltMs(cpuStatsSnapshot.name); + cpuPower += HealthStatsHelper.estimateCpuActivePower(powerProfile, cpuTimeMs) + + HealthStatsHelper.estimateCpuClustersPowerByUidStats(powerProfile, cpuStatsSnapshot, cpuTimeMs, scaled) + + HealthStatsHelper.estimateCpuCoresPowerByUidStats(powerProfile, cpuStatsSnapshot, cpuTimeMs, scaled); + } + dict.put("power-cpu-uidDiffScale", cpuPower); + } + + monitors.getDelta(CpuStatFeature.CpuStateSnapshot.class, new Consumer>() { + @Override + public void accept(Delta pidCpuStatDelta) { + { + // DEV Diff + CpuStatFeature.CpuStateSnapshot cpuStatsSnapshot = pidCpuStatDelta.dlt; + long cpuTimeMs = cpuTime.getDltMs(null); + double cpuPower = HealthStatsHelper.estimateCpuActivePower(powerProfile, cpuTimeMs) + + HealthStatsHelper.estimateCpuClustersPowerByDevStats(powerProfile, cpuStatsSnapshot, cpuTimeMs) + + HealthStatsHelper.estimateCpuCoresPowerByDevStats(powerProfile, cpuStatsSnapshot, cpuTimeMs); + dict.put("power-cpu-devDiff", cpuPower); + } + { + // CpuFreq Diff + CompositeMonitors.CpuFreqSampler cpuFreqSampler = monitors.getCpuFreqSampler(); + if (cpuFreqSampler != null) { + if (cpuFreqSampler.isCompat(powerProfile)) { + long cpuTimeMs = cpuTime.getDltMs(null); + double cpuPower = HealthStatsHelper.estimateCpuActivePower(powerProfile, cpuTimeMs) + + estimateCpuPowerByCpuFreqStats(powerProfile, cpuFreqSampler, cpuTimeMs); + dict.put("power-cpu-cpuFreq", cpuPower); + } + } + } + } + }); + } + }); + + return dict; + } + + private static double estimateCpuPowerByCpuFreqStats(PowerProfile powerProfile, CompositeMonitors.CpuFreqSampler sampler, long cpuTimeMs) { + double powerMah = 0; + if (cpuTimeMs > 0) { + long totalSum = 0; + for (int i = 0; i < sampler.cpuFreqCounters.size(); i++) { + for (int j = 0; j < sampler.cpuFreqCounters.get(i).length; j++) { + totalSum += sampler.cpuFreqCounters.get(i)[j]; + } + } + if (totalSum > 0) { + for (int i = 0; i < sampler.cpuFreqCounters.size(); i++) { + int clusterNum = powerProfile.getClusterByCpuNum(i); + long coreSum = 0; + for (int j = 0; j < sampler.cpuFreqCounters.get(i).length; j++) { + int step = sampler.cpuFreqCounters.get(i)[j]; + if (step > 0) { + long figuredCoreTimeMs = (long) ((step * 1.0f / totalSum) * cpuTimeMs); + double powerMa = powerProfile.getAveragePowerForCpuCore(clusterNum, j); + powerMah += new HealthStatsHelper.UsageBasedPowerEstimator(powerMa).calculatePower(figuredCoreTimeMs); + } + coreSum += step; + } + if (coreSum > 0) { + long figuredClusterTimeMs = (long) ((coreSum * 1.0f / totalSum) * cpuTimeMs); + double powerMa = powerProfile.getAveragePowerForCpuCluster(clusterNum); + powerMah += new HealthStatsHelper.UsageBasedPowerEstimator(powerMa).calculatePower(figuredClusterTimeMs); + } + } + } + } + return powerMah; + } + } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/CpuStatFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/CpuStatFeature.java index d1af3617d..6e9aefe36 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/CpuStatFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/CpuStatFeature.java @@ -1,12 +1,18 @@ package com.tencent.matrix.batterycanary.monitor.feature; +import android.os.Parcel; +import android.os.Parcelable; import android.os.Process; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.DigitEntry; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.ListEntry; +import com.tencent.matrix.batterycanary.shell.TopThreadFeature; +import com.tencent.matrix.batterycanary.shell.ui.TopThreadIndicator; +import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.KernelCpuSpeedReader; import com.tencent.matrix.batterycanary.utils.KernelCpuUidFreqTimeReader; import com.tencent.matrix.batterycanary.utils.PowerProfile; +import com.tencent.matrix.batterycanary.utils.ProcStatUtil; import com.tencent.matrix.util.MatrixLog; import java.io.IOException; @@ -15,6 +21,7 @@ import java.util.List; import androidx.annotation.WorkerThread; +import androidx.core.util.Pair; import static com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil.JIFFY_MILLIS; import static com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil.ONE_HOR; @@ -24,7 +31,7 @@ * @since 2021/9/10 */ @SuppressWarnings("SpellCheckingInspection") -public class CpuStatFeature extends AbsTaskMonitorFeature { +public class CpuStatFeature extends AbsTaskMonitorFeature { private static final String TAG = "Matrix.battery.CpuStatFeature"; private PowerProfile mPowerProfile; @@ -98,6 +105,10 @@ public PowerProfile getPowerProfile() { } public CpuStateSnapshot currentCpuStateSnapshot() { + return currentCpuStateSnapshot(Process.myPid()); + } + + public CpuStateSnapshot currentCpuStateSnapshot(int pid) { CpuStateSnapshot snapshot = new CpuStateSnapshot(); try { if (!isSupported()) { @@ -108,21 +119,22 @@ public CpuStateSnapshot currentCpuStateSnapshot() { throw new IOException("PowerProfile not supported"); } // Cpu core steps jiffies - snapshot.cpuCoreStates = new ArrayList<>(); - for (int i = 0; i < mPowerProfile.getCpuCoreNum(); i++) { - final int numSpeedSteps = mPowerProfile.getNumSpeedStepsInCpuCluster(mPowerProfile.getClusterByCpuNum(i)); - KernelCpuSpeedReader cpuStepJiffiesReader = new KernelCpuSpeedReader(i, numSpeedSteps); - long[] cpuCoreStepJiffies = cpuStepJiffiesReader.readAbsolute(); - ListEntry> cpuCoreState = ListEntry.ofDigits(cpuCoreStepJiffies); - snapshot.cpuCoreStates.add(cpuCoreState); + if (pid == Process.myPid()) { + snapshot.cpuCoreStates = new ArrayList<>(); + for (int i = 0; i < mPowerProfile.getCpuCoreNum(); i++) { + final int numSpeedSteps = mPowerProfile.getNumSpeedStepsInCpuCluster(mPowerProfile.getClusterByCpuNum(i)); + KernelCpuSpeedReader cpuStepJiffiesReader = new KernelCpuSpeedReader(i, numSpeedSteps); + long[] cpuCoreStepJiffies = cpuStepJiffiesReader.readAbsolute(); + ListEntry> cpuCoreState = ListEntry.ofDigits(cpuCoreStepJiffies); + snapshot.cpuCoreStates.add(cpuCoreState); + } } - // Proc cluster steps jiffies int[] clusterSteps = new int[mPowerProfile.getNumCpuClusters()]; for (int i = 0; i < clusterSteps.length; i++) { clusterSteps[i] = mPowerProfile.getNumSpeedStepsInCpuCluster(i); } - KernelCpuUidFreqTimeReader procStepJiffiesReader = new KernelCpuUidFreqTimeReader(Process.myPid(), clusterSteps); + KernelCpuUidFreqTimeReader procStepJiffiesReader = new KernelCpuUidFreqTimeReader(pid, clusterSteps); List procStepJiffies = procStepJiffiesReader.readAbsolute(); snapshot.procCpuCoreStates = new ArrayList<>(); for (long[] item : procStepJiffies) { @@ -137,6 +149,47 @@ public CpuStateSnapshot currentCpuStateSnapshot() { return snapshot; } + public UidCpuStateSnapshot currentUidCpuStateSnapshot() { + UidCpuStateSnapshot curr = new UidCpuStateSnapshot(); + try { + List> procList = TopThreadFeature.getProcList(mCore.getContext()); + curr.pidCurrCupSateList = new ArrayList<>(procList.size()); + + for (Pair item : procList) { + //noinspection ConstantConditions + int pid = item.first; + String procName = String.valueOf(item.second); + CpuStateSnapshot snapshot = null; + + if (pid == Process.myPid()) { + // from local + snapshot = currentCpuStateSnapshot(); + } else { + if (ProcStatUtil.exists(pid)) { + // from pid + snapshot = currentCpuStateSnapshot(pid); + } + if (snapshot != null && !snapshot.isValid() && mCore.getConfig().ipcCpuStatCollector != null) { + // from ipc + UidCpuStateSnapshot.IpcCpuStat.RemoteStat remote = mCore.getConfig().ipcCpuStatCollector.apply(item); + if (remote != null) { + snapshot = UidCpuStateSnapshot.IpcCpuStat.toLocal(remote); + } + } + } + if (snapshot != null) { + snapshot.pid = pid; + snapshot.name = TopThreadIndicator.getProcSuffix(procName); + curr.pidCurrCupSateList.add(snapshot); + } + } + } catch (Exception e) { + MatrixLog.w(TAG, "get curr UidCpuStatSnapshot failed: " + e.getMessage()); + curr.setValid(false); + } + return curr; + } + public static final class CpuStateSnapshot extends Snapshot { /* * cpuCoreStates @@ -155,6 +208,8 @@ public static final class CpuStateSnapshot extends Snapshot { */ public List>> cpuCoreStates = Collections.emptyList(); public List>> procCpuCoreStates = Collections.emptyList(); + public int pid = Process.myPid(); + public String name = BatteryCanaryUtil.getProcessName(); public long totalCpuJiffies() { long sum = 0; @@ -166,6 +221,16 @@ public long totalCpuJiffies() { return sum; } + public long totalProcCpuJiffies() { + long sum = 0; + for (ListEntry> cpuCoreState : procCpuCoreStates) { + for (DigitEntry item : cpuCoreState.getList()) { + sum += item.value; + } + } + return sum; + } + public double configureCpuSip(PowerProfile powerProfile) { if (!powerProfile.isSupported()) { return 0; @@ -217,6 +282,8 @@ public Delta diff(CpuStateSnapshot bgn) { @Override protected CpuStateSnapshot computeDelta() { CpuStateSnapshot delta = new CpuStateSnapshot(); + delta.pid = end.pid; + delta.name = end.name; if (bgn.cpuCoreStates.size() != end.cpuCoreStates.size()) { delta.setValid(false); } else { @@ -234,4 +301,111 @@ protected CpuStateSnapshot computeDelta() { }; } } + + public static final class UidCpuStateSnapshot extends MonitorFeature.Snapshot { + public List pidCurrCupSateList = Collections.emptyList(); + public List> pidDeltaCpuSateList = Collections.emptyList(); + + @Override + public MonitorFeature.Snapshot.Delta diff(UidCpuStateSnapshot bgn) { + return new MonitorFeature.Snapshot.Delta(bgn, this) { + @Override + protected UidCpuStateSnapshot computeDelta() { + UidCpuStateSnapshot delta = new UidCpuStateSnapshot(); + if (end.pidCurrCupSateList.size() > 0) { + delta.pidDeltaCpuSateList = new ArrayList<>(); + for (CpuStateSnapshot end : end.pidCurrCupSateList) { + CpuStateSnapshot last = null; + for (CpuStateSnapshot bgn : bgn.pidCurrCupSateList) { + if (bgn.pid == end.pid) { + last = bgn; + break; + } + } + if (last == null) { + // newAdded Pid + CpuStateSnapshot empty = new CpuStateSnapshot(); + empty.pid = end.pid; + empty.name = end.name; + empty.procCpuCoreStates = new ArrayList<>(end.procCpuCoreStates.size()); + for (ListEntry> item : end.procCpuCoreStates) { + long[] emptyStats = new long[item.getList().size()]; + empty.procCpuCoreStates.add(ListEntry.ofDigits(emptyStats)); + } + last = empty; + } + MonitorFeature.Snapshot.Delta deltaPidCpuState = end.diff(last); + delta.pidDeltaCpuSateList.add(deltaPidCpuState); + } + } + return delta; + } + }; + } + + + public static class IpcCpuStat { + public static RemoteStat toIpc(CpuStateSnapshot local) { + RemoteStat remote = new RemoteStat(); + remote.procCpuCoreStates = new ArrayList<>(local.procCpuCoreStates.size()); + for (ListEntry> item : local.procCpuCoreStates) { + long[] stats = new long[item.getList().size()]; + for (int i = 0; i < stats.length; i++) { + stats[i] = item.getList().get(i).get(); + } + remote.procCpuCoreStates.add(stats); + } + return remote; + } + + public static CpuStateSnapshot toLocal(RemoteStat remote) { + CpuStateSnapshot local = new CpuStateSnapshot(); + local.procCpuCoreStates = new ArrayList<>(remote.procCpuCoreStates.size()); + for (long[] item : remote.procCpuCoreStates) { + local.procCpuCoreStates.add(ListEntry.ofDigits(item)); + } + return local; + } + + public static class RemoteStat implements Parcelable { + public List procCpuCoreStates = Collections.emptyList(); + + public RemoteStat() { + } + + protected RemoteStat(Parcel in) { + int size = in.readInt(); + procCpuCoreStates = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + procCpuCoreStates.add(in.createLongArray()); + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + int numOfArrays = procCpuCoreStates.size(); + dest.writeInt(numOfArrays); + for (int i = 0; i < numOfArrays; i++) { + dest.writeLongArray(procCpuCoreStates.get(i)); + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public RemoteStat createFromParcel(Parcel in) { + return new RemoteStat(in); + } + @Override + public RemoteStat[] newArray(int size) { + return new RemoteStat[size]; + } + }; + } + } + } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/DeviceStatMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/DeviceStatMonitorFeature.java index 570c3e697..a22c3b581 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/DeviceStatMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/DeviceStatMonitorFeature.java @@ -5,6 +5,7 @@ import android.content.Intent; import com.tencent.matrix.batterycanary.BatteryEventDelegate; +import com.tencent.matrix.batterycanary.monitor.AppStats; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Differ.DigitDiffer; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Differ.ListDiffer; @@ -74,14 +75,7 @@ public void onTurnOn() { @SuppressLint("VisibleForTests") @Override public void accept(Integer integer) { - BatteryCanaryUtil.getProxy().updateDevStat(integer); - synchronized (TAG) { - if (mStampList != Collections.EMPTY_LIST) { - MatrixLog.i(BatteryEventDelegate.TAG, "onStat >> " + BatteryCanaryUtil.convertDevStat(integer)); - mStampList.add(0, new TimeBreaker.Stamp(String.valueOf(integer))); - checkOverHeat(); - } - } + onStatDevStat(integer); } }); @@ -90,6 +84,17 @@ public void accept(Integer integer) { } } + public void onStatDevStat(int devStat) { + BatteryCanaryUtil.getProxy().updateDevStat(devStat); + synchronized (TAG) { + if (mStampList != Collections.EMPTY_LIST) { + MatrixLog.i(BatteryEventDelegate.TAG, "onStat >> " + BatteryCanaryUtil.convertDevStat(devStat)); + mStampList.add(0, new TimeBreaker.Stamp(String.valueOf(devStat))); + checkOverHeat(); + } + } + } + @Override public void onTurnOff() { super.onTurnOff(); @@ -112,13 +117,18 @@ public int weight() { } public CpuFreqSnapshot currentCpuFreq() { - CpuFreqSnapshot snapshot = new CpuFreqSnapshot(); try { - snapshot.cpuFreqs = Snapshot.Entry.ListEntry.ofDigits(BatteryCanaryUtil.getCpuCurrentFreq()); + int[] cpuFreqs = BatteryCanaryUtil.getCpuCurrentFreq(); + return currentCpuFreq(cpuFreqs); } catch (Throwable e) { MatrixLog.printErrStackTrace(TAG, e, "#currentCpuFreq error"); - snapshot.cpuFreqs = Snapshot.Entry.ListEntry.ofDigits(new int[]{}); + return currentCpuFreq(new int[]{}); } + } + + public CpuFreqSnapshot currentCpuFreq(int[] cpuFreqs) { + CpuFreqSnapshot snapshot = new CpuFreqSnapshot(); + snapshot.cpuFreqs = Snapshot.Entry.ListEntry.ofDigits(cpuFreqs); return snapshot; } @@ -163,6 +173,30 @@ public List getStampList() { return new ArrayList<>(mStampList); } + public ThermalStatSnapshot currentThermalStat(Context context) { + ThermalStatSnapshot snapshot = new ThermalStatSnapshot(); + snapshot.stat = Snapshot.Entry.DigitEntry.of(BatteryCanaryUtil.getThermalStat(context)); + return snapshot; + } + + public ThermalHeadroomSnapshot currentThermalHeadroom(Context context, int forecastSeconds) { + ThermalHeadroomSnapshot snapshot = new ThermalHeadroomSnapshot(); + snapshot.stat = Snapshot.Entry.DigitEntry.of(BatteryCanaryUtil.getThermalHeadroom(context, forecastSeconds)); + return snapshot; + } + + public ChargeWattageSnapshot currentChargeWattage(Context context) { + ChargeWattageSnapshot snapshot = new ChargeWattageSnapshot(); + snapshot.stat = Snapshot.Entry.DigitEntry.of(BatteryCanaryUtil.getChargingWatt(context)); + return snapshot; + } + + public BatteryCurrentSnapshot currentBatteryCurrency(Context context) { + BatteryCurrentSnapshot snapshot = new BatteryCurrentSnapshot(); + snapshot.stat = Snapshot.Entry.DigitEntry.of(BatteryCanaryUtil.getBatteryCurrencyImmediately(context)); + return snapshot; + } + static final class DevStatListener { Consumer mListener = new Consumer() { @@ -197,20 +231,20 @@ public boolean onStateChanged(String event) { switch (event) { case Intent.ACTION_POWER_CONNECTED: mIsCharging = true; - mListener.accept(1); + mListener.accept(AppStats.DEV_STAT_CHARGING); break; case Intent.ACTION_POWER_DISCONNECTED: mIsCharging = false; - mListener.accept(2); + mListener.accept(AppStats.DEV_STAT_UN_CHARGING); break; case Intent.ACTION_SCREEN_ON: if (!mIsCharging) { - mListener.accept(2); + mListener.accept(AppStats.DEV_STAT_SCREEN_ON); } break; case Intent.ACTION_SCREEN_OFF: if (!mIsCharging) { - mListener.accept(3); + mListener.accept(AppStats.DEV_STAT_SCREEN_OFF); } break; default: @@ -277,6 +311,71 @@ protected BatteryTmpSnapshot computeDelta() { } } + public static class ThermalStatSnapshot extends Snapshot { + public Entry.DigitEntry stat; + + @Override + public Delta diff(ThermalStatSnapshot bgn) { + return new Delta(bgn, this) { + @Override + protected ThermalStatSnapshot computeDelta() { + ThermalStatSnapshot delta = new ThermalStatSnapshot(); + delta.stat = DigitDiffer.globalDiff(bgn.stat, end.stat); + return delta; + } + }; + } + } + + public static class ThermalHeadroomSnapshot extends Snapshot { + public Entry.DigitEntry stat; + + @Override + public Delta diff(ThermalHeadroomSnapshot bgn) { + return new Delta(bgn, this) { + @Override + protected ThermalHeadroomSnapshot computeDelta() { + ThermalHeadroomSnapshot delta = new ThermalHeadroomSnapshot(); + delta.stat = DigitDiffer.globalDiff(bgn.stat, end.stat); + return delta; + } + }; + } + } + + public static class ChargeWattageSnapshot extends Snapshot { + public Entry.DigitEntry stat; + + @Override + public Delta diff(ChargeWattageSnapshot bgn) { + return new Delta(bgn, this) { + @Override + protected ChargeWattageSnapshot computeDelta() { + ChargeWattageSnapshot delta = new ChargeWattageSnapshot(); + delta.stat = DigitDiffer.globalDiff(bgn.stat, end.stat); + return delta; + } + }; + } + + } + + public static class BatteryCurrentSnapshot extends Snapshot { + public Entry.DigitEntry stat; + + @Override + public Delta diff(BatteryCurrentSnapshot bgn) { + return new Delta(bgn, this) { + @Override + protected BatteryCurrentSnapshot computeDelta() { + BatteryCurrentSnapshot delta = new BatteryCurrentSnapshot(); + delta.stat = DigitDiffer.globalDiff(bgn.stat, end.stat); + return delta; + } + }; + } + } + public static final class DevStatSnapshot extends Snapshot { public Entry.DigitEntry uptime = Entry.DigitEntry.of(0L); public Entry.DigitEntry chargingRatio = Entry.DigitEntry.of(0L); diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/JiffiesMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/JiffiesMonitorFeature.java index 7db8086fd..7f8cdb56b 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/JiffiesMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/JiffiesMonitorFeature.java @@ -1,19 +1,21 @@ package com.tencent.matrix.batterycanary.monitor.feature; +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Parcel; +import android.os.Parcelable; import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; -import androidx.annotation.AnyThread; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.annotation.WorkerThread; - import com.tencent.matrix.Matrix; -import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorConfig; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore.Callback; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.DigitEntry; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.ListEntry; +import com.tencent.matrix.batterycanary.shell.TopThreadFeature; +import com.tencent.matrix.batterycanary.shell.ui.TopThreadIndicator; import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.ProcStatUtil; import com.tencent.matrix.util.MatrixLog; @@ -26,9 +28,17 @@ import java.util.Comparator; import java.util.List; +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.WorkerThread; +import androidx.core.util.Pair; + @SuppressWarnings("NotNullFieldNotInitialized") public final class JiffiesMonitorFeature extends AbsMonitorFeature { private static final String TAG = "Matrix.battery.JiffiesMonitorFeature"; + private static boolean sSkipNewAdded = false; // FIXME: move to feature's configuration public interface JiffiesListener { @Deprecated @@ -45,6 +55,12 @@ protected String getTag() { return TAG; } + @Override + public void onTurnOn() { + super.onTurnOn(); + sSkipNewAdded = mCore.getConfig().isSkipNewAddedPidTid; + } + @Override public int weight() { return Integer.MAX_VALUE; @@ -75,8 +91,18 @@ public JiffiesSnapshot currentJiffiesSnapshot() { return JiffiesSnapshot.currentJiffiesSnapshot(ProcessInfo.getProcessInfo(), mCore.getConfig().isStatPidProc); } + @WorkerThread + public JiffiesSnapshot currentJiffiesSnapshot(int pid) { + return JiffiesSnapshot.currentJiffiesSnapshot(ProcessInfo.getProcessInfo(pid), mCore.getConfig().isStatPidProc); + } + + @WorkerThread + public UidJiffiesSnapshot currentUidJiffiesSnapshot() { + return UidJiffiesSnapshot.of(mCore.getContext(), mCore.getConfig()); + } + @AnyThread - public void currentJiffiesSnapshot(@NonNull final BatteryMonitorCore.Callback callback) { + public void currentJiffiesSnapshot(@NonNull final Callback callback) { mCore.getHandler().post(new Runnable() { @Override public void run() { @@ -85,6 +111,17 @@ public void run() { }); } + @AnyThread + public void currentJiffiesSnapshot(final int pid, @NonNull final Callback callback) { + mCore.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onGetJiffies(currentJiffiesSnapshot(pid)); + } + }); + } + + @SuppressWarnings("SpellCheckingInspection") @RestrictTo(RestrictTo.Scope.LIBRARY) public static class ProcessInfo { @@ -98,6 +135,19 @@ static ProcessInfo getProcessInfo() { return processInfo; } + static ProcessInfo getProcessInfo(int pid) { + if (pid == Process.myPid()) { + return getProcessInfo(); + } + ProcessInfo processInfo = new ProcessInfo(); + processInfo.pid = pid; + processInfo.name = Matrix.isInstalled() ? MatrixUtil.getProcessName(Matrix.with().getApplication()) : "default"; + processInfo.threadInfo = ThreadInfo.parseThreadsInfo(processInfo.pid); + processInfo.upTime = SystemClock.uptimeMillis(); + processInfo.time = System.currentTimeMillis(); + return processInfo; + } + int pid; String name; long time; @@ -231,12 +281,16 @@ public static JiffiesSnapshot currentJiffiesSnapshot(ProcessInfo processInfo, bo } public int pid; + public boolean isNewAdded; public String name; public DigitEntry totalJiffies; public ListEntry threadEntries; public DigitEntry threadNum; + public ListEntry deadThreadEntries; private JiffiesSnapshot() { + isNewAdded = false; + deadThreadEntries = ListEntry.ofEmpty(); } @Override @@ -246,11 +300,13 @@ public Delta diff(JiffiesSnapshot bgn) { protected JiffiesSnapshot computeDelta() { JiffiesSnapshot delta = new JiffiesSnapshot(); delta.pid = end.pid; + delta.isNewAdded = end.isNewAdded; delta.name = end.name; delta.totalJiffies = Differ.DigitDiffer.globalDiff(bgn.totalJiffies, end.totalJiffies); delta.threadNum = Differ.DigitDiffer.globalDiff(bgn.threadNum, end.threadNum); delta.threadEntries = ListEntry.ofEmpty(); + // for Existing threads if (end.threadEntries.getList().size() > 0) { List deltaThreadEntries = new ArrayList<>(); for (ThreadJiffiesSnapshot endRecord : end.threadEntries.getList()) { @@ -269,7 +325,10 @@ protected JiffiesSnapshot computeDelta() { deltaThreadJiffies.name = endRecord.name; deltaThreadJiffies.stat = endRecord.stat; deltaThreadJiffies.isNewAdded = isNewAdded; - deltaThreadEntries.add(deltaThreadJiffies); + if (!isNewAdded || !sSkipNewAdded) { + // Skip new added tid for now + deltaThreadEntries.add(deltaThreadJiffies); + } } } if (deltaThreadEntries.size() > 0) { @@ -285,6 +344,30 @@ public int compare(ThreadJiffiesSnapshot o1, ThreadJiffiesSnapshot o2) { delta.threadEntries = ListEntry.of(deltaThreadEntries); } } + + // for Dead threads + if (bgn.threadEntries.getList().size() > 0) { + List deadThreadEntries = Collections.emptyList(); + for (ThreadJiffiesSnapshot bgn : bgn.threadEntries.getList()) { + boolean isDead = true; + for (ThreadJiffiesSnapshot exist : delta.threadEntries.getList()) { + if (exist.tid == bgn.tid) { + isDead = false; + break; + } + } + if (isDead) { + if (deadThreadEntries.isEmpty()) { + deadThreadEntries = new ArrayList<>(); + } + deadThreadEntries.add(bgn); + } + } + if (!deadThreadEntries.isEmpty()) { + delta.deadThreadEntries = ListEntry.of(deadThreadEntries); + } + } + return delta; } }; @@ -323,6 +406,8 @@ public static class ThreadJiffiesEntry extends DigitEntry { public boolean isNewAdded; @NonNull public String stat; + @Nullable + public String stack; public ThreadJiffiesEntry(Long value) { super(value); @@ -338,6 +423,7 @@ public Long diff(Long right) { class ThreadWatchDog implements Runnable { private long duringMillis; private final List mWatchingThreads = new ArrayList<>(); + @Nullable private Handler mWatchHandler; @Override public void run() { @@ -361,14 +447,20 @@ public void run() { } // next loop - if (duringMillis <= 5 * 60 * 1000L) { - mCore.getHandler().postDelayed(this, setNext(5 * 60 * 1000L)); - } else if (duringMillis <= 10 * 60 * 1000L) { - mCore.getHandler().postDelayed(this, setNext(10 * 60 * 1000L)); - } else { - // done - synchronized (mWatchingThreads) { - mWatchingThreads.clear(); + synchronized (mWatchingThreads) { + if (duringMillis <= 5 * 60 * 1000L) { + if (mWatchHandler != null) { + mWatchHandler.postDelayed(this, setNext(5 * 60 * 1000L)); + } + } else if (duringMillis <= 10 * 60 * 1000L) { + if (mWatchHandler != null) { + mWatchHandler.postDelayed(this, setNext(10 * 60 * 1000L)); + } + } else { + // done + synchronized (mWatchingThreads) { + mWatchingThreads.clear(); + } } } } @@ -389,13 +481,22 @@ void start() { synchronized (mWatchingThreads) { MatrixLog.i(TAG, "ThreadWatchDog start watching, count = " + mWatchingThreads.size()); if (!mWatchingThreads.isEmpty()) { - mCore.getHandler().postDelayed(this, reset()); + HandlerThread handlerThread = new HandlerThread("matrix_watchdog"); + handlerThread.start(); + mWatchHandler = new Handler(handlerThread.getLooper()); + mWatchHandler.postDelayed(this, reset()); } } } void stop() { - mCore.getHandler().removeCallbacks(this); + synchronized (mWatchingThreads) { + if (mWatchHandler != null) { + mWatchHandler.removeCallbacks(this); + mWatchHandler.getLooper().quit(); + mWatchHandler = null; + } + } } private long reset() { @@ -409,4 +510,232 @@ private long setNext(long millis) { return millis; } } + + public static class UidJiffiesSnapshot extends Snapshot { + public static UidJiffiesSnapshot of(Context context, BatteryMonitorConfig config) { + UidJiffiesSnapshot curr = new UidJiffiesSnapshot(); + List> procList = TopThreadFeature.getProcList(context); + curr.pidCurrJiffiesList = new ArrayList<>(procList.size()); + long sum = 0; + MatrixLog.i(TAG, "currProcList: " + procList); + for (Pair item : procList) { + //noinspection ConstantConditions + int pid = item.first; + String procName = String.valueOf(item.second); + if (ProcStatUtil.exists(pid)) { + MatrixLog.i(TAG, "proc: " + pid); + JiffiesSnapshot snapshot = JiffiesSnapshot.currentJiffiesSnapshot(ProcessInfo.getProcessInfo(pid), config.isStatPidProc); + snapshot.name = TopThreadIndicator.getProcSuffix(procName); + sum += snapshot.totalJiffies.get(); + curr.pidCurrJiffiesList.add(snapshot); + } else { + if (config.ipcJiffiesCollector != null) { + IpcJiffies.IpcProcessJiffies ipcProcessJiffies = config.ipcJiffiesCollector.apply(item); + if (ipcProcessJiffies != null) { + MatrixLog.i(TAG, "ipc: " + pid); + JiffiesSnapshot snapshot = IpcJiffies.toLocal(ipcProcessJiffies); + snapshot.name = TopThreadIndicator.getProcSuffix(procName); + sum += snapshot.totalJiffies.get(); + curr.pidCurrJiffiesList.add(snapshot); + continue; + } + } + MatrixLog.i(TAG, "skip: " + pid); + } + } + curr.totalUidJiffies = DigitEntry.of(sum); + return curr; + } + + public DigitEntry totalUidJiffies = DigitEntry.of(0L); + public List pidCurrJiffiesList = Collections.emptyList(); + public List> pidDeltaJiffiesList = Collections.emptyList(); + + @Override + public Delta diff(UidJiffiesSnapshot bgn) { + return new Delta(bgn, this) { + @Override + protected UidJiffiesSnapshot computeDelta() { + UidJiffiesSnapshot delta = new UidJiffiesSnapshot(); + delta.totalUidJiffies = Differ.DigitDiffer.globalDiff(bgn.totalUidJiffies, end.totalUidJiffies); + if (end.pidCurrJiffiesList.size() > 0) { + delta.pidDeltaJiffiesList = new ArrayList<>(); + for (JiffiesSnapshot end : end.pidCurrJiffiesList) { + JiffiesSnapshot last = null; + for (JiffiesSnapshot bgn : bgn.pidCurrJiffiesList) { + if (bgn.pid == end.pid) { + last = bgn; + break; + } + } + if (last == null) { + // newAdded Pid + end.isNewAdded = true; + JiffiesSnapshot empty = new JiffiesSnapshot(); + empty.pid = end.pid; + empty.name = end.name; + empty.totalJiffies = DigitEntry.of(0L); + empty.threadEntries = ListEntry.ofEmpty(); + empty.threadNum = DigitEntry.of(0); + last = empty; + } + if (!end.isNewAdded || !sSkipNewAdded) { + // Skip new added pid for now + Delta deltaPidJiffies = end.diff(last); + delta.pidDeltaJiffiesList.add(deltaPidJiffies); + } + } + + Collections.sort(delta.pidDeltaJiffiesList, new Comparator>() { + @Override + public int compare(Delta o1, Delta o2) { + long minus = o1.dlt.totalJiffies.get() - o2.dlt.totalJiffies.get(); + if (minus == 0) return 0; + if (minus > 0) return -1; + return 1; + } + }); + } + return delta; + } + }; + } + + + public static final class IpcJiffies { + public static IpcProcessJiffies toIpc(JiffiesSnapshot local) { + IpcProcessJiffies ipc = new IpcProcessJiffies(); + ipc.pid = local.pid; + ipc.name = local.name; + ipc.threadNum = local.threadNum.get(); + ipc.totalJiffies = local.totalJiffies.get(); + ipc.threadJiffyList = new ArrayList<>(local.threadEntries.getList().size()); + for (JiffiesSnapshot.ThreadJiffiesSnapshot item : local.threadEntries.getList()) { + ipc.threadJiffyList.add(toIpc(item)); + } + return ipc; + } + + public static IpcProcessJiffies.IpcThreadJiffies toIpc(JiffiesSnapshot.ThreadJiffiesSnapshot local) { + IpcProcessJiffies.IpcThreadJiffies ipc = new IpcProcessJiffies.IpcThreadJiffies(); + ipc.tid = local.tid; + ipc.name = local.name; + ipc.stat = local.stat; + ipc.jiffies = local.get(); + return ipc; + } + + public static JiffiesSnapshot toLocal(IpcProcessJiffies ipc) { + JiffiesSnapshot local = new JiffiesSnapshot(); + local.pid = ipc.pid; + local.name = ipc.name; + local.totalJiffies = DigitEntry.of(ipc.totalJiffies); + List threadJiffiesList = Collections.emptyList(); + if (!ipc.threadJiffyList.isEmpty()) { + threadJiffiesList = new ArrayList<>(ipc.threadJiffyList.size()); + for (IpcProcessJiffies.IpcThreadJiffies item : ipc.threadJiffyList) { + JiffiesSnapshot.ThreadJiffiesSnapshot threadJiffies = toLocal(item); + threadJiffiesList.add(threadJiffies); + } + } + local.threadEntries = ListEntry.of(threadJiffiesList); + local.threadNum = DigitEntry.of(threadJiffiesList.size()); + return local; + } + + public static JiffiesSnapshot.ThreadJiffiesSnapshot toLocal(IpcProcessJiffies.IpcThreadJiffies ipc) { + JiffiesSnapshot.ThreadJiffiesSnapshot local = new JiffiesSnapshot.ThreadJiffiesSnapshot(ipc.jiffies); + local.name = ipc.name; + local.stat = ipc.stat; + local.tid = ipc.tid; + local.isNewAdded = true; + return local; + } + + public static class IpcProcessJiffies implements Parcelable { + public int pid; + public String name; + public long totalJiffies; + public int threadNum; + public List threadJiffyList; + + protected IpcProcessJiffies(Parcel in) { + pid = in.readInt(); + name = in.readString(); + totalJiffies = in.readLong(); + threadNum = in.readInt(); + threadJiffyList = in.createTypedArrayList(IpcThreadJiffies.CREATOR); + } + + public static final Creator CREATOR = new Creator() { + @Override + public IpcProcessJiffies createFromParcel(Parcel in) { + return new IpcProcessJiffies(in); + } + @Override + public IpcProcessJiffies[] newArray(int size) { + return new IpcProcessJiffies[size]; + } + }; + + public IpcProcessJiffies() { + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(pid); + dest.writeString(name); + dest.writeLong(totalJiffies); + dest.writeInt(threadNum); + dest.writeTypedList(threadJiffyList); + } + + public static class IpcThreadJiffies implements Parcelable { + public int tid; + public String name; + public String stat; + public long jiffies; + + protected IpcThreadJiffies(Parcel in) { + tid = in.readInt(); + name = in.readString(); + stat = in.readString(); + jiffies = in.readLong(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public IpcThreadJiffies createFromParcel(Parcel in) { + return new IpcThreadJiffies(in); + } + @Override + public IpcThreadJiffies[] newArray(int size) { + return new IpcThreadJiffies[size]; + } + }; + + public IpcThreadJiffies() { + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(tid); + dest.writeString(name); + dest.writeString(stat); + dest.writeLong(jiffies); + } + } + } + } + } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/LocationMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/LocationMonitorFeature.java index 794a3525d..4776a8baf 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/LocationMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/LocationMonitorFeature.java @@ -1,12 +1,12 @@ package com.tencent.matrix.batterycanary.monitor.feature; -import androidx.annotation.NonNull; import android.text.TextUtils; -import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.LocationManagerServiceHooker; import com.tencent.matrix.util.MatrixLog; +import androidx.annotation.NonNull; + public final class LocationMonitorFeature extends AbsMonitorFeature { private static final String TAG = "Matrix.battery.LocationMonitorFeature"; final LocationTracing mTracing = new LocationTracing(); @@ -24,7 +24,7 @@ public void onTurnOn() { mListener = new LocationManagerServiceHooker.IListener() { @Override public void onRequestLocationUpdates(long minTimeMillis, float minDistance) { - String stack = shouldTracing() ? BatteryCanaryUtil.stackTraceToString(new Throwable().getStackTrace()) : ""; + String stack = shouldTracing() ? mCore.getConfig().callStackCollector.collectCurr() : ""; MatrixLog.i(TAG, "#onRequestLocationUpdates, time = " + minTimeMillis + ", distance = " + minDistance + ", stack = " + stack); mTracing.setStack(stack); mTracing.onStartScan(); diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/LooperTaskMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/LooperTaskMonitorFeature.java index 73693817f..e71956f59 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/LooperTaskMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/LooperTaskMonitorFeature.java @@ -6,8 +6,10 @@ import android.os.Process; import android.text.TextUtils; +import com.tencent.matrix.batterycanary.BuildConfig; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; import com.tencent.matrix.trace.core.LooperMonitor; +import com.tencent.matrix.trace.listeners.ILooperListener; import com.tencent.matrix.util.MatrixLog; import java.util.ArrayList; @@ -37,7 +39,7 @@ public interface LooperTaskListener { final Map mLooperMonitorTrace = new HashMap<>(); @Nullable - LooperMonitor.LooperDispatchListener mLooperTaskListener; + ILooperListener mLooperTaskListener; @Nullable Runnable mDelayWatchingTask; @@ -53,15 +55,14 @@ LooperTaskListener getListener() { @Override public void onTurnOn() { super.onTurnOn(); - mLooperTaskListener = new LooperMonitor.LooperDispatchListener() { + mLooperTaskListener = new ILooperListener() { @Override public boolean isValid() { return mCore.isTurnOn(); } @Override - public void onDispatchStart(String x) { - super.onDispatchStart(x); + public void onDispatchBegin(String x) { if (mCore.getConfig().isAggressiveMode) { MatrixLog.i(TAG, "[" + Thread.currentThread().getName() + "]" + x); } @@ -75,8 +76,7 @@ public void onDispatchStart(String x) { } @Override - public void onDispatchEnd(String x) { - super.onDispatchEnd(x); + public void onDispatchEnd(String x, long beginNs, long endNs) { if (mCore.getConfig().isAggressiveMode) { MatrixLog.i(TAG, "[" + Thread.currentThread().getName() + "]" + x); } @@ -310,6 +310,11 @@ protected void onConcurrentOverHeat(String key, int concurrentCount, long during protected void onParseTaskJiffiesFail(String key, int pid, int tid) { } + @Override + protected boolean shouldTraceTask(Snapshot.Delta delta) { + return BuildConfig.DEBUG || delta.during > 10L; + } + @Deprecated public static class TaskTraceInfo { diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeature.java index d05a18915..176d4b8ab 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/MonitorFeature.java @@ -5,6 +5,7 @@ import android.os.SystemClock; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.utils.Function; import com.tencent.matrix.util.MatrixLog; import java.util.ArrayList; @@ -67,11 +68,29 @@ public Delta(RECORD bgn, RECORD end) { dlt.isDelta = true; } + public Delta(RECORD bgn, RECORD end, RECORD dlt) { + this.bgn = bgn; + this.end = end; + this.during = end.time - bgn.time; + this.dlt = dlt; + dlt.isDelta = true; + } + public boolean isValid() { return bgn.isValid() && end.isValid(); } protected abstract RECORD computeDelta(); + + public static class SimpleDelta extends Delta { + public SimpleDelta(RECORD bgn, RECORD end, RECORD dlt) { + super(bgn, end, dlt); + } + @Override + protected RECORD computeDelta() { + throw new RuntimeException("stub!"); + } + } } public abstract static class Entry { @@ -419,28 +438,33 @@ public Entry.ListEntry diff(@NonNull Entry.ListEntry bgn, @NonNull public static class Sampler { private static final String TAG = "Matrix.battery.Sampler"; + public static final Integer INVALID = Integer.MIN_VALUE; + final String mTag; final Handler mHandler; - final Callable mSamplingBlock; + final Function mSamplingBlock; private final Runnable mSamplingTask = new Runnable() { @Override public void run() { try { - Number currSample = mSamplingBlock.call(); - mSampleLst = currSample.doubleValue(); - mCount++; - mSampleAvg = (mSampleAvg * (mCount - 1) + mSampleLst) / mCount; - if (mSampleFst == Double.MIN_VALUE) { - mSampleFst = mSampleLst; - mSampleMax = mSampleLst; - mSampleMin = mSampleLst; - } else { - if (mSampleLst > mSampleMax) { + Number currSample = mSamplingBlock.apply(Sampler.this); + if (!currSample.equals(INVALID)) { + mSampleLst = currSample.doubleValue(); + mCount++; + // FIXME: calc vag on finished + mSampleAvg = (mSampleAvg * (mCount - 1) + mSampleLst) / mCount; + if (mSampleFst == Double.MIN_VALUE) { + mSampleFst = mSampleLst; mSampleMax = mSampleLst; - } - if (mSampleLst < mSampleMin) { mSampleMin = mSampleLst; + } else { + if (mSampleLst > mSampleMax) { + mSampleMax = mSampleLst; + } + if (mSampleLst < mSampleMin) { + mSampleMin = mSampleLst; + } } } } catch (Exception e) { @@ -465,10 +489,38 @@ public void run() { double mSampleAvg = Double.MIN_VALUE; public Sampler(Handler handler, Callable onSampling) { + this("dft", handler, onSampling); + } + + public Sampler(String tag, Handler handler, final Callable onSampling) { + mTag = tag; + mHandler = handler; + mSamplingBlock = new Function() { + @Override + public Number apply(Sampler sampler) { + try { + return onSampling.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + } + + public Sampler(String tag, Handler handler, Function onSampling) { + mTag = tag; mHandler = handler; mSamplingBlock = onSampling; } + public String getTag() { + return mTag; + } + + public int getCount() { + return mCount; + } + public void setInterval(long interval) { if (interval > 0) { mInterval = interval; @@ -518,6 +570,20 @@ public static final class Result { public double sampleMax; public double sampleMin; public double sampleAvg; + + @Override + public String toString() { + return "Result{" + + "interval=" + interval + + ", count=" + count + + ", duringMillis=" + duringMillis + + ", sampleFst=" + sampleFst + + ", sampleLst=" + sampleLst + + ", sampleMax=" + sampleMax + + ", sampleMin=" + sampleMin + + ", sampleAvg=" + sampleAvg + + '}'; + } } } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/NotificationMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/NotificationMonitorFeature.java index ae0674c39..c196bf295 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/NotificationMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/NotificationMonitorFeature.java @@ -7,7 +7,6 @@ import android.os.SystemClock; import android.text.TextUtils; -import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.NotificationManagerServiceHooker; import com.tencent.matrix.batterycanary.utils.NotificationManagerServiceHooker.IListener; import com.tencent.matrix.util.MatrixLog; @@ -115,7 +114,7 @@ public int weight() { @VisibleForTesting void checkNotifyContent(@Nullable final String title, @Nullable final String text) { final long bgMillis = mLastBgStartMillis > 0 ? SystemClock.uptimeMillis() - mLastBgStartMillis : 0; - final String stack = mCore.getConfig().isAggressiveMode ? BatteryCanaryUtil.stackTraceToString(new Throwable().getStackTrace()) : ""; + final String stack = mCore.getConfig().isAggressiveMode ? mCore.getConfig().callStackCollector.collectCurr() : ""; mCore.getHandler().post(new Runnable() { @Override diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/TrafficMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/TrafficMonitorFeature.java index b63877e0f..209760e52 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/TrafficMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/TrafficMonitorFeature.java @@ -35,19 +35,45 @@ public TrafficMonitorFeature.RadioStatSnapshot currentRadioSnapshot(Context cont if (stat == null) { return null; } + TrafficMonitorFeature.RadioStatSnapshot snapshot = new TrafficMonitorFeature.RadioStatSnapshot(); snapshot.wifiRxBytes = Snapshot.Entry.DigitEntry.of(stat.wifiRxBytes); snapshot.wifiTxBytes = Snapshot.Entry.DigitEntry.of(stat.wifiTxBytes); + snapshot.wifiRxPackets = Snapshot.Entry.DigitEntry.of(stat.wifiRxPackets); + snapshot.wifiTxPackets = Snapshot.Entry.DigitEntry.of(stat.wifiTxPackets); + snapshot.mobileRxBytes = Snapshot.Entry.DigitEntry.of(stat.mobileRxBytes); snapshot.mobileTxBytes = Snapshot.Entry.DigitEntry.of(stat.mobileTxBytes); + snapshot.mobileRxPackets = Snapshot.Entry.DigitEntry.of(stat.mobileRxPackets); + snapshot.mobileTxPackets = Snapshot.Entry.DigitEntry.of(stat.mobileTxPackets); + return snapshot; + } + + @Nullable + public TrafficMonitorFeature.RadioBpsSnapshot currentRadioBpsSnapshot(Context context) { + RadioStatUtil.RadioBps stat = RadioStatUtil.getCurrentBps(context); + if (stat == null) { + return null; + } + + TrafficMonitorFeature.RadioBpsSnapshot snapshot = new TrafficMonitorFeature.RadioBpsSnapshot(); + snapshot.wifiRxBps = Snapshot.Entry.DigitEntry.of(stat.wifiRxBps); + snapshot.wifiTxBps = Snapshot.Entry.DigitEntry.of(stat.wifiTxBps); + snapshot.mobileRxBps = Snapshot.Entry.DigitEntry.of(stat.mobileRxBps); + snapshot.mobileTxBps = Snapshot.Entry.DigitEntry.of(stat.mobileTxBps); return snapshot; } public static class RadioStatSnapshot extends Snapshot { public Entry.DigitEntry wifiRxBytes = Entry.DigitEntry.of(0L); public Entry.DigitEntry wifiTxBytes = Entry.DigitEntry.of(0L); + public Entry.DigitEntry wifiRxPackets = Entry.DigitEntry.of(0L); + public Entry.DigitEntry wifiTxPackets = Entry.DigitEntry.of(0L); + public Entry.DigitEntry mobileRxBytes = Entry.DigitEntry.of(0L); public Entry.DigitEntry mobileTxBytes = Entry.DigitEntry.of(0L); + public Entry.DigitEntry mobileRxPackets = Entry.DigitEntry.of(0L); + public Entry.DigitEntry mobileTxPackets = Entry.DigitEntry.of(0L); @Override public Delta diff(RadioStatSnapshot bgn) { @@ -57,8 +83,35 @@ protected RadioStatSnapshot computeDelta() { RadioStatSnapshot delta = new RadioStatSnapshot(); delta.wifiRxBytes = DigitDiffer.globalDiff(bgn.wifiRxBytes, end.wifiRxBytes); delta.wifiTxBytes = DigitDiffer.globalDiff(bgn.wifiTxBytes, end.wifiTxBytes); + delta.wifiRxPackets = DigitDiffer.globalDiff(bgn.wifiRxPackets, end.wifiRxPackets); + delta.wifiTxPackets = DigitDiffer.globalDiff(bgn.wifiTxPackets, end.wifiTxPackets); + delta.mobileRxBytes = DigitDiffer.globalDiff(bgn.mobileRxBytes, end.mobileRxBytes); delta.mobileTxBytes = DigitDiffer.globalDiff(bgn.mobileTxBytes, end.mobileTxBytes); + delta.mobileRxPackets = DigitDiffer.globalDiff(bgn.mobileRxPackets, end.mobileRxPackets); + delta.mobileTxPackets = DigitDiffer.globalDiff(bgn.mobileTxPackets, end.mobileTxPackets); + return delta; + } + }; + } + } + + public static class RadioBpsSnapshot extends Snapshot { + public Entry.DigitEntry wifiRxBps = Entry.DigitEntry.of(0L); + public Entry.DigitEntry wifiTxBps = Entry.DigitEntry.of(0L); + public Entry.DigitEntry mobileRxBps = Entry.DigitEntry.of(0L); + public Entry.DigitEntry mobileTxBps = Entry.DigitEntry.of(0L); + + @Override + public Delta diff(RadioBpsSnapshot bgn) { + return new Delta(bgn, this) { + @Override + protected RadioBpsSnapshot computeDelta() { + RadioBpsSnapshot delta = new RadioBpsSnapshot(); + delta.wifiRxBps = DigitDiffer.globalDiff(bgn.wifiRxBps, end.wifiRxBps); + delta.wifiTxBps = DigitDiffer.globalDiff(bgn.wifiTxBps, end.wifiTxBps); + delta.mobileRxBps = DigitDiffer.globalDiff(bgn.mobileRxBps, end.mobileRxBps); + delta.mobileTxBps = DigitDiffer.globalDiff(bgn.mobileTxBps, end.mobileTxBps); return delta; } }; diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/WakeLockMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/WakeLockMonitorFeature.java index cbcc33c16..c34a49260 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/WakeLockMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/WakeLockMonitorFeature.java @@ -4,13 +4,9 @@ import android.os.IBinder; import android.os.SystemClock; import android.os.WorkSource; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.BeanEntry; -import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.PowerManagerServiceHooker; import com.tencent.matrix.util.MatrixLog; @@ -18,6 +14,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + @SuppressWarnings("NotNullFieldNotInitialized") public final class WakeLockMonitorFeature extends AbsMonitorFeature { private static final String TAG = "Matrix.battery.WakeLockMonitorFeature"; @@ -58,7 +58,7 @@ public void onTurnOn() { mListener = new PowerManagerServiceHooker.IListener() { @Override public void onAcquireWakeLock(IBinder token, int flags, String tag, String packageName, @Nullable WorkSource workSource, @Nullable String historyTag) { - String stack = shouldTracing(tag) ? BatteryCanaryUtil.stackTraceToString(new Throwable().getStackTrace()) : ""; + String stack = shouldTracing(tag) ? mCore.getConfig().callStackCollector.collectCurr() : ""; MatrixLog.i(TAG, "[onAcquireWakeLock] token=%s flags=%s tag=%s historyTag=%s packageName=%s workSource=%s stack=%s", String.valueOf(token), flags, tag, historyTag, packageName, workSource, stack); @@ -111,7 +111,7 @@ public void onReleaseWakeLock(IBinder token, int flags) { wakeLockTrace.finish(mCore.getHandler()); mWakeLockTracing.add(wakeLockTrace.record); String tag = wakeLockTrace.record.tag; - String stack = shouldTracing(tag) ? BatteryCanaryUtil.stackTraceToString(new Throwable().getStackTrace()) : ""; + String stack = shouldTracing(tag) ? mCore.getConfig().callStackCollector.collectCurr() : ""; MatrixLog.i(TAG, "[onReleaseWakeLock] tag = " + tag + ", stack = " + stack); // dump tracing info diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/WifiMonitorFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/WifiMonitorFeature.java index 6955d2699..5aef27a1a 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/WifiMonitorFeature.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/monitor/feature/WifiMonitorFeature.java @@ -1,12 +1,12 @@ package com.tencent.matrix.batterycanary.monitor.feature; -import androidx.annotation.NonNull; import android.text.TextUtils; -import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; import com.tencent.matrix.batterycanary.utils.WifiManagerServiceHooker; import com.tencent.matrix.util.MatrixLog; +import androidx.annotation.NonNull; + public final class WifiMonitorFeature extends AbsMonitorFeature { private static final String TAG = "Matrix.battery.WifiMonitorFeature"; final WifiTracing mTracing = new WifiTracing(); @@ -24,7 +24,7 @@ public void onTurnOn() { mListener = new WifiManagerServiceHooker.IListener() { @Override public void onStartScan() { - String stack = shouldTracing() ? BatteryCanaryUtil.stackTraceToString(new Throwable().getStackTrace()) : ""; + String stack = shouldTracing() ? mCore.getConfig().callStackCollector.collectCurr() : ""; MatrixLog.i(TAG, "#onStartScan, stack = " + stack); mTracing.setStack(stack); mTracing.onStartScan(); @@ -32,7 +32,7 @@ public void onStartScan() { @Override public void onGetScanResults() { - String stack = shouldTracing() ? BatteryCanaryUtil.stackTraceToString(new Throwable().getStackTrace()) : ""; + String stack = shouldTracing() ? mCore.getConfig().callStackCollector.collectCurr() : ""; MatrixLog.i(TAG, "#onGetScanResults, stack = " + stack); mTracing.setStack(stack); mTracing.onGetScanResults(); diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/shell/TopThreadFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/shell/TopThreadFeature.java new file mode 100644 index 000000000..840453574 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/shell/TopThreadFeature.java @@ -0,0 +1,205 @@ +package com.tencent.matrix.batterycanary.shell; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.util.SparseArray; + +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCallback.BatteryPrinter.Printer; +import com.tencent.matrix.batterycanary.monitor.feature.AbsMonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.CompositeMonitors; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import androidx.core.util.Supplier; + +/** + * Something like 'adb shell top -Hb -u ' + * + * @author Kaede + * @since 2022/2/22 + */ +public class TopThreadFeature extends AbsMonitorFeature { + private static final String TAG = "Matrix.battery.TopThread"; + private boolean sStopShell; + + public interface ContinuousCallback { + boolean onGetDeltas(CompositeMonitors monitors, long windowMillis); + } + + @Nullable private Runnable mTopTask; + private final SparseArray mLastPidJiffiesHolder = new SparseArray<>(); + + @Override + protected String getTag() { + return TAG; + } + + @Override + public int weight() { + return Integer.MIN_VALUE; + } + + public void topShell(final int seconds) { + sStopShell = false; + top(seconds, new Supplier() { + @Override + public CompositeMonitors get() { + CompositeMonitors monitors = new CompositeMonitors(mCore, CompositeMonitors.SCOPE_TOP_SHELL); + monitors.metric(JiffiesMonitorFeature.UidJiffiesSnapshot.class); + return monitors; + } + }, new ContinuousCallback() { + @Override + public boolean onGetDeltas(CompositeMonitors monitors, long windowMillis) { + // Proc Load + List> deltaList = monitors.getAllPidDeltaList(); + long allProcJiffies = 0; + for (Delta delta : deltaList) { + allProcJiffies += delta.dlt.totalJiffies.get(); + } + float totalLoad = figureCupLoad(allProcJiffies, seconds * 100L); + + Printer printer = new Printer(); + printer.writeTitle(); + printer.append("| TOP Thread\tpidNum=").append(deltaList.size()) + .append("\tcpuLoad=").append(formatFloat(totalLoad, 1)).append("%") + .append("\n"); + + // Thread Load + for (Delta delta : deltaList) { + if (delta.isValid()) { + printer.createSection("Proc"); + printer.writeLine("pid", String.valueOf(delta.dlt.pid)); + printer.writeLine("cmm", String.valueOf(delta.dlt.name)); + printer.writeLine("load", formatFloat(figureCupLoad(delta.dlt.totalJiffies.get(), seconds * 100L), 1) + "%"); + printer.createSubSection("Thread(" + delta.dlt.threadEntries.getList().size() + ")"); + printer.writeLine(" TID\tLOAD \tSTATUS \tTHREAD_NAME \tJIFFY"); + for (JiffiesSnapshot.ThreadJiffiesEntry threadJiffies : delta.dlt.threadEntries.getList()) { + long entryJffies = threadJiffies.get(); + printer.append("| -> ") + .append(fixedColumn(String.valueOf(threadJiffies.tid), 5)).append("\t") + .append(fixedColumn(formatFloat(figureCupLoad(entryJffies, seconds * 100L), 1), 4)).append("\t") + .append(threadJiffies.isNewAdded ? "+" : "~").append("/").append(threadJiffies.stat).append("\t") + .append(fixedColumn(threadJiffies.name, 16)).append("\t") + .append(entryJffies).append("\t") + .append("\n"); + } + } + } + + printer.writeEnding(); + printer.dump(); + return sStopShell; + } + }); + } + + public void stopShell() { + sStopShell = true; + if (mTopTask != null) { + mCore.getHandler().removeCallbacks(mTopTask); + mTopTask = null; + } + mLastPidJiffiesHolder.clear(); + } + + + public void top(final int seconds, final Supplier supplier, final ContinuousCallback callback) { + final JiffiesMonitorFeature jiffiesFeat = mCore.getMonitorFeature(JiffiesMonitorFeature.class); + if (jiffiesFeat == null) { + return; + } + final long windowMillis = seconds * 1000L; + final AtomicReference lastMonitors = new AtomicReference<>(null); + HandlerThread thread = new HandlerThread("matrix_top"); + thread.start(); + final Handler handler = new Handler(thread.getLooper()); + final Runnable action = new Runnable() { + @Override + public void run() { + CompositeMonitors monitors = lastMonitors.get(); + if (monitors == null) { + // Fist time + scheduleNext(); + + } else { + lastMonitors.set(null); + monitors.finish(); + boolean stop = callback.onGetDeltas(monitors, windowMillis); + if (stop) { + handler.getLooper().quit(); + } else { + // Next + scheduleNext(); + } + } + } + + private void scheduleNext() { + CompositeMonitors monitors = supplier.get(); + monitors.start(); + lastMonitors.set(monitors); + handler.postDelayed(this, windowMillis); + } + }; + handler.postDelayed(action, windowMillis); + } + + static List getAllPidList(Context context) { + ArrayList list = new ArrayList<>(); + list.add(Process.myPid()); + List> procList = getProcList(context); + for (Pair item : procList) { + if (!list.contains(item.first)) { + list.add(item.first); + } + } + return list; + } + + public static List> getProcList(Context context) { + ArrayList> list = new ArrayList<>(); + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + List processes = am.getRunningAppProcesses(); + if (processes != null) { + for (ActivityManager.RunningAppProcessInfo item : processes) { + if (item.processName.contains(context.getPackageName())) { + list.add(new Pair<>(item.pid, item.processName)); + } + } + } + } + return list; + } + + public static float figureCupLoad(long jiffies, long cpuJiffies) { + return (jiffies / (cpuJiffies * 1f)) * 100; + } + + public static String formatFloat(float input, int decimal) { + return String.format("%." + decimal + "f", input); + } + + public static String fixedColumn(String input, int width) { + if (input != null && input.length() >= width) { + return input; + } + return repeat(" ", width - (input == null ? 0 : input.length())) + input; + } + + @SuppressWarnings("SameParameterValue") + static String repeat(String symbol, int count) { + return new String(new char[count]).replace("\0", symbol); + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/shell/ui/TopThreadIndicator.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/shell/ui/TopThreadIndicator.java new file mode 100644 index 000000000..3c7efe83c --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/shell/ui/TopThreadIndicator.java @@ -0,0 +1,830 @@ +package com.tencent.matrix.batterycanary.shell.ui; + +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.SparseBooleanArray; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import com.tencent.matrix.batterycanary.BatteryCanary; +import com.tencent.matrix.batterycanary.R; +import com.tencent.matrix.batterycanary.monitor.AppStats; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCallback; +import com.tencent.matrix.batterycanary.monitor.BatteryMonitorCore; +import com.tencent.matrix.batterycanary.monitor.feature.CompositeMonitors; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature; +import com.tencent.matrix.batterycanary.monitor.feature.DeviceStatMonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Sampler.Result; +import com.tencent.matrix.batterycanary.monitor.feature.TrafficMonitorFeature; +import com.tencent.matrix.batterycanary.shell.TopThreadFeature; +import com.tencent.matrix.batterycanary.shell.TopThreadFeature.ContinuousCallback; +import com.tencent.matrix.batterycanary.stats.BatteryStatsFeature; +import com.tencent.matrix.batterycanary.stats.HealthStatsFeature; +import com.tencent.matrix.batterycanary.stats.HealthStatsHelper; +import com.tencent.matrix.batterycanary.stats.ui.BatteryStatsActivity; +import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; +import com.tencent.matrix.batterycanary.utils.CallStackCollector; +import com.tencent.matrix.batterycanary.utils.Consumer; +import com.tencent.matrix.util.MatrixLog; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.UiThread; +import androidx.core.util.Pair; +import androidx.core.util.Supplier; + +import static com.tencent.matrix.batterycanary.shell.TopThreadFeature.figureCupLoad; +import static com.tencent.matrix.batterycanary.shell.TopThreadFeature.fixedColumn; +import static com.tencent.matrix.batterycanary.shell.TopThreadFeature.formatFloat; + +/** + * See {@link TopThreadFeature}. + * + * @author Kaede + * @since 2022/2/23 + */ +final public class TopThreadIndicator { + private static final String TAG = "Matrix.TopThreadIndicator"; + private static final int MAX_PROC_NUM = 10; + private static final int MAX_THREAD_NUM = 10; + private static final int MAX_POWER_NUM = 30; + @SuppressLint("StaticFieldLeak") + private static final TopThreadIndicator sInstance = new TopThreadIndicator(); + + private final DisplayMetrics displayMetrics = new DisplayMetrics(); + private final Handler mUiHandler = new Handler(Looper.getMainLooper()); + private final SparseBooleanArray mRunningRef = new SparseBooleanArray(); + @SuppressLint("RestrictedApi") + private Pair mCurrProc = new Pair<>(Process.myPid(), getProcSuffix(BatteryCanaryUtil.getProcessName())); + + @Nullable + private View mRootView; + @Nullable + private BatteryMonitorCore mCore; + @Nullable + private Delta mCurrDelta; + private boolean mShowPower = false; + @NonNull + private CallStackCollector mCollector = new CallStackCollector(); + @NonNull + private Consumer> mDumpHandler = new Consumer>() { + @Override + public void accept(final Delta delta) { + if (delta == null) { + if (mRootView != null) { + Toast.makeText(mRootView.getContext(), "Skip dump: no data", Toast.LENGTH_SHORT).show(); + } + return; + } + if (delta.dlt.pid != Process.myPid()) { + if (mRootView != null) { + Toast.makeText(mRootView.getContext(), "Skip dump: only support curr process now", Toast.LENGTH_SHORT).show(); + } + return; + } + + String tag = "TOP_THREAD_DUMP"; + BatteryMonitorCallback.BatteryPrinter.Printer printer = new BatteryMonitorCallback.BatteryPrinter.Printer(); + printer.writeTitle(); + printer.append("| " + tag + "\n"); + if (delta.isValid()) { + final Map extras = new LinkedHashMap<>(); + float cpuLoad = figureCupLoad(delta.dlt.totalJiffies.get(), delta.during / 10); + extras.put("load", cpuLoad); + // Load + printer.createSection("Proc"); + printer.writeLine("pid", String.valueOf(delta.dlt.pid)); + printer.writeLine("cmm", String.valueOf(delta.dlt.name)); + printer.writeLine("load", formatFloat(cpuLoad, 1) + "%"); + printer.createSubSection("Thread(" + delta.dlt.threadEntries.getList().size() + ")"); + printer.writeLine(" TID\tLOAD \tSTATUS \tTHREAD_NAME \tJIFFY"); + for (JiffiesSnapshot.ThreadJiffiesEntry threadJiffies : delta.dlt.threadEntries.getList()) { + long entryJffies = threadJiffies.get(); + printer.append("| -> ") + .append(fixedColumn(String.valueOf(threadJiffies.tid), 5)).append("\t") + .append(fixedColumn(formatFloat(figureCupLoad(entryJffies, delta.during / 10), 1) + "%", 4)).append("\t") + .append(threadJiffies.isNewAdded ? "+" : "~").append("/").append(threadJiffies.stat).append("\t") + .append(fixedColumn(threadJiffies.name, 16)).append("\t") + .append(entryJffies).append("\t") + .append("\n"); + } + // Thread Stack + printer.createSection("Stacks"); + for (JiffiesSnapshot.ThreadJiffiesEntry threadJiffies : delta.dlt.threadEntries.getList()) { + long entryJffies = threadJiffies.get(); + float load = figureCupLoad(entryJffies, delta.during / 10); + if (load > 0) { + printer.createSubSection(threadJiffies.name + "(" + threadJiffies.tid + ")"); + String stack = mCollector.collect(threadJiffies.tid); + extras.put("stack_" + threadJiffies.name + "(" + threadJiffies.tid + ")", stack); + int idx = 0; + for (String line : stack.split("\n")) { + printer.append(idx == 0 ? "| -> " : "| ").append(line).append("\n"); + idx++; + } + } else { + break; + } + } + // Stats + if (mCore != null) { + BatteryStatsFeature stats = mCore.getMonitorFeature(BatteryStatsFeature.class); + if (stats != null) { + String event = "MATRIX_TOP_DUMP"; + int eventId = delta.dlt.pid; + stats.statsEvent(event, eventId, extras); + } + } + } else { + printer.createSection("Invalid data, ignore"); + } + printer.writeEnding(); + printer.dump(); + if (mRootView != null) { + Toast.makeText(mRootView.getContext(), "Dump finish, search TAG '" + tag + "' for detail", Toast.LENGTH_LONG).show(); + } + } + }; + + @NonNull + private Runnable mShowReportHandler = new Runnable() { + @Override + public void run() { + if (mCore == null) { + if (mRootView != null) { + String tag = "TOP_THREAD_DUMP"; + Toast.makeText(mRootView.getContext(), "Search TAG '" + tag + "' for detail report", Toast.LENGTH_LONG).show(); + } + return; + } + // Show battery stats report + Intent intent = new Intent(mCore.getContext(), BatteryStatsActivity.class); + if (!(mCore.getContext() instanceof Activity)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + mCore.getContext().startActivity(intent); + } + }; + + public static TopThreadIndicator instance() { + return sInstance; + } + + TopThreadIndicator() { + } + + public TopThreadIndicator attach(@NonNull BatteryMonitorCore core) { + mCore = core; + return this; + } + + public TopThreadIndicator attach(@NonNull Consumer> dumpHandler) { + mDumpHandler = dumpHandler; + return this; + } + + public TopThreadIndicator attach(@NonNull CallStackCollector collector) { + mCollector = collector; + return this; + } + + public TopThreadIndicator attach(@NonNull Runnable showReportHandler) { + mShowReportHandler = showReportHandler; + return this; + } + + public void requestPermission(Context context, int reqCode) { + if (checkPermission(context)) { + return; + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName())); + if (reqCode > 0 && context instanceof Activity) { + ((Activity) context).startActivityForResult(intent, reqCode); + } else { + context.startActivity(intent); + } + } + } + + public boolean checkPermission(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return Settings.canDrawOverlays(context); + } else { + return true; + } + } + + @UiThread + public boolean isShowing() { + return mRootView != null; + } + + @UiThread + public boolean isRunning() { + return mRootView != null && mRunningRef.get(mRootView.hashCode(), false); + } + + + @SuppressLint("InflateParams") + @UiThread + public boolean show(Context context) { + if (!checkPermission(context)) { + return false; + } + + try { + // 1. Window View + mRootView = LayoutInflater.from(context).inflate(R.layout.float_top_thread_container, null); + if (mRootView == null) { + MatrixLog.w(TAG, "Can not load indicator view!"); + return false; + } + final int hashcode = mRootView.hashCode(); + final WindowManager windowManager = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics metrics = new DisplayMetrics(); + if (null != windowManager.getDefaultDisplay()) { + windowManager.getDefaultDisplay().getMetrics(displayMetrics); + windowManager.getDefaultDisplay().getMetrics(metrics); + } + + final WindowManager.LayoutParams layoutParam = new WindowManager.LayoutParams(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + layoutParam.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + layoutParam.type = WindowManager.LayoutParams.TYPE_PHONE; + } + layoutParam.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + layoutParam.gravity = Gravity.START | Gravity.TOP; + // if (null != rootView) { + // layoutParam.x = metrics.widthPixels - rootView.getLayoutParams().width * 2; + // } + layoutParam.y = 0; + layoutParam.width = WindowManager.LayoutParams.WRAP_CONTENT; + layoutParam.height = WindowManager.LayoutParams.WRAP_CONTENT; + layoutParam.format = PixelFormat.TRANSPARENT; + + windowManager.addView(mRootView, layoutParam); + + // 2. Init views + final TextView tvPid = mRootView.findViewById(R.id.tv_curr_pid); + tvPid.setText(String.valueOf(mCurrProc.first)); + final TextView tvProc = mRootView.findViewById(R.id.tv_proc); + tvProc.setText(mCurrProc.second); + + final CheckBox checkPower = mRootView.findViewById(R.id.check_power); + mShowPower = checkPower.isChecked(); + + // init thread entryGroup + LinearLayout procEntryGroup = mRootView.findViewById(R.id.layout_entry_proc_group); + for (int i = 0; i < MAX_PROC_NUM - 1; i++) { + View entryItemView = LayoutInflater.from(procEntryGroup.getContext()).inflate(R.layout.float_item_proc_entry, procEntryGroup, false); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, procEntryGroup.getContext().getResources().getDisplayMetrics()); + entryItemView.setVisibility(View.GONE); + procEntryGroup.addView(entryItemView, layoutParams); + } + // init thread entryGroup + LinearLayout threadEntryGroup = mRootView.findViewById(R.id.layout_entry_group); + for (int i = 0; i < MAX_THREAD_NUM - 1; i++) { + View entryItemView = LayoutInflater.from(threadEntryGroup.getContext()).inflate(R.layout.float_item_thread_entry, threadEntryGroup, false); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, threadEntryGroup.getContext().getResources().getDisplayMetrics()); + entryItemView.setVisibility(View.GONE); + threadEntryGroup.addView(entryItemView, layoutParams); + } + // init power entryGroup + LinearLayout powerEntryGroup = mRootView.findViewById(R.id.layout_entry_power_group); + for (int i = 0; i < MAX_POWER_NUM - 1; i++) { + View entryItemView = LayoutInflater.from(powerEntryGroup.getContext()).inflate(R.layout.float_item_power_entry, powerEntryGroup, false); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, powerEntryGroup.getContext().getResources().getDisplayMetrics()); + entryItemView.setVisibility(View.GONE); + powerEntryGroup.addView(entryItemView, layoutParams); + } + + // 3. Drag + View.OnTouchListener onTouchListener = new View.OnTouchListener() { + float downX = 0; + float downY = 0; + int downOffsetX = 0; + int downOffsetY = 0; + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(final View v, MotionEvent event) { + boolean consumed = false; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + downX = event.getX(); + downY = event.getY(); + downOffsetX = layoutParam.x; + downOffsetY = layoutParam.y; + break; + case MotionEvent.ACTION_MOVE: + float moveX = event.getX(); + float moveY = event.getY(); + layoutParam.x += (moveX - downX) / 3; + layoutParam.y += (moveY - downY) / 3; + if (v != null) { + windowManager.updateViewLayout(v, layoutParam); + } + break; + case MotionEvent.ACTION_UP: + PropertyValuesHolder holder = PropertyValuesHolder.ofInt( + "trans", + layoutParam.x, + layoutParam.x > (displayMetrics.widthPixels - mRootView.getWidth()) / 2 ? displayMetrics.widthPixels - mRootView.getWidth() : 0 + ); + ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder(holder); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (mRootView == null || mRootView.hashCode() != hashcode) { + return; + } + layoutParam.x = (int) animation.getAnimatedValue("trans"); + windowManager.updateViewLayout(v, layoutParam); + } + }); + animator.setInterpolator(new AccelerateInterpolator()); + animator.setDuration(180).start(); + + int upOffsetX = layoutParam.x; + int upOffsetY = layoutParam.y; + if (Math.abs(upOffsetX - downOffsetX) > 20 || Math.abs(upOffsetY - downOffsetY) > 20) { + consumed = true; + } + } + return consumed; + } + + }; + mRootView.setOnTouchListener(onTouchListener); + + // 4. Listener + View.OnClickListener listener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getId() == R.id.layout_dump) { + // Dump all threads + mDumpHandler.accept(mCurrDelta); + return; + } + if (v.getId() == R.id.layout_proc) { + // Choose proc + final TextView tvProc = mRootView.findViewById(R.id.tv_proc); + PopupMenu menu = new PopupMenu(v.getContext(), tvProc); + final List> procList = TopThreadFeature.getProcList(v.getContext()); + for (Pair item : procList) { + //noinspection ConstantConditions + menu.getMenu().add("Process :" + getProcSuffix(item.second)); + } + menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @SuppressLint("SetTextI18n") + @Override + public boolean onMenuItemClick(MenuItem item) { + String title = item.getTitle().toString(); + if (title.contains(":")) { + String proc = title.substring(title.lastIndexOf(":") + 1); + for (Pair procItem : procList) { + //noinspection ConstantConditions + if (title.equals("Process :" + getProcSuffix(procItem.second))) { + mCurrProc = procItem; + tvProc.setText(":" + proc); + tvPid.setText(String.valueOf(mCurrProc.first)); + } + } + } + return false; + } + }); + menu.show(); + return; + } + if (v.getId() == R.id.iv_logo) { + // Show dump report + mShowReportHandler.run(); + return; + } + if (v.getId() == R.id.tv_minify) { + // Minify + mRootView.findViewById(R.id.layout_top).setVisibility(View.GONE); + mRootView.findViewById(R.id.iv_logo_minify).setVisibility(View.VISIBLE); + return; + } + if (v.getId() == R.id.layout_check_power) { + CheckBox view = v.findViewById(R.id.check_power); + view.setChecked(!view.isChecked()); + mShowPower = view.isChecked(); + return; + } + if (v == mRootView && mRootView.findViewById(R.id.layout_top).getVisibility() == View.GONE) { + // Minify LOGO + View anchorView = mRootView.findViewById(R.id.iv_logo_minify); + PopupMenu menu = new PopupMenu(v.getContext(), anchorView); + menu.getMenu().add("Expand"); + menu.getMenu().add("Close"); + menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @SuppressLint("SetTextI18n") + @Override + public boolean onMenuItemClick(MenuItem item) { + String title = item.getTitle().toString(); + switch (title) { + case "Expand": + mRootView.findViewById(R.id.layout_top).setVisibility(View.VISIBLE); + mRootView.findViewById(R.id.iv_logo_minify).setVisibility(View.GONE); + break; + case "Close": + mUiHandler.postDelayed(new Runnable() { + @Override + public void run() { + dismiss(); + } + }, 200L); + break; + default: + break; + } + return false; + } + }); + menu.show(); + } + } + }; + + mRootView.findViewById(R.id.layout_proc).setOnClickListener(listener); + mRootView.findViewById(R.id.layout_dump).setOnClickListener(listener); + mRootView.findViewById(R.id.iv_logo).setOnClickListener(listener); + mRootView.findViewById(R.id.tv_minify).setOnClickListener(listener); + mRootView.findViewById(R.id.layout_check_power).setOnClickListener(listener); + mRootView.setOnClickListener(listener); + return true; + } catch (Exception e) { + MatrixLog.w(TAG, "Create float view failed:" + e.getMessage()); + return false; + } + } + + public void start(final int seconds) { + if (mRootView == null) { + MatrixLog.w(TAG, "Call #prepare first to show the indicator"); + return; + } + if (isRunning()) { + MatrixLog.w(TAG, "Already started!"); + return; + } + final int hashcode = mRootView.hashCode(); + mRunningRef.clear(); + mRunningRef.put(hashcode, true); + BatteryCanary.getMonitorFeature(TopThreadFeature.class, new Consumer() { + @Override + public void accept(TopThreadFeature topThreadFeat) { + topThreadFeat.top(seconds, new Supplier() { + @Override + public CompositeMonitors get() { + CompositeMonitors monitors = new CompositeMonitors(mCore, CompositeMonitors.SCOPE_TOP_INDICATOR); + monitors.metric(JiffiesMonitorFeature.UidJiffiesSnapshot.class); + monitors.metric(CpuStatFeature.CpuStateSnapshot.class); + monitors.metric(CpuStatFeature.UidCpuStateSnapshot.class); + monitors.metric(HealthStatsFeature.HealthStatsSnapshot.class); + monitors.metric(TrafficMonitorFeature.RadioStatSnapshot.class); + monitors.sample(DeviceStatMonitorFeature.CpuFreqSnapshot.class, 500L); + monitors.sample(DeviceStatMonitorFeature.BatteryCurrentSnapshot.class, 500L); + monitors.sample(TrafficMonitorFeature.RadioBpsSnapshot.class, 500L); + return monitors; + } + }, new ContinuousCallback() { + @Override + public boolean onGetDeltas(final CompositeMonitors monitors, long windowMillis) { + refresh(monitors); + if (mRootView == null || !mRunningRef.get(hashcode, false)) { + return true; + } + return false; + } + }); + } + }); + } + + public void stop() { + if (mRootView != null) { + mRunningRef.put(mRootView.hashCode(), false); + } + } + + @UiThread + public void dismiss() { + if (mRootView != null) { + WindowManager windowManager = (WindowManager) mRootView.getContext().getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + windowManager.removeView(mRootView); + mRootView = null; + } + } + + @NonNull + public Consumer> getDumpHandler() { + return mDumpHandler; + } + + private void setTextAlertColor(TextView tv, int level) { + tv.setTextColor(tv.getResources().getColor( + level == 2 ? R.color.Red_80 : level == 1 ? R.color.Orange_80 : R.color.FG_2 + )); + } + + @SuppressLint({"SetTextI18n", "RestrictedApi"}) + private void refresh(final CompositeMonitors monitors) { + mUiHandler.post(new Runnable() { + @Override + public void run() { + if (mRootView == null) { + return; + } + AppStats appStats = monitors.getAppStats(); + if (appStats == null) { + return; + } + + List> deltaList = monitors.getAllPidDeltaList(); + int battTemp = BatteryCanaryUtil.getBatteryTemperatureImmediately(mRootView.getContext()); + TextView tvBattTemp = mRootView.findViewById(R.id.tv_header_left); + tvBattTemp.setText((battTemp > 0 ? battTemp / 10f : "/") + "°C"); + setTextAlertColor(tvBattTemp, battTemp >= 350 ? 2 : battTemp >= 300 ? 1 : 0); + tvBattTemp = mRootView.findViewById(R.id.tv_temp_minify); + tvBattTemp.setText((battTemp > 0 ? battTemp / 10f : "/") + "°C"); + setTextAlertColor(tvBattTemp, battTemp >= 350 ? 2 : battTemp >= 300 ? 1 : 0); + + // CpuLoad EntryList + LinearLayout procEntryGroup = mRootView.findViewById(R.id.layout_entry_proc_group); + for (int i = 0; i < procEntryGroup.getChildCount(); i++) { + procEntryGroup.getChildAt(i).setVisibility(View.GONE); + } + LinearLayout threadEntryGroup = mRootView.findViewById(R.id.layout_entry_group); + for (int i = 0; i < threadEntryGroup.getChildCount(); i++) { + threadEntryGroup.getChildAt(i).setVisibility(View.GONE); + } + + List> procList = TopThreadFeature.getProcList(mRootView.getContext()); + float totalCpuLoad = 0; + for (int i = 0; i < deltaList.size(); i++) { + Delta delta = deltaList.get(i); + if (delta.isValid()) { + // Proc + int pid = delta.dlt.pid; + String name = delta.dlt.name; + for (Pair item : procList) { + //noinspection ConstantConditions + if (item.first == pid) { + //noinspection ConstantConditions + name = getProcSuffix(item.second); + } + } + + //noinspection ConstantConditions + if (pid == mCurrProc.first) { + // Curr Selected Proc's threads + name = name + " <-"; + int idx = 0; + for (JiffiesSnapshot.ThreadJiffiesEntry threadJiffies : delta.dlt.threadEntries.getList()) { + long entryJffies = threadJiffies.get(); + int tid = threadJiffies.tid; + String threadName = threadJiffies.name; + String status = (threadJiffies.isNewAdded ? "+" : "~") + threadJiffies.stat; + float threadLoad = figureCupLoad(entryJffies, delta.during / 10L); + + View threadItemView = threadEntryGroup.getChildAt(idx); + if (!mShowPower) { + threadItemView.setVisibility(View.VISIBLE); + } + TextView tvName = threadItemView.findViewById(R.id.tv_name); + TextView tvTid = threadItemView.findViewById(R.id.tv_tid); + TextView tvStatus = threadItemView.findViewById(R.id.tv_status); + TextView tvLoad = threadItemView.findViewById(R.id.tv_load); + tvName.setText(threadName); + tvTid.setText(String.valueOf(tid)); + tvStatus.setText(status); + tvLoad.setText(formatFloat(threadLoad, 1) + "%"); + + int alertLevel = threadJiffies.isNewAdded ? 0 : threadLoad >= 50 ? 2 : threadLoad >= 10 ? 1 : 0; + setTextAlertColor(tvName, alertLevel); + setTextAlertColor(tvTid, alertLevel); + setTextAlertColor(tvStatus, alertLevel); + setTextAlertColor(tvLoad, alertLevel); + + idx++; + if (idx >= MAX_THREAD_NUM) { + break; + } + } + mCurrDelta = delta; + } + + float procLoad = figureCupLoad(delta.dlt.totalJiffies.get(), delta.during / 10L); + totalCpuLoad += procLoad; + View procItemView = procEntryGroup.getChildAt(i); + procItemView.setVisibility(View.VISIBLE); + TextView tvProcName = procItemView.findViewById(R.id.tv_name); + TextView tvProcPid = procItemView.findViewById(R.id.tv_pid); + TextView tvProcLoad = procItemView.findViewById(R.id.tv_load); + tvProcName.setText(":" + name); + tvProcPid.setText(String.valueOf(pid)); + tvProcLoad.setText(formatFloat(procLoad, 1) + "%"); + } + } + + String totalLoad = formatFloat(totalCpuLoad, 1) + "%"; + TextView tvLoad = mRootView.findViewById(R.id.tv_header_right); + tvLoad.setText(totalLoad); + tvLoad = mRootView.findViewById(R.id.tv_load_minify); + tvLoad.setText(totalLoad); + + // Power EntryList + LinearLayout powerEntryGroup = mRootView.findViewById(R.id.layout_entry_power_group); + for (int i = 0; i < powerEntryGroup.getChildCount(); i++) { + powerEntryGroup.getChildAt(i).setVisibility(View.GONE); + } + if (mShowPower) { + final Map> powerMap = new LinkedHashMap<>(); + Result result = monitors.getSamplingResult(DeviceStatMonitorFeature.BatteryCurrentSnapshot.class); + if (result != null) { + double power = (result.sampleAvg / -1000) * (appStats.duringMillis * 1f / BatteryCanaryUtil.ONE_HOR); + double deltaPh = (Double) power * BatteryCanaryUtil.ONE_HOR / appStats.duringMillis; + powerMap.put("currency", new Pair<>("mAh", deltaPh / 1000)); + } else { + powerMap.put("currency", new Pair("mAh", null)); + } + final Delta healthStatsDelta = monitors.getDelta(HealthStatsFeature.HealthStatsSnapshot.class); + if (healthStatsDelta != null) { + powerMap.put("total", new Pair<>("mAh", healthStatsDelta.dlt.getTotalPower())); + powerMap.put("cpu", new Pair<>("mAh", healthStatsDelta.dlt.cpuPower.get())); + { + List modes = Arrays.asList("JiffyUid"); + for (String mode : modes) { + Object powers = healthStatsDelta.dlt.extras.get(mode); + if (powers != null) { + if (powers instanceof Map) { + // tuning cpu powers + for (Map.Entry entry : ((Map) powers).entrySet()) { + String key = String.valueOf(entry.getKey()); + Object val = entry.getValue(); + if (key.startsWith("power-cpu") && val instanceof Double) { + double cpuPower = (Double) val; + powerMap.put(key.replace("power-cpu", " - cpu"), new Pair<>("mAh", cpuPower)); + } + } + } + } + } + } + powerMap.put("wakelocks", new Pair<>("mAh", healthStatsDelta.dlt.wakelocksPower.get())); + powerMap.put("mobile", new Pair<>("mAh", healthStatsDelta.dlt.mobilePower.get())); + { + + monitors.getDelta(TrafficMonitorFeature.RadioStatSnapshot.class, new Consumer>() { + @Override + public void accept(final Delta delta) { + if (healthStatsDelta.dlt.extras.containsKey("power-mobile-statByte")) { + Object val = healthStatsDelta.dlt.extras.get("power-mobile-statByte"); + if (val instanceof Double) { + double power = (double) val; + powerMap.put(" - mobile-PowerBytes", new Pair<>("mAh", power)); + powerMap.put(" - mobile-RxBytes", new Pair<>("byte", (double) delta.dlt.mobileRxBytes.get())); + powerMap.put(" - mobile-TxBytes", new Pair<>("byte", (double) delta.dlt.mobileTxBytes.get())); + } + } + } + }); + } + powerMap.put("wifi", new Pair<>("mAh", healthStatsDelta.dlt.wifiPower.get())); + { + + monitors.getDelta(TrafficMonitorFeature.RadioStatSnapshot.class, new Consumer>() { + @Override + public void accept(final Delta delta) { + if (healthStatsDelta.dlt.extras.containsKey("power-wifi-statByte")) { + Object val = healthStatsDelta.dlt.extras.get("power-wifi-statByte"); + if (val instanceof Double) { + double power = (double) val; + powerMap.put(" - wifi-PowerBytes", new Pair<>("mAh", power)); + powerMap.put(" - wifi-RxBytes", new Pair<>("byte", (double) delta.dlt.wifiRxBytes.get())); + powerMap.put(" - wifi-TxBytes", new Pair<>("byte", (double) delta.dlt.wifiTxBytes.get())); + } + } + if (healthStatsDelta.dlt.extras.containsKey("power-wifi-statPacket")) { + Object val = healthStatsDelta.dlt.extras.get("power-wifi-statPacket"); + if (val instanceof Double) { + double power = (double) val; + powerMap.put(" - wifi-PowerPackets", new Pair<>("mAh", power)); + powerMap.put(" - wifi-RxPackets", new Pair<>("packet", (double) delta.dlt.wifiRxPackets.get())); + powerMap.put(" - wifi-TxPackets", new Pair<>("packet", (double) delta.dlt.wifiTxPackets.get())); + } + } + } + }); + } + powerMap.put("blueTooth", new Pair<>("mAh", healthStatsDelta.dlt.blueToothPower.get())); + powerMap.put("gps", new Pair<>("mAh", healthStatsDelta.dlt.gpsPower.get())); + powerMap.put("sensors", new Pair<>("mAh", healthStatsDelta.dlt.sensorsPower.get())); + powerMap.put("camera", new Pair<>("mAh", healthStatsDelta.dlt.cameraPower.get())); + powerMap.put("flashLight", new Pair<>("mAh", healthStatsDelta.dlt.flashLightPower.get())); + powerMap.put("audio", new Pair<>("mAh", healthStatsDelta.dlt.audioPower.get())); + powerMap.put("video", new Pair<>("mAh", healthStatsDelta.dlt.videoPower.get())); + powerMap.put("screen", new Pair<>("mAh", healthStatsDelta.dlt.screenPower.get())); + // powerMap.put("systemService", healthStatsDelta.dlt.systemServicePower.get()); + powerMap.put("idle", new Pair<>("mAh", healthStatsDelta.dlt.idlePower.get())); + } + // for (Iterator> iterator = powerMap.entrySet().iterator(); iterator.hasNext(); ) { + // Map.Entry item = iterator.next(); + // if (item.getValue() == 0d) { + // iterator.remove(); + // } + // } + int idx = 0; + for (Map.Entry> entry : powerMap.entrySet()) { + String module = entry.getKey(); + String unit = ""; + Double value = null; + Pair pair = entry.getValue(); + if (pair.first != null) { + if (pair.first.equals("mAh")) { + unit = ""; + if (pair.second != null) { + double power = (double) pair.second; + value = power * BatteryCanaryUtil.ONE_HOR / appStats.duringMillis; + } + } else { + unit = pair.first; + if (pair.second != null) { + value = pair.second; + } + } + } + View threadItemView = powerEntryGroup.getChildAt(idx); + threadItemView.setVisibility(View.VISIBLE); + TextView tvName = threadItemView.findViewById(R.id.tv_name); + TextView tvUnit = threadItemView.findViewById(R.id.tv_unit); + TextView tvPower = threadItemView.findViewById(R.id.tv_power); + tvName.setText(module); + tvUnit.setText(unit); + if (value == null) { + tvPower.setText("NULL"); + } else { + tvPower.setText(String.valueOf(HealthStatsHelper.round(value, 5))); + } + idx++; + if (idx >= MAX_POWER_NUM) { + break; + } + } + } + } + }); + } + + @RestrictTo(RestrictTo.Scope.LIBRARY) + public static String getProcSuffix(String input) { + String proc = "main"; + if (input.contains(":")) { + proc = input.substring(input.lastIndexOf(":") + 1); + } + return proc; + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryRecord.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryRecord.java new file mode 100644 index 000000000..3e423fbba --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryRecord.java @@ -0,0 +1,530 @@ +package com.tencent.matrix.batterycanary.stats; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; + +import com.tencent.matrix.batterycanary.monitor.AppStats; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.CallSuper; + +/** + * @author Kaede + * @since 2021/12/10 + */ +public abstract class BatteryRecord implements Parcelable { + + public static final int RECORD_TYPE_PROC_STAT = 1; + public static final int RECORD_TYPE_DEV_STAT = 2; + public static final int RECORD_TYPE_APP_STAT = 3; + public static final int RECORD_TYPE_SCENE_STAT = 4; + public static final int RECORD_TYPE_EVENT_STAT = 5; + public static final int RECORD_TYPE_REPORT = 6; + + public static byte[] encode(BatteryRecord record) { + int type; + Class recordClass = record.getClass(); + if (recordClass == ProcStatRecord.class) { + type = RECORD_TYPE_PROC_STAT; + } else if (recordClass == DevStatRecord.class) { + type = RECORD_TYPE_DEV_STAT; + } else if (recordClass == AppStatRecord.class) { + type = RECORD_TYPE_APP_STAT; + } else if (recordClass == SceneStatRecord.class) { + type = RECORD_TYPE_SCENE_STAT; + } else if (recordClass == EventStatRecord.class) { + type = RECORD_TYPE_EVENT_STAT; + } else if (recordClass == ReportRecord.class) { + type = RECORD_TYPE_REPORT; + } else { + throw new UnsupportedOperationException("Unknown record type: " + record); + } + + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.writeInt(type); + record.writeToParcel(parcel, 0); + return parcel.marshall(); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } + + @SuppressWarnings("ConstantConditions") + public static BatteryRecord decode(byte[] bytes) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + int type = parcel.readInt(); + + Creator creator; + switch (type) { + case RECORD_TYPE_PROC_STAT: + creator = ProcStatRecord.CREATOR; + break; + case RECORD_TYPE_DEV_STAT: + creator = DevStatRecord.CREATOR; + break; + case RECORD_TYPE_APP_STAT: + creator = AppStatRecord.CREATOR; + break; + case RECORD_TYPE_SCENE_STAT: + creator = SceneStatRecord.CREATOR; + break; + case RECORD_TYPE_EVENT_STAT: + creator = EventStatRecord.CREATOR; + break; + case RECORD_TYPE_REPORT: + creator = ReportRecord.CREATOR; + break; + default: + throw new UnsupportedOperationException("Unknown record type: " + type); + } + return (BatteryRecord) creator.createFromParcel(parcel); + } finally { + parcel.recycle(); + } + } + + public int version; + public long millis; + + protected BatteryRecord() { + version = 0; + millis = System.currentTimeMillis(); + } + + protected BatteryRecord(Parcel in) { + version = in.readInt(); + millis = in.readLong(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + @CallSuper + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(version); + dest.writeLong(millis); + } + + public static class ProcStatRecord extends BatteryRecord implements Parcelable { + public static final int VERSION = 0; + public static final int STAT_PROC_LAUNCH = 1; + public static final int STAT_PROC_OFF = 2; + + public int procStat = STAT_PROC_LAUNCH; + public int pid; + + public ProcStatRecord() { + version = VERSION; + } + + protected ProcStatRecord(Parcel in) { + super(in); + procStat = in.readInt(); + pid = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ProcStatRecord createFromParcel(Parcel in) { + return new ProcStatRecord(in); + } + + @Override + public ProcStatRecord[] newArray(int size) { + return new ProcStatRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(procStat); + dest.writeInt(pid); + } + } + + public static class DevStatRecord extends BatteryRecord implements Parcelable { + public static final int VERSION = 0; + + @AppStats.DevStatusDef + public int devStat; + + public DevStatRecord() { + } + + protected DevStatRecord(Parcel in) { + super(in); + devStat = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public DevStatRecord createFromParcel(Parcel in) { + return new DevStatRecord(in); + } + + @Override + public DevStatRecord[] newArray(int size) { + return new DevStatRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(devStat); + } + } + + public static class AppStatRecord extends BatteryRecord implements Parcelable { + public static final int VERSION = 0; + + @AppStats.AppStatusDef + public int appStat; + + public AppStatRecord() { + version = VERSION; + } + + protected AppStatRecord(Parcel in) { + super(in); + appStat = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public AppStatRecord createFromParcel(Parcel in) { + return new AppStatRecord(in); + } + + @Override + public AppStatRecord[] newArray(int size) { + return new AppStatRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(appStat); + } + } + + public static class SceneStatRecord extends BatteryRecord implements Parcelable { + public static final int VERSION = 0; + + public String scene; + + public SceneStatRecord() { + version = VERSION; + } + + protected SceneStatRecord(Parcel in) { + super(in); + scene = in.readString(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public SceneStatRecord createFromParcel(Parcel in) { + return new SceneStatRecord(in); + } + + @Override + public SceneStatRecord[] newArray(int size) { + return new SceneStatRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(scene); + } + } + + + /** + * EventStatRecord { + * extras [:] // since version >= 1 + * } + */ + public static class EventStatRecord extends BatteryRecord implements Parcelable { + public static final int VERSION = 1; + public static final String EVENT_REPORT = "REPORT"; + public static final String EVENT_BATTERY_STAT = "BATTERY_STAT"; + + public long id; + public String event; + public Map extras = Collections.emptyMap(); + + public EventStatRecord() { + id = 0; + version = VERSION; + } + + protected EventStatRecord(Parcel in) { + super(in); + id = in.readLong(); + event = in.readString(); + if (version >= 1) { + extras = new HashMap<>(); + in.readMap(extras, getClass().getClassLoader()); + } + } + + public String getString(String key) { + if (extras.containsKey(key)) { + return String.valueOf(extras.get(key)); + } + return ""; + } + + public long getDigit(String key, long def) { + try { + if (extras.containsKey(key)) { + return Long.parseLong(String.valueOf(extras.get(key))); + } + } catch (Exception ignored) { + } + return def; + } + + public boolean getBoolean(String key, boolean def) { + if (extras.containsKey(key)) { + try { + return Boolean.parseBoolean(String.valueOf(extras.get(key))); + } catch (Exception ignored) { + } + } + return def; + } + + public static final Creator CREATOR = new Creator() { + @Override + public EventStatRecord createFromParcel(Parcel in) { + return new EventStatRecord(in); + } + + @Override + public EventStatRecord[] newArray(int size) { + return new EventStatRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(id); + dest.writeString(event); + dest.writeMap(extras); + } + } + + + /** + * ReportRecord { + * extras [:] // since version >= 1 + * + * ThreadInfo [] { + * extraInfo [:] + * } + * + * EntryInfo [] { + * entries [:] + * } + * } + */ + public static class ReportRecord extends EventStatRecord implements Parcelable { + public static final int VERSION = EventStatRecord.VERSION; + + public static final String EXTRA_APP_FOREGROUND = "app_fg"; + public static final String EXTRA_JIFFY_TOTAL = "jiffy_total"; + public static final String EXTRA_JIFFY_OVERHEAT = "jiffy_overheat"; + public static final String EXTRA_THREAD_STACK = "extra_stack_top"; + + public String scope; + public long windowMillis; + public List threadInfoList = Collections.emptyList(); + public List entryList = Collections.emptyList(); + + public ReportRecord() { + version = VERSION; + event = EVENT_REPORT; + } + + protected ReportRecord(Parcel in) { + super(in); + scope = in.readString(); + windowMillis = in.readLong(); + threadInfoList = in.createTypedArrayList(ReportRecord.ThreadInfo.CREATOR); + entryList = in.createTypedArrayList(ReportRecord.EntryInfo.CREATOR); + } + + public boolean isOverHeat() { + for (String item : extras.keySet()) { + if (item.endsWith("_overheat")) { + if (getBoolean(item, false)) { + return true; + } + } + } + return false; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ReportRecord createFromParcel(Parcel in) { + return new ReportRecord(in); + } + + @Override + public ReportRecord[] newArray(int size) { + return new ReportRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(scope); + dest.writeLong(windowMillis); + dest.writeTypedList(threadInfoList); + dest.writeTypedList(entryList); + } + + + public static class ThreadInfo implements Parcelable { + public int tid; + public String name; + public String stat; + public long jiffies; + public Map extraInfo = Collections.emptyMap(); + + public ThreadInfo() { + } + + protected ThreadInfo(Parcel in) { + tid = in.readInt(); + name = in.readString(); + stat = in.readString(); + jiffies = in.readLong(); + extraInfo = new ArrayMap<>(); + in.readMap(extraInfo, getClass().getClassLoader()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ReportRecord.ThreadInfo createFromParcel(Parcel in) { + return new ReportRecord.ThreadInfo(in); + } + + @Override + public ReportRecord.ThreadInfo[] newArray(int size) { + return new ReportRecord.ThreadInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(tid); + dest.writeString(name); + dest.writeString(stat); + dest.writeLong(jiffies); + dest.writeMap(extraInfo); + } + } + + public static class EntryInfo implements Parcelable { + public String name; + public String stat; + public Map entries = Collections.emptyMap(); + + public EntryInfo() { + } + + protected EntryInfo(Parcel in) { + name = in.readString(); + stat = in.readString(); + entries = new LinkedHashMap<>(); + in.readMap(entries, getClass().getClassLoader()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ReportRecord.EntryInfo createFromParcel(Parcel in) { + return new ReportRecord.EntryInfo(in); + } + + @Override + public ReportRecord.EntryInfo[] newArray(int size) { + return new ReportRecord.EntryInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(name); + dest.writeString(stat); + dest.writeMap(entries); + } + } + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryRecorder.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryRecorder.java new file mode 100644 index 000000000..fd61e71de --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryRecorder.java @@ -0,0 +1,217 @@ +package com.tencent.matrix.batterycanary.stats; + +import android.os.Process; +import android.text.TextUtils; + +import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; +import com.tencent.matrix.batterycanary.utils.ThreadSafeReference; +import com.tencent.matrix.util.MatrixLog; +import com.tencent.mmkv.MMKV; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Kaede + * @since 2021/12/10 + */ +public interface BatteryRecorder { + String TAG = "Matrix.battery.recorder"; + + void updateProc(String proc); + + Set getProcSet(); + + void write(String date, BatteryRecord record); + + List read(String date, String proc); + + void clean(String date, String proc); + + void clean(int dayToKeepOnly); + + + class MMKVRecorder implements BatteryRecorder { + protected static final String MAGIC = "bs"; + protected static final ThreadSafeReference sProcSuffixRef = new ThreadSafeReference() { + @Override + public String onCreate() { + String processName = BatteryCanaryUtil.getProcessName(); + if (processName.contains(":")) { + return processName.substring(processName.lastIndexOf(":") + 1); + } else { + return "main"; + } + } + }; + protected static final ThreadSafeReference sFormatRef = new ThreadSafeReference() { + @Override + public DateFormat onCreate() { + return new SimpleDateFormat("yyyy-MM-dd", Locale.US); + } + }; + + protected final int pid = Process.myPid(); + protected final MMKV mmkv; + protected AtomicInteger inc = new AtomicInteger(0); + + public MMKVRecorder(MMKV mmkv) { + this.mmkv = mmkv; + } + + protected String getRecordKeyPrefix(String date, String proc) { + return MAGIC + "-" + date + (TextUtils.isEmpty(proc) ? "" : "-" + proc); + } + + protected String getProcSetKey() { + return MAGIC + "-proc-set"; + } + + @Override + public void updateProc(String proc) { + if (!TextUtils.isEmpty(proc)) { + Set procSet = getProcSet(); + if (!procSet.contains(proc)) { + if (procSet.isEmpty()) { + procSet = new HashSet<>(); + } + procSet.add(proc); + String key = getProcSetKey(); + mmkv.encode(key, procSet); + } + } + } + + @Override + public Set getProcSet() { + String key = getProcSetKey(); + Set procSet = mmkv.decodeStringSet(key); + return procSet == null ? Collections.emptySet() : procSet; + } + + @Override + public void write(String date, BatteryRecord record) { + String proc = getProcNameSuffix(); + write(date, record, proc); + } + + public void write(String date, BatteryRecord record, String proc) { + try { + String key = getRecordKeyPrefix(date, proc) + "-" + pid + "-" + inc.getAndIncrement(); + byte[] bytes = BatteryRecord.encode(record); + mmkv.encode(key, bytes); + // mmkv.sync(); + } catch (Exception e) { + MatrixLog.w(TAG, "record encode failed: " + e.getMessage()); + } + } + + @Override + public List read(String date, String proc) { + String[] keys = mmkv.allKeys(); + if (keys == null || keys.length == 0) { + return Collections.emptyList(); + } + List records = new ArrayList<>(Math.min(16, keys.length)); + String keyPrefix = getRecordKeyPrefix(date, proc) + "-"; + for (String item : keys) { + if (item.startsWith(keyPrefix)) { + try { + byte[] bytes = mmkv.decodeBytes(item); + if (bytes != null) { + BatteryRecord record = BatteryRecord.decode(bytes); + records.add(record); + } + } catch (Exception e) { + MatrixLog.w(TAG, "record decode failed: " + e.getMessage()); + } + } + } + Collections.sort(records, new Comparator() { + @Override + public int compare(BatteryRecord left, BatteryRecord right) { + return Long.compare(left.millis, right.millis); + } + }); + return records; + } + + @Override + public void clean(String date, String proc) { + String[] keys = mmkv.allKeys(); + if (keys == null || keys.length == 0) { + return; + } + String keyPrefix = getRecordKeyPrefix(date, proc); + for (String item : keys) { + if (item.startsWith(keyPrefix)) { + try { + mmkv.remove(item); + } catch (Exception e) { + MatrixLog.w(TAG, "record clean failed: " + e.getMessage()); + } + } + } + } + + @Override + public void clean(int dayToKeepOnly) { + if (dayToKeepOnly > 0) { + List datesToKeep = new ArrayList<>(); + for (int i = 0; i < dayToKeepOnly; i++) { + datesToKeep.add(getDateString(-i)); + } + String[] keys = mmkv.allKeys(); + if (keys == null || keys.length == 0) { + return; + } + String procSetKey = getProcSetKey(); + for (String item : keys) { + if (procSetKey.equals(item)) { + continue; + } + boolean keep = false; + for (String date : datesToKeep) { + String keyPrefix = getRecordKeyPrefix(date, ""); + if (item.startsWith(keyPrefix)) { + keep = true; + break; + } + } + if (!keep) { + try { + mmkv.remove(item); + } catch (Exception e) { + MatrixLog.w(TAG, "record clean failed: " + e.getMessage()); + } + } + } + } + } + + public void flush() { + mmkv.sync(); + } + + public static String getProcNameSuffix() { + return sProcSuffixRef.safeGet(); + } + + public static String getDateString(int dayOffset) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, dayOffset); + return sFormatRef.safeGet().format(cal.getTime()); + } + + } + +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryStats.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryStats.java new file mode 100644 index 000000000..b85bd23ac --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryStats.java @@ -0,0 +1,240 @@ +package com.tencent.matrix.batterycanary.stats; + +import android.text.TextUtils; + +import com.tencent.matrix.batterycanary.monitor.AppStats; +import com.tencent.matrix.batterycanary.monitor.feature.BlueToothMonitorFeature.BlueToothSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.CompositeMonitors; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature.CpuStateSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.DeviceStatMonitorFeature.BatteryTmpSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.JiffiesMonitorFeature.JiffiesSnapshot.ThreadJiffiesEntry; +import com.tencent.matrix.batterycanary.monitor.feature.LocationMonitorFeature.LocationSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Delta; +import com.tencent.matrix.batterycanary.monitor.feature.WifiMonitorFeature.WifiSnapshot; +import com.tencent.matrix.batterycanary.stats.BatteryRecord.ReportRecord.EntryInfo; +import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; +import com.tencent.matrix.batterycanary.utils.Consumer; +import com.tencent.matrix.batterycanary.utils.PowerProfile; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +/** + * @author Kaede + * @since 2021/12/15 + */ +public interface BatteryStats { + + BatteryRecord.AppStatRecord statsAppStat(int appStat); + + BatteryRecord.DevStatRecord statsDevStat(int devStat); + + BatteryRecord.SceneStatRecord statsScene(String scene); + + BatteryRecord.EventStatRecord statsEvent(String event, int eventId, Map extras); + + BatteryRecord.ReportRecord statsMonitors(final CompositeMonitors monitors); + + + class BatteryStatsImpl implements BatteryStats { + + @Override + public BatteryRecord.AppStatRecord statsAppStat(int appStat) { + BatteryRecord.AppStatRecord statRecord = new BatteryRecord.AppStatRecord(); + statRecord.appStat = appStat; + return statRecord; + } + + @Override + public BatteryRecord.DevStatRecord statsDevStat(int devStat) { + BatteryRecord.DevStatRecord statRecord = new BatteryRecord.DevStatRecord(); + statRecord.devStat = devStat; + return statRecord; + } + + @Override + public BatteryRecord.SceneStatRecord statsScene(String scene) { + BatteryRecord.SceneStatRecord statRecord = new BatteryRecord.SceneStatRecord(); + statRecord.scene = scene; + return statRecord; + } + + @Override + public BatteryRecord.EventStatRecord statsEvent(String event, int eventId, Map extras) { + BatteryRecord.EventStatRecord statRecord = new BatteryRecord.EventStatRecord(); + statRecord.id = eventId; + statRecord.event = event; + if (!extras.isEmpty()) { + statRecord.extras = extras; + } + return statRecord; + } + + @Override + public BatteryRecord.ReportRecord statsMonitors(final CompositeMonitors monitors) { + final BatteryRecord.ReportRecord statRecord = new BatteryRecord.ReportRecord(); + final AppStats appStats = monitors.getAppStats(); + if (appStats == null) { + return statRecord; + } + + boolean fg = appStats.isForeground(); + long widowMillis = appStats.duringMillis; + long minute = appStats.getMinute(); + + statRecord.scope = monitors.getScope(); + statRecord.windowMillis = widowMillis; + statRecord.extras = new HashMap<>(); + + if (appStats.isForeground()) { + statRecord.extras.put(BatteryRecord.ReportRecord.EXTRA_APP_FOREGROUND, true); + } + + // Thread Entry + final Delta jiffiesDelta = monitors.getDelta(JiffiesSnapshot.class); + if (jiffiesDelta != null) { + long appJiffiesDelta = jiffiesDelta.dlt.totalJiffies.get(); + statRecord.extras.put(BatteryRecord.ReportRecord.EXTRA_JIFFY_TOTAL, appJiffiesDelta); + if (!fg && monitors.isOverHeat(JiffiesSnapshot.class)) { + // Jiffies overheat + statRecord.extras.put(BatteryRecord.ReportRecord.EXTRA_JIFFY_OVERHEAT, true); + } + + statRecord.threadInfoList = new ArrayList<>(); + for (ThreadJiffiesEntry threadJiffies : jiffiesDelta.dlt.threadEntries.getList().subList(0, Math.min(jiffiesDelta.dlt.threadEntries.getList().size(), 5))) { + BatteryRecord.ReportRecord.ThreadInfo threadInfo = new BatteryRecord.ReportRecord.ThreadInfo(); + threadInfo.stat = threadJiffies.stat; + threadInfo.tid = threadJiffies.tid; + threadInfo.name = threadJiffies.name; + threadInfo.jiffies = threadJiffies.get(); + + // stack + if (!TextUtils.isEmpty(threadJiffies.stack)) { + if (!threadInfo.extraInfo.containsKey(BatteryRecord.ReportRecord.EXTRA_THREAD_STACK)) { + if (threadInfo.extraInfo.isEmpty()) { + threadInfo.extraInfo = new HashMap<>(); + } + threadInfo.extraInfo.put(BatteryRecord.ReportRecord.EXTRA_THREAD_STACK, threadJiffies.stack); + } + } + statRecord.threadInfoList.add(threadInfo); + } + } + + statRecord.entryList = new ArrayList<>(); + + // DevStat Entry + createEntryInfo(new Consumer() { + @Override + public void accept(final EntryInfo entryInfo) { + entryInfo.name = "设备状态"; + entryInfo.entries = new LinkedHashMap<>(); + + // cpu load & cpu sip + monitors.getFeature(CpuStatFeature.class, new Consumer() { + @Override + public void accept(final CpuStatFeature cpuStatFeature) { + monitors.getDelta(CpuStateSnapshot.class, new Consumer>() { + @Override + public void accept(Delta delta) { + if (jiffiesDelta != null) { + long appJiffiesDelta = jiffiesDelta.dlt.totalJiffies.get(); + long cpuJiffiesDelta = delta.dlt.totalCpuJiffies(); + float cpuLoad = (float) appJiffiesDelta / cpuJiffiesDelta; + float cpuLoadAvg = cpuLoad * BatteryCanaryUtil.getCpuCoreNum(); + entryInfo.entries.put("Cpu Load", (int) (Math.max(cpuLoadAvg, 0) * 100) + "%"); + + final PowerProfile powerProfile = cpuStatFeature.getPowerProfile(); + double procSipBgn = delta.bgn.configureProcSip(powerProfile, jiffiesDelta.bgn.totalJiffies.get()); + double procSipEnd = delta.end.configureProcSip(powerProfile, jiffiesDelta.end.totalJiffies.get()); + entryInfo.entries.put("Cpu Power", String.format(Locale.US, "%.2f mAh", Math.max(procSipEnd - procSipBgn, 0))); + } + } + }); + } + }); + + // temperature + monitors.getDelta(BatteryTmpSnapshot.class, new Consumer>() { + @Override + public void accept(Delta delta) { + double currTemp = delta.end.temp.get(); + entryInfo.entries.put("当前电池温度", String.format(Locale.US, "%.1f ℃", currTemp / 10.0f)); + monitors.getSamplingResult(BatteryTmpSnapshot.class, new Consumer() { + @Override + public void accept(MonitorFeature.Snapshot.Sampler.Result result) { + double maxTemp = result.sampleMax; + double minTemp = result.sampleMin; + entryInfo.entries.put("最大电池温度", String.format(Locale.US, "%.1f ℃", maxTemp / 10.0f)); + entryInfo.entries.put("电池温度变化", String.format(Locale.US, "%.1f ℃", (maxTemp - minTemp) / 10.0f)); + } + }); + } + }); + + statRecord.entryList.add(entryInfo); + } + }); + + // AppStat Entry + createEntryInfo(new Consumer() { + @Override + public void accept(final EntryInfo entryInfo) { + entryInfo.name = "App 状态"; + entryInfo.entries = new LinkedHashMap<>(); + + entryInfo.entries.put("前台时间占比", appStats.appFgRatio + "%"); + entryInfo.entries.put("后台时间占比", appStats.appBgRatio + "%"); + entryInfo.entries.put("前台服务时间占比", appStats.appFgSrvRatio + "%"); + entryInfo.entries.put("充电时间占比", appStats.devChargingRatio + "%"); + entryInfo.entries.put("息屏时间占比 (排除充电)", appStats.devSceneOffRatio + "%"); + + statRecord.entryList.add(entryInfo); + } + }); + + // SystemService Entry + createEntryInfo(new Consumer() { + @Override + public void accept(final EntryInfo entryInfo) { + entryInfo.name = "系统服务调用"; + entryInfo.entries = new LinkedHashMap<>(); + + monitors.getDelta(BlueToothSnapshot.class, new Consumer>() { + @Override + public void accept(Delta delta) { + entryInfo.entries.put("BlueTooth 扫描", String.format(Locale.US, "register %s, discovery %s, scan %s 次", delta.dlt.regsCount.get(), delta.dlt.discCount.get(), delta.dlt.scanCount.get())); + } + }); + monitors.getDelta(WifiSnapshot.class, new Consumer>() { + @Override + public void accept(Delta delta) { + entryInfo.entries.put("Wifi 扫描", String.format(Locale.US, "query %s, scan %s 次", delta.dlt.queryCount.get(), delta.dlt.scanCount.get())); + } + }); + monitors.getDelta(LocationSnapshot.class, new Consumer>() { + @Override + public void accept(Delta delta) { + entryInfo.entries.put("GPS 扫描", String.format(Locale.US, "scan %s 次", delta.dlt.scanCount.get())); + } + }); + + statRecord.entryList.add(entryInfo); + } + }); + + return statRecord; + } + + protected void createEntryInfo(Consumer consumer) { + EntryInfo entryInfo = new EntryInfo(); + consumer.accept(entryInfo); + } + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryStatsFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryStatsFeature.java new file mode 100644 index 000000000..40cf45e7e --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/BatteryStatsFeature.java @@ -0,0 +1,256 @@ +package com.tencent.matrix.batterycanary.stats; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; + +import com.tencent.matrix.batterycanary.monitor.AppStats; +import com.tencent.matrix.batterycanary.monitor.feature.AbsMonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.CompositeMonitors; +import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; +import com.tencent.matrix.util.MatrixHandlerThread; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +/** + * @author Kaede + * @since 2021/12/3 + */ +public final class BatteryStatsFeature extends AbsMonitorFeature { + private static final String TAG = "Matrix.battery.BatteryStats"; + private static final int DAY_LIMIT = 7; + + private HandlerThread mStatsThread; + private Handler mStatsHandler; + private BatteryRecorder mBatteryRecorder; + private BatteryStats mBatteryStats; + private boolean mStatsImmediately = false; + + @Override + public int weight() { + return 0; + } + + @Override + protected String getTag() { + return TAG; + } + + @Override + public void onTurnOn() { + super.onTurnOn(); + mBatteryRecorder = mCore.getConfig().batteryRecorder; + mBatteryStats = mCore.getConfig().batteryStats; + if (mBatteryRecorder != null) { + mStatsThread = MatrixHandlerThread.getNewHandlerThread("matrix_stats", Thread.NORM_PRIORITY); + mStatsHandler = new Handler(mStatsThread.getLooper()); + mStatsHandler.post(new Runnable() { + @Override + public void run() { + mBatteryRecorder.updateProc(BatteryRecorder.MMKVRecorder.getProcNameSuffix()); + + // Clean expired records if need + mBatteryRecorder.clean(DAY_LIMIT); + } + }); + } + + BatteryRecord.ProcStatRecord procStatRecord = new BatteryRecord.ProcStatRecord(); + procStatRecord.pid = Process.myPid(); + procStatRecord.procStat = BatteryRecord.ProcStatRecord.STAT_PROC_LAUNCH; + writeRecord(procStatRecord); + } + + @Override + public void onForeground(boolean isForeground) { + super.onForeground(isForeground); + statsAppStat(isForeground ? AppStats.APP_STAT_FOREGROUND : AppStats.APP_STAT_BACKGROUND); + } + + @Override + public void onTurnOff() { + super.onTurnOff(); + BatteryRecord.ProcStatRecord procStatRecord = new BatteryRecord.ProcStatRecord(); + procStatRecord.pid = Process.myPid(); + procStatRecord.procStat = BatteryRecord.ProcStatRecord.STAT_PROC_OFF; + writeRecord(procStatRecord); + + if (mStatsHandler != null) { + mStatsHandler.post(new Runnable() { + @Override + public void run() { + if (mStatsThread != null) { + mStatsThread.quit(); + } + } + }); + } + } + + @VisibleForTesting + public void setStatsImmediately(boolean statsImmediately) { + mStatsImmediately = statsImmediately; + } + + public Set getProcSet() { + if (mBatteryRecorder != null) { + return mBatteryRecorder.getProcSet(); + } + return Collections.emptySet(); + } + + public void writeRecord(final BatteryRecord record) { + if (mBatteryRecorder != null) { + final String date = getDateString(0); + if (mStatsImmediately) { + writeRecord(date, record); + return; + } + if (mStatsHandler != null) { + mStatsHandler.post(new Runnable() { + @Override + public void run() { + writeRecord(date, record); + } + }); + } + } + } + + @WorkerThread + public void writeRecord(String date, BatteryRecord record) { + if (mBatteryRecorder != null) { + mBatteryRecorder.write(date, record); + } + } + + @WorkerThread + public List readRecords(int dayOffset, String proc) { + if (mBatteryRecorder != null) { + final String date = getDateString(dayOffset); + return mBatteryRecorder.read(date, proc); + } + return Collections.emptyList(); + } + + @WorkerThread + public List readRecords(String date, String proc) { + if (mBatteryRecorder != null) { + return mBatteryRecorder.read(date, proc); + } + return Collections.emptyList(); + } + + @WorkerThread + public BatteryRecords readBatteryRecords(int dayOffset, String proc) { + BatteryRecords batteryRecords = new BatteryRecords(); + batteryRecords.date = getDateString(dayOffset); + batteryRecords.records = readRecords(dayOffset, proc); + return batteryRecords; + } + + @WorkerThread + void cleanRecords(String date, String proc) { + if (mBatteryRecorder != null) { + mBatteryRecorder.clean(date, proc); + } + } + + @WorkerThread + void cleanRecords() { + if (mBatteryRecorder != null) { + mBatteryRecorder.clean(DAY_LIMIT); + } + } + + public void statsAppStat(int appStat) { + if (mBatteryStats != null) { + writeRecord(mBatteryStats.statsAppStat(appStat)); + } + } + + public void statsDevStat(int devStat) { + if (mBatteryStats != null) { + writeRecord(mBatteryStats.statsDevStat(devStat)); + } + } + + public void statsScene(String scene) { + if (mBatteryStats != null) { + writeRecord(mBatteryStats.statsScene(scene)); + } + } + + public void statsEvent(String event) { + statsEvent(event, 0); + } + + public void statsEvent(String event, int eventId) { + statsEvent(event, eventId, Collections.emptyMap()); + } + + public void statsEvent(String event, int eventId, Map extras) { + if (mBatteryStats != null) { + writeRecord(mBatteryStats.statsEvent(event, eventId, extras)); + } + } + + public void statsBatteryEvent(boolean isLowBattery) { + String event = BatteryRecord.EventStatRecord.EVENT_BATTERY_STAT; + int id = 0; + int pct = BatteryCanaryUtil.getBatteryPercentage(mCore.getContext()); + Map extras = new HashMap<>(); + extras.put("battery-low", isLowBattery); + extras.put("battery-pct", pct); + statsEvent(event, id, extras); + } + + public void statsBatteryEvent(int pct) { + String event = BatteryRecord.EventStatRecord.EVENT_BATTERY_STAT; + int id = 0; + Map extras = new HashMap<>(); + extras.put("battery-change", true); + extras.put("battery-pct", pct); + statsEvent(event, id, extras); + } + + public void statsBatteryTempEvent(int temperature) { + String event = BatteryRecord.EventStatRecord.EVENT_BATTERY_STAT; + int id = 0; + int pct = BatteryCanaryUtil.getBatteryPercentage(mCore.getContext()); + Map extras = new HashMap<>(); + extras.put("battery-temp", temperature); + extras.put("battery-pct", pct); + statsEvent(event, id, extras); + } + + public void statsMonitors(CompositeMonitors monitors) { + if (mBatteryRecorder == null) { + return; + } + final AppStats appStats = monitors.getAppStats(); + if (appStats == null) { + return; + } + if (mBatteryStats != null) { + writeRecord(mBatteryStats.statsMonitors(monitors)); + } + } + + public static String getDateString(int dayOffset) { + return BatteryRecorder.MMKVRecorder.getDateString(dayOffset); + } + + + public static class BatteryRecords { + public String date; + public List records; + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/HealthStatsFeature.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/HealthStatsFeature.java new file mode 100644 index 000000000..0d81da45e --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/HealthStatsFeature.java @@ -0,0 +1,826 @@ +package com.tencent.matrix.batterycanary.stats; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.health.HealthStats; +import android.os.health.PidHealthStats; +import android.os.health.ProcessHealthStats; +import android.os.health.TimerStat; +import android.os.health.UidHealthStats; + +import com.tencent.matrix.batterycanary.monitor.feature.AbsMonitorFeature; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.DigitEntry; +import com.tencent.matrix.batterycanary.utils.BatteryCanaryUtil; +import com.tencent.matrix.batterycanary.utils.PowerProfile; +import com.tencent.matrix.util.MatrixLog; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +/** + * @author Kaede + * @since 18/7/2022 + */ +public class HealthStatsFeature extends AbsMonitorFeature { + private static final String TAG = "Matrix.battery.HealthStats"; + + @Override + protected String getTag() { + return TAG; + } + + @Override + public int weight() { + return 0; + } + + public HealthStats currHealthStats() { + return HealthStatsHelper.getCurrStats(mCore.getContext()); + } + + @SuppressLint("VisibleForTests") + public HealthStatsSnapshot currHealthStatsSnapshot() { + HealthStatsSnapshot snapshot = new HealthStatsSnapshot(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return snapshot; + } + final HealthStats healthStats = currHealthStats(); + if (healthStats != null) { + snapshot.healthStats = healthStats; + + // Power + CpuStatFeature cpuStatFeat = mCore.getMonitorFeature(CpuStatFeature.class); + if (cpuStatFeat != null) { + PowerProfile powerProfile = cpuStatFeat.getPowerProfile(); + if (powerProfile != null && powerProfile.isSupported()) { + snapshot.cpuPower = DigitEntry.of(HealthStatsHelper.calcCpuPower(powerProfile, healthStats)); + snapshot.wakelocksPower = DigitEntry.of(HealthStatsHelper.calcWakelocksPower(powerProfile, healthStats)); + snapshot.mobilePower = DigitEntry.of(HealthStatsHelper.calcMobilePower(powerProfile, healthStats)); + snapshot.wifiPower = DigitEntry.of(HealthStatsHelper.calcWifiPower(powerProfile, healthStats)); + snapshot.blueToothPower = DigitEntry.of(HealthStatsHelper.calcBlueToothPower(powerProfile, healthStats)); + snapshot.gpsPower = DigitEntry.of(HealthStatsHelper.calcGpsPower(powerProfile, healthStats)); + snapshot.sensorsPower = DigitEntry.of(HealthStatsHelper.calcSensorsPower(mCore.getContext(), healthStats)); + snapshot.cameraPower = DigitEntry.of(HealthStatsHelper.calcCameraPower(powerProfile, healthStats)); + snapshot.flashLightPower = DigitEntry.of(HealthStatsHelper.calcFlashLightPower(powerProfile, healthStats)); + snapshot.audioPower = DigitEntry.of(HealthStatsHelper.calcAudioPower(powerProfile, healthStats)); + snapshot.videoPower = DigitEntry.of(HealthStatsHelper.calcVideoPower(powerProfile, healthStats)); + snapshot.screenPower = DigitEntry.of(HealthStatsHelper.calcScreenPower(powerProfile, healthStats)); + snapshot.systemServicePower = DigitEntry.of(HealthStatsHelper.calcSystemServicePower(powerProfile, healthStats)); + snapshot.idlePower = DigitEntry.of(HealthStatsHelper.calcIdlePower(powerProfile, healthStats)); + } + } + + // Meta data + snapshot.cpuPowerMams = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_CPU_POWER_MAMS)); + snapshot.cpuUsrTimeMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_USER_CPU_TIME_MS)); + snapshot.cpuSysTimeMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_SYSTEM_CPU_TIME_MS)); + snapshot.realTimeMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_REALTIME_BATTERY_MS)); + snapshot.upTimeMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_UPTIME_BATTERY_MS)); + snapshot.offRealTimeMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_REALTIME_SCREEN_OFF_BATTERY_MS)); + snapshot.offUpTimeMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_UPTIME_SCREEN_OFF_BATTERY_MS)); + + snapshot.mobilePowerMams = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_POWER_MAMS)); + snapshot.mobileRadioActiveMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_MOBILE_RADIO_ACTIVE) / 1000L); + snapshot.mobileIdleMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_IDLE_MS)); + snapshot.mobileRxMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_RX_MS)); + snapshot.mobileTxMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_TX_MS)); + + snapshot.mobileRxBytes = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_RX_BYTES)); + snapshot.mobileTxBytes = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_TX_BYTES)); + snapshot.mobileRxPackets = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_RX_PACKETS)); + snapshot.mobileTxPackets = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_TX_PACKETS)); + + snapshot.wifiPowerMams = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_POWER_MAMS)); + snapshot.wifiIdleMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_IDLE_MS)); + snapshot.wifiRxMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_RX_MS)); + snapshot.wifiTxMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_TX_MS)); + snapshot.wifiRunningMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_RUNNING_MS)); + snapshot.wifiLockMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_FULL_LOCK_MS)); + snapshot.wifiScanMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_WIFI_SCAN)); + snapshot.wifiMulticastMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_MULTICAST_MS)); + snapshot.wifiRxBytes = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_RX_BYTES)); + snapshot.wifiTxBytes = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_TX_BYTES)); + snapshot.wifiRxPackets = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_RX_PACKETS)); + snapshot.wifiTxPackets = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_TX_PACKETS)); + + snapshot.blueToothPowerMams = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_BLUETOOTH_POWER_MAMS)); + snapshot.blueToothIdleMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_BLUETOOTH_IDLE_MS)); + snapshot.blueToothRxMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_BLUETOOTH_RX_MS)); + snapshot.blueToothTxMs = DigitEntry.of(HealthStatsHelper.getMeasure(healthStats, UidHealthStats.MEASUREMENT_BLUETOOTH_TX_MS)); + + { + if (healthStats.hasTimers(UidHealthStats.TIMERS_WAKELOCKS_PARTIAL)) { + long timeMs = 0; + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_WAKELOCKS_PARTIAL); + for (Map.Entry item : timers.entrySet()) { + String tag = item.getKey(); + long lockTime = item.getValue().getTime(); + if (snapshot.tagWakelocksPartialMs.isEmpty()) { + snapshot.tagWakelocksPartialMs = new HashMap<>(); + } + snapshot.tagWakelocksPartialMs.put(tag, DigitEntry.of(lockTime)); + timeMs += lockTime; + } + snapshot.wakelocksPartialMs = DigitEntry.of(timeMs); + } + if (healthStats.hasTimers(UidHealthStats.TIMERS_WAKELOCKS_FULL)) { + long timeMs = 0; + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_WAKELOCKS_FULL); + for (Map.Entry item : timers.entrySet()) { + String tag = item.getKey(); + long lockTime = item.getValue().getTime(); + if (snapshot.tagWakelocksFullMs.isEmpty()) { + snapshot.tagWakelocksFullMs = new HashMap<>(); + } + snapshot.tagWakelocksFullMs.put(tag, DigitEntry.of(lockTime)); + timeMs += lockTime; + } + snapshot.wakelocksFullMs = DigitEntry.of(timeMs); + } + if (healthStats.hasTimers(UidHealthStats.TIMERS_WAKELOCKS_WINDOW)) { + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_WAKELOCKS_WINDOW); + long timeMs = 0; + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + snapshot.wakelocksWindowMs = DigitEntry.of(timeMs); + } + if (healthStats.hasTimers(UidHealthStats.TIMERS_WAKELOCKS_DRAW)) { + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_WAKELOCKS_DRAW); + long timeMs = 0; + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + snapshot.wakelocksDrawMs = DigitEntry.of(timeMs); + } + if (healthStats.hasStats(UidHealthStats.STATS_PIDS)) { + long sum = 0; + Map pidStats = healthStats.getStats(UidHealthStats.STATS_PIDS); + for (HealthStats item : pidStats.values()) { + if (item.hasMeasurement(PidHealthStats.MEASUREMENT_WAKE_SUM_MS)) { + sum += item.getMeasurement(PidHealthStats.MEASUREMENT_WAKE_SUM_MS); + } + } + snapshot.wakelocksPidSum = DigitEntry.of(sum); + } + } + snapshot.gpsMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_GPS_SENSOR)); + if (healthStats.hasTimers(UidHealthStats.TIMERS_SENSORS)) { + SensorManager sm = (SensorManager) mCore.getContext().getSystemService(Context.SENSOR_SERVICE); + List sensorList = sm.getSensorList(Sensor.TYPE_ALL); + Map sensorMap = new HashMap<>(); + for (Sensor item : sensorList) { + try { + //noinspection JavaReflectionMemberAccess + @SuppressLint("DiscouragedPrivateApi") + Method method = item.getClass().getDeclaredMethod("getHandle"); + //noinspection ConstantConditions + int handle = (int) method.invoke(item); + sensorMap.put(String.valueOf(handle), item); + } catch (Throwable e) { + MatrixLog.w(TAG, "getSensorHandle err: " + e.getMessage()); + } + } + + long sensorsPowerMams = 0; + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_SENSORS); + for (Map.Entry item : timers.entrySet()) { + String handle = item.getKey(); + long timeMs = item.getValue().getTime(); + if (handle.equals("-10000")) { + continue; // skip GPS Sensors + } + Sensor sensor = sensorMap.get(handle); + if (sensor != null) { + sensorsPowerMams += sensor.getPower() * timeMs; + } + } + snapshot.sensorsPowerMams = DigitEntry.of(sensorsPowerMams); + } + snapshot.cameraMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_CAMERA)); + snapshot.flashLightMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_FLASHLIGHT)); + snapshot.audioMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_AUDIO)); + snapshot.videoMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_VIDEO)); + if (healthStats.hasTimers(UidHealthStats.TIMERS_JOBS)) { + long timeMs = 0; + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_JOBS); + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + snapshot.jobsMs = DigitEntry.of(timeMs); + } + if (healthStats.hasTimers(UidHealthStats.TIMERS_SYNCS)) { + long timeMs = 0; + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_SYNCS); + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + snapshot.syncMs = DigitEntry.of(timeMs); + } + + snapshot.fgActMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_FOREGROUND_ACTIVITY)); + snapshot.procTopAppMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_PROCESS_STATE_TOP_MS)); + snapshot.procTopSleepMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_PROCESS_STATE_TOP_SLEEPING_MS)); + snapshot.procFgMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_PROCESS_STATE_FOREGROUND_MS)); + snapshot.procFgSrvMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_PROCESS_STATE_FOREGROUND_SERVICE_MS)); + snapshot.procBgMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_PROCESS_STATE_BACKGROUND_MS)); + snapshot.procCacheMs = DigitEntry.of(HealthStatsHelper.getTimerTime(healthStats, UidHealthStats.TIMER_PROCESS_STATE_CACHED_MS)); + + { + if (healthStats.hasStats(UidHealthStats.STATS_PROCESSES)) { + Map processes = healthStats.getStats(UidHealthStats.STATS_PROCESSES); + for (Map.Entry item : processes.entrySet()) { + String pkg = item.getKey(); + HealthStats procStats = item.getValue(); + if (procStats != null) { + if (snapshot.procStatsCpuUsrTimeMs.isEmpty()) { + snapshot.procStatsCpuUsrTimeMs = new HashMap<>(); + } + snapshot.procStatsCpuUsrTimeMs.put(pkg, DigitEntry.of(HealthStatsHelper.getMeasure(procStats, ProcessHealthStats.MEASUREMENT_USER_TIME_MS))); + + if (snapshot.procStatsCpuSysTimeMs.isEmpty()) { + snapshot.procStatsCpuSysTimeMs = new HashMap<>(); + } + snapshot.procStatsCpuSysTimeMs.put(pkg, DigitEntry.of(HealthStatsHelper.getMeasure(procStats, ProcessHealthStats.MEASUREMENT_SYSTEM_TIME_MS))); + + if (snapshot.procStatsCpuFgTimeMs.isEmpty()) { + snapshot.procStatsCpuFgTimeMs = new HashMap<>(); + } + snapshot.procStatsCpuFgTimeMs.put(pkg, DigitEntry.of(HealthStatsHelper.getMeasure(procStats, ProcessHealthStats.MEASUREMENT_FOREGROUND_MS))); + + if (snapshot.procStatsStartCount.isEmpty()) { + snapshot.procStatsStartCount = new HashMap<>(); + } + snapshot.procStatsStartCount.put(pkg, DigitEntry.of(HealthStatsHelper.getMeasure(procStats, ProcessHealthStats.MEASUREMENT_STARTS_COUNT))); + } + } + } + } + } + return snapshot; + } + + public static class HealthStatsSnapshot extends Snapshot { + @Nullable + public AccCollector accCollector; + + @VisibleForTesting + @Nullable + public HealthStats healthStats; + // For test & tunings values + public Map extras = Collections.emptyMap(); + + // Estimated Powers + public DigitEntry cpuPower = DigitEntry.of(0D); + public DigitEntry wakelocksPower = DigitEntry.of(0D); + public DigitEntry mobilePower = DigitEntry.of(0D); + public DigitEntry wifiPower = DigitEntry.of(0D); + public DigitEntry blueToothPower = DigitEntry.of(0D); + public DigitEntry gpsPower = DigitEntry.of(0D); + public DigitEntry sensorsPower = DigitEntry.of(0D); + public DigitEntry cameraPower = DigitEntry.of(0D); + public DigitEntry flashLightPower = DigitEntry.of(0D); + public DigitEntry audioPower = DigitEntry.of(0D); + public DigitEntry videoPower = DigitEntry.of(0D); + public DigitEntry screenPower = DigitEntry.of(0D); + public DigitEntry systemServicePower = DigitEntry.of(0D); // WIP + public DigitEntry idlePower = DigitEntry.of(0D); + + // Meta Data: + // CPU + public DigitEntry cpuPowerMams = DigitEntry.of(0L); + public DigitEntry cpuUsrTimeMs = DigitEntry.of(0L); + public DigitEntry cpuSysTimeMs = DigitEntry.of(0L); + public DigitEntry realTimeMs = DigitEntry.of(0L); + public DigitEntry upTimeMs = DigitEntry.of(0L); + public DigitEntry offRealTimeMs = DigitEntry.of(0L); + public DigitEntry offUpTimeMs = DigitEntry.of(0L); + + // Network + public DigitEntry mobilePowerMams = DigitEntry.of(0L); + public DigitEntry mobileRadioActiveMs = DigitEntry.of(0L); + public DigitEntry mobileIdleMs = DigitEntry.of(0L); + public DigitEntry mobileRxMs = DigitEntry.of(0L); + public DigitEntry mobileTxMs = DigitEntry.of(0L); + public DigitEntry mobileRxBytes = DigitEntry.of(0L); + public DigitEntry mobileTxBytes = DigitEntry.of(0L); + public DigitEntry mobileRxPackets = DigitEntry.of(0L); + public DigitEntry mobileTxPackets = DigitEntry.of(0L); + + public DigitEntry wifiPowerMams = DigitEntry.of(0L); + public DigitEntry wifiIdleMs = DigitEntry.of(0L); + public DigitEntry wifiRxMs = DigitEntry.of(0L); + public DigitEntry wifiTxMs = DigitEntry.of(0L); + public DigitEntry wifiRunningMs = DigitEntry.of(0L); + public DigitEntry wifiLockMs = DigitEntry.of(0L); + public DigitEntry wifiScanMs = DigitEntry.of(0L); + public DigitEntry wifiMulticastMs = DigitEntry.of(0L); + public DigitEntry wifiRxBytes = DigitEntry.of(0L); + public DigitEntry wifiTxBytes = DigitEntry.of(0L); + public DigitEntry wifiRxPackets = DigitEntry.of(0L); + public DigitEntry wifiTxPackets = DigitEntry.of(0L); + + public DigitEntry blueToothPowerMams = DigitEntry.of(0L); + public DigitEntry blueToothIdleMs = DigitEntry.of(0L); + public DigitEntry blueToothRxMs = DigitEntry.of(0L); + public DigitEntry blueToothTxMs = DigitEntry.of(0L); + + // SystemService & Media + public DigitEntry wakelocksPartialMs = DigitEntry.of(0L); + public DigitEntry wakelocksFullMs = DigitEntry.of(0L); + public DigitEntry wakelocksWindowMs = DigitEntry.of(0L); + public DigitEntry wakelocksDrawMs = DigitEntry.of(0L); + public DigitEntry wakelocksPidSum = DigitEntry.of(0L); + public DigitEntry gpsMs = DigitEntry.of(0L); + public DigitEntry sensorsPowerMams = DigitEntry.of(0L); + public DigitEntry cameraMs = DigitEntry.of(0L); + public DigitEntry flashLightMs = DigitEntry.of(0L); + public DigitEntry audioMs = DigitEntry.of(0L); + public DigitEntry videoMs = DigitEntry.of(0L); + public DigitEntry jobsMs = DigitEntry.of(0L); + public DigitEntry syncMs = DigitEntry.of(0L); + + // Foreground + public DigitEntry fgActMs = DigitEntry.of(0L); + public DigitEntry procTopAppMs = DigitEntry.of(0L); + public DigitEntry procTopSleepMs = DigitEntry.of(0L); + public DigitEntry procFgMs = DigitEntry.of(0L); + public DigitEntry procFgSrvMs = DigitEntry.of(0L); + public DigitEntry procBgMs = DigitEntry.of(0L); + public DigitEntry procCacheMs = DigitEntry.of(0L); + + // Map: Nested data in collections + // Process + public Map> procStatsCpuUsrTimeMs = Collections.emptyMap(); + public Map> procStatsCpuSysTimeMs = Collections.emptyMap(); + public Map> procStatsCpuFgTimeMs = Collections.emptyMap(); + public Map> procStatsStartCount = Collections.emptyMap(); + // Wakelocks + public Map> tagWakelocksPartialMs = Collections.emptyMap(); + public Map> tagWakelocksFullMs = Collections.emptyMap(); + + + public double getTotalPower() { + return cpuPower.get() + + wakelocksPower.get() + + mobilePower.get() + + wifiPower.get() + + blueToothPower.get() + + gpsPower.get() + + sensorsPower.get() + + flashLightPower.get() + + audioPower.get() + + videoPower.get() + + screenPower.get() + // + cameraPower.get() // WIP + // + systemServicePower.get() // WIP + + idlePower.get(); + } + + public AccCollector startAccCollecting() { + accCollector = new AccCollector(this); + return accCollector; + } + + @Nullable + public Delta accCollect(HealthStatsSnapshot curr) { + if (accCollector == null) { + throw new IllegalStateException("Call start collect first!"); + } + return accCollector.collect(curr); + } + + public Delta diffByAccCollector(HealthStatsSnapshot bgn) { + if (bgn.accCollector == null) { + throw new IllegalStateException("Call start collect first!"); + } + bgn.accCollect(this); + HealthStatsSnapshot delta = bgn.accCollector.accDelta; + return new Delta.SimpleDelta<>(bgn, this, delta); + } + + Delta diffInternal(HealthStatsSnapshot bgn) { + return new Delta(bgn, this) { + @Override + protected HealthStatsSnapshot computeDelta() { + HealthStatsSnapshot delta = new HealthStatsSnapshot(); + + // UID + delta.cpuPower = Differ.DigitDiffer.globalDiff(bgn.cpuPower, end.cpuPower); + delta.wakelocksPower = Differ.DigitDiffer.globalDiff(bgn.wakelocksPower, end.wakelocksPower); + delta.mobilePower = Differ.DigitDiffer.globalDiff(bgn.mobilePower, end.mobilePower); + delta.wifiPower = Differ.DigitDiffer.globalDiff(bgn.wifiPower, end.wifiPower); + delta.blueToothPower = Differ.DigitDiffer.globalDiff(bgn.blueToothPower, end.blueToothPower); + delta.gpsPower = Differ.DigitDiffer.globalDiff(bgn.gpsPower, end.gpsPower); + delta.sensorsPower = Differ.DigitDiffer.globalDiff(bgn.sensorsPower, end.sensorsPower); + delta.cameraPower = Differ.DigitDiffer.globalDiff(bgn.cameraPower, end.cameraPower); + delta.flashLightPower = Differ.DigitDiffer.globalDiff(bgn.flashLightPower, end.flashLightPower); + delta.audioPower = Differ.DigitDiffer.globalDiff(bgn.audioPower, end.audioPower); + delta.videoPower = Differ.DigitDiffer.globalDiff(bgn.videoPower, end.videoPower); + delta.screenPower = Differ.DigitDiffer.globalDiff(bgn.screenPower, end.screenPower); + delta.systemServicePower = Differ.DigitDiffer.globalDiff(bgn.systemServicePower, end.systemServicePower); + delta.idlePower = Differ.DigitDiffer.globalDiff(bgn.idlePower, end.idlePower); + + delta.cpuPowerMams = Differ.DigitDiffer.globalDiff(bgn.cpuPowerMams, end.cpuPowerMams); + delta.cpuUsrTimeMs = Differ.DigitDiffer.globalDiff(bgn.cpuUsrTimeMs, end.cpuUsrTimeMs); + delta.cpuSysTimeMs = Differ.DigitDiffer.globalDiff(bgn.cpuSysTimeMs, end.cpuSysTimeMs); + delta.realTimeMs = Differ.DigitDiffer.globalDiff(bgn.realTimeMs, end.realTimeMs); + delta.upTimeMs = Differ.DigitDiffer.globalDiff(bgn.upTimeMs, end.upTimeMs); + + delta.mobilePowerMams = Differ.DigitDiffer.globalDiff(bgn.mobilePowerMams, end.mobilePowerMams); + delta.mobileRadioActiveMs = Differ.DigitDiffer.globalDiff(bgn.mobileRadioActiveMs, end.mobileRadioActiveMs); + delta.mobileIdleMs = Differ.DigitDiffer.globalDiff(bgn.mobileIdleMs, end.mobileIdleMs); + delta.mobileRxMs = Differ.DigitDiffer.globalDiff(bgn.mobileRxMs, end.mobileRxMs); + delta.mobileTxMs = Differ.DigitDiffer.globalDiff(bgn.mobileTxMs, end.mobileTxMs); + delta.mobileRxBytes = Differ.DigitDiffer.globalDiff(bgn.mobileRxBytes, end.mobileRxBytes); + delta.mobileTxBytes = Differ.DigitDiffer.globalDiff(bgn.mobileTxBytes, end.mobileTxBytes); + delta.mobileRxPackets = Differ.DigitDiffer.globalDiff(bgn.mobileRxPackets, end.mobileRxPackets); + delta.mobileTxPackets = Differ.DigitDiffer.globalDiff(bgn.mobileTxPackets, end.mobileTxPackets); + + + delta.wifiPowerMams = Differ.DigitDiffer.globalDiff(bgn.wifiPowerMams, end.wifiPowerMams); + delta.wifiIdleMs = Differ.DigitDiffer.globalDiff(bgn.wifiIdleMs, end.wifiIdleMs); + delta.wifiRxMs = Differ.DigitDiffer.globalDiff(bgn.wifiRxMs, end.wifiRxMs); + delta.wifiTxMs = Differ.DigitDiffer.globalDiff(bgn.wifiTxMs, end.wifiTxMs); + delta.wifiRunningMs = Differ.DigitDiffer.globalDiff(bgn.wifiRunningMs, end.wifiRunningMs); + delta.wifiLockMs = Differ.DigitDiffer.globalDiff(bgn.wifiLockMs, end.wifiLockMs); + delta.wifiScanMs = Differ.DigitDiffer.globalDiff(bgn.wifiScanMs, end.wifiScanMs); + delta.wifiMulticastMs = Differ.DigitDiffer.globalDiff(bgn.wifiMulticastMs, end.wifiMulticastMs); + delta.wifiRxBytes = Differ.DigitDiffer.globalDiff(bgn.wifiRxBytes, end.wifiRxBytes); + delta.wifiTxBytes = Differ.DigitDiffer.globalDiff(bgn.wifiTxBytes, end.wifiTxBytes); + delta.wifiRxPackets = Differ.DigitDiffer.globalDiff(bgn.wifiRxPackets, end.wifiRxPackets); + delta.wifiTxPackets = Differ.DigitDiffer.globalDiff(bgn.wifiTxPackets, end.wifiTxPackets); + + delta.blueToothPowerMams = Differ.DigitDiffer.globalDiff(bgn.blueToothPowerMams, end.blueToothPowerMams); + delta.blueToothIdleMs = Differ.DigitDiffer.globalDiff(bgn.blueToothIdleMs, end.blueToothIdleMs); + delta.blueToothRxMs = Differ.DigitDiffer.globalDiff(bgn.blueToothRxMs, end.blueToothRxMs); + delta.blueToothTxMs = Differ.DigitDiffer.globalDiff(bgn.blueToothTxMs, end.blueToothTxMs); + + delta.wakelocksPartialMs = Differ.DigitDiffer.globalDiff(bgn.wakelocksPartialMs, end.wakelocksPartialMs); + delta.wakelocksFullMs = Differ.DigitDiffer.globalDiff(bgn.wakelocksFullMs, end.wakelocksFullMs); + delta.wakelocksWindowMs = Differ.DigitDiffer.globalDiff(bgn.wakelocksWindowMs, end.wakelocksWindowMs); + delta.wakelocksDrawMs = Differ.DigitDiffer.globalDiff(bgn.wakelocksDrawMs, end.wakelocksDrawMs); + delta.wakelocksPidSum = Differ.DigitDiffer.globalDiff(bgn.wakelocksPidSum, end.wakelocksPidSum); + delta.gpsMs = Differ.DigitDiffer.globalDiff(bgn.gpsMs, end.gpsMs); + delta.sensorsPowerMams = Differ.DigitDiffer.globalDiff(bgn.sensorsPowerMams, end.sensorsPowerMams); + delta.cameraMs = Differ.DigitDiffer.globalDiff(bgn.cameraMs, end.cameraMs); + delta.flashLightMs = Differ.DigitDiffer.globalDiff(bgn.flashLightMs, end.flashLightMs); + delta.audioMs = Differ.DigitDiffer.globalDiff(bgn.audioMs, end.audioMs); + delta.videoMs = Differ.DigitDiffer.globalDiff(bgn.videoMs, end.videoMs); + delta.jobsMs = Differ.DigitDiffer.globalDiff(bgn.jobsMs, end.jobsMs); + delta.syncMs = Differ.DigitDiffer.globalDiff(bgn.syncMs, end.syncMs); + + delta.fgActMs = Differ.DigitDiffer.globalDiff(bgn.fgActMs, end.fgActMs); + delta.procTopAppMs = Differ.DigitDiffer.globalDiff(bgn.procTopAppMs, end.procTopAppMs); + delta.procTopSleepMs = Differ.DigitDiffer.globalDiff(bgn.procTopSleepMs, end.procTopSleepMs); + delta.procFgMs = Differ.DigitDiffer.globalDiff(bgn.procFgMs, end.procFgMs); + delta.procFgSrvMs = Differ.DigitDiffer.globalDiff(bgn.procFgSrvMs, end.procFgSrvMs); + delta.procBgMs = Differ.DigitDiffer.globalDiff(bgn.procBgMs, end.procBgMs); + delta.procCacheMs = Differ.DigitDiffer.globalDiff(bgn.procCacheMs, end.procCacheMs); + + // PID + { + Map> map = new HashMap<>(); + for (Map.Entry> entry : end.procStatsCpuUsrTimeMs.entrySet()) { + String key = entry.getKey(); + DigitEntry endEntry = entry.getValue(); + long bgnValue = 0L; + DigitEntry bgnEntry = bgn.procStatsCpuUsrTimeMs.get(key); + if (bgnEntry != null) { + bgnValue = bgnEntry.get(); + } + map.put(key, Differ.DigitDiffer.globalDiff(DigitEntry.of(bgnValue), endEntry)); + } + delta.procStatsCpuUsrTimeMs = map; + } + { + Map> map = new HashMap<>(); + for (Map.Entry> entry : end.procStatsCpuSysTimeMs.entrySet()) { + String key = entry.getKey(); + DigitEntry endEntry = entry.getValue(); + long bgnValue = 0L; + DigitEntry bgnEntry = bgn.procStatsCpuSysTimeMs.get(key); + if (bgnEntry != null) { + bgnValue = bgnEntry.get(); + } + map.put(key, Differ.DigitDiffer.globalDiff(DigitEntry.of(bgnValue), endEntry)); + } + delta.procStatsCpuSysTimeMs = map; + } + { + Map> map = new HashMap<>(); + for (Map.Entry> entry : end.procStatsCpuFgTimeMs.entrySet()) { + String key = entry.getKey(); + DigitEntry endEntry = entry.getValue(); + long bgnValue = 0L; + DigitEntry bgnEntry = bgn.procStatsCpuFgTimeMs.get(key); + if (bgnEntry != null) { + bgnValue = bgnEntry.get(); + } + map.put(key, Differ.DigitDiffer.globalDiff(DigitEntry.of(bgnValue), endEntry)); + } + delta.procStatsCpuFgTimeMs = map; + } + { + Map> map = new HashMap<>(); + for (Map.Entry> entry : end.procStatsStartCount.entrySet()) { + String key = entry.getKey(); + DigitEntry endEntry = entry.getValue(); + long bgnValue = 0L; + DigitEntry bgnEntry = bgn.procStatsStartCount.get(key); + if (bgnEntry != null) { + bgnValue = bgnEntry.get(); + } + map.put(key, Differ.DigitDiffer.globalDiff(DigitEntry.of(bgnValue), endEntry)); + } + delta.procStatsStartCount = map; + } + { + Map> map = new HashMap<>(); + for (Map.Entry> entry : end.tagWakelocksPartialMs.entrySet()) { + String key = entry.getKey(); + DigitEntry endEntry = entry.getValue(); + long bgnValue = 0L; + DigitEntry bgnEntry = bgn.tagWakelocksPartialMs.get(key); + if (bgnEntry != null) { + bgnValue = bgnEntry.get(); + } + map.put(key, Differ.DigitDiffer.globalDiff(DigitEntry.of(bgnValue), endEntry)); + } + delta.tagWakelocksPartialMs = map; + } + { + Map> map = new HashMap<>(); + for (Map.Entry> entry : end.tagWakelocksFullMs.entrySet()) { + String key = entry.getKey(); + DigitEntry endEntry = entry.getValue(); + long bgnValue = 0L; + DigitEntry bgnEntry = bgn.tagWakelocksFullMs.get(key); + if (bgnEntry != null) { + bgnValue = bgnEntry.get(); + } + map.put(key, Differ.DigitDiffer.globalDiff(DigitEntry.of(bgnValue), endEntry)); + } + delta.tagWakelocksFullMs = map; + } + return delta; + } + }; + } + + @Override + public Delta diff(HealthStatsSnapshot bgn) { + Delta delta = diffInternal(bgn); + // Sort + delta.dlt.procStatsCpuUsrTimeMs = decrease(procStatsCpuUsrTimeMs); + delta.dlt.procStatsCpuSysTimeMs = decrease(procStatsCpuSysTimeMs); + delta.dlt.procStatsCpuFgTimeMs = decrease(procStatsCpuFgTimeMs); + delta.dlt.procStatsStartCount = decrease(procStatsStartCount); + delta.dlt.tagWakelocksPartialMs = decrease(tagWakelocksPartialMs); + delta.dlt.tagWakelocksFullMs = decrease(tagWakelocksFullMs); + return delta; + } + + private Map> decrease(Map> input) { + return BatteryCanaryUtil.sortMapByValue(input, new Comparator>>() { + @Override + public int compare(Map.Entry> o1, Map.Entry> o2) { + long sumLeft = o1.getValue().get(), sumRight = o2.getValue().get(); + long minus = sumLeft - sumRight; + if (minus == 0) return 0; + if (minus > 0) return -1; + return 1; + } + }); + } + + public static double getPower(@NonNull Map extra, String key) { + Object val = extra.get(key); + if (val instanceof Double) { + return (double) val; + } + return 0; + } + + + public static class AccCollector { + public int count; + public long beginMs; + public long duringMs; + public HealthStatsSnapshot last; + public HealthStatsSnapshot accDelta; + + public AccCollector(HealthStatsSnapshot bgn) { + beginMs = bgn.time; + last = bgn; + accDelta = new HealthStatsSnapshot(); + accDelta.procStatsCpuUsrTimeMs = new HashMap<>(); + accDelta.procStatsCpuSysTimeMs = new HashMap<>(); + accDelta.procStatsCpuFgTimeMs = new HashMap<>(); + accDelta.procStatsStartCount = new HashMap<>(); + accDelta.tagWakelocksPartialMs = new HashMap<>(); + accDelta.tagWakelocksFullMs = new HashMap<>(); + } + + @Nullable + public Delta collect(HealthStatsSnapshot curr) { + Delta delta = null; + if (isHealthStatsNotReset(last, curr)) { + delta = curr.diffInternal(last); + + accDelta.cpuPower = DigitEntry.of(accDelta.cpuPower.get() + delta.dlt.cpuPower.get()); + accDelta.wakelocksPower = DigitEntry.of(accDelta.wakelocksPower.get() + delta.dlt.wakelocksPower.get()); + accDelta.mobilePower = DigitEntry.of(accDelta.mobilePower.get() + delta.dlt.mobilePower.get()); + accDelta.wifiPower = DigitEntry.of(accDelta.wifiPower.get() + delta.dlt.wifiPower.get()); + accDelta.blueToothPower = DigitEntry.of(accDelta.blueToothPower.get() + delta.dlt.blueToothPower.get()); + accDelta.gpsPower = DigitEntry.of(accDelta.gpsPower.get() + delta.dlt.gpsPower.get()); + accDelta.sensorsPower = DigitEntry.of(accDelta.sensorsPower.get() + delta.dlt.sensorsPower.get()); + accDelta.cameraPower = DigitEntry.of(accDelta.cameraPower.get() + delta.dlt.cameraPower.get()); + accDelta.flashLightPower = DigitEntry.of(accDelta.flashLightPower.get() + delta.dlt.flashLightPower.get()); + accDelta.audioPower = DigitEntry.of(accDelta.audioPower.get() + delta.dlt.audioPower.get()); + accDelta.videoPower = DigitEntry.of(accDelta.videoPower.get() + delta.dlt.videoPower.get()); + accDelta.screenPower = DigitEntry.of(accDelta.screenPower.get() + delta.dlt.screenPower.get()); + accDelta.systemServicePower = DigitEntry.of(accDelta.systemServicePower.get() + delta.dlt.systemServicePower.get()); + accDelta.idlePower = DigitEntry.of(accDelta.idlePower.get() + delta.dlt.idlePower.get()); + + accDelta.cpuPowerMams = DigitEntry.of(accDelta.cpuPowerMams.get() + delta.dlt.cpuPowerMams.get()); + accDelta.cpuUsrTimeMs = DigitEntry.of(accDelta.cpuUsrTimeMs.get() + delta.dlt.cpuUsrTimeMs.get()); + accDelta.cpuSysTimeMs = DigitEntry.of(accDelta.cpuSysTimeMs.get() + delta.dlt.cpuSysTimeMs.get()); + accDelta.realTimeMs = DigitEntry.of(accDelta.realTimeMs.get() + delta.dlt.realTimeMs.get()); + accDelta.upTimeMs = DigitEntry.of(accDelta.upTimeMs.get() + delta.dlt.upTimeMs.get()); + + accDelta.mobilePowerMams = DigitEntry.of(accDelta.mobilePowerMams.get() + delta.dlt.mobilePowerMams.get()); + accDelta.mobileRadioActiveMs = DigitEntry.of(accDelta.mobileRadioActiveMs.get() + delta.dlt.mobileRadioActiveMs.get()); + accDelta.mobileIdleMs = DigitEntry.of(accDelta.mobileIdleMs.get() + delta.dlt.mobileIdleMs.get()); + accDelta.mobileRxMs = DigitEntry.of(accDelta.mobileRxMs.get() + delta.dlt.mobileRxMs.get()); + accDelta.mobileTxMs = DigitEntry.of(accDelta.mobileTxMs.get() + delta.dlt.mobileTxMs.get()); + accDelta.mobileRxBytes = DigitEntry.of(accDelta.mobileRxBytes.get() + delta.dlt.mobileRxBytes.get()); + accDelta.mobileTxBytes = DigitEntry.of(accDelta.mobileTxBytes.get() + delta.dlt.mobileTxBytes.get()); + accDelta.mobileRxPackets = DigitEntry.of(accDelta.mobileRxPackets.get() + delta.dlt.mobileRxPackets.get()); + accDelta.mobileTxPackets = DigitEntry.of(accDelta.mobileTxPackets.get() + delta.dlt.mobileTxPackets.get()); + + + accDelta.wifiPowerMams = DigitEntry.of(accDelta.wifiPowerMams.get() + delta.dlt.wifiPowerMams.get()); + accDelta.wifiIdleMs = DigitEntry.of(accDelta.wifiIdleMs.get() + delta.dlt.wifiIdleMs.get()); + accDelta.wifiRxMs = DigitEntry.of(accDelta.wifiRxMs.get() + delta.dlt.wifiRxMs.get()); + accDelta.wifiTxMs = DigitEntry.of(accDelta.wifiTxMs.get() + delta.dlt.wifiTxMs.get()); + accDelta.wifiRunningMs = DigitEntry.of(accDelta.wifiRunningMs.get() + delta.dlt.wifiRunningMs.get()); + accDelta.wifiLockMs = DigitEntry.of(accDelta.wifiLockMs.get() + delta.dlt.wifiLockMs.get()); + accDelta.wifiScanMs = DigitEntry.of(accDelta.wifiScanMs.get() + delta.dlt.wifiScanMs.get()); + accDelta.wifiMulticastMs = DigitEntry.of(accDelta.wifiMulticastMs.get() + delta.dlt.wifiMulticastMs.get()); + accDelta.wifiRxBytes = DigitEntry.of(accDelta.wifiRxBytes.get() + delta.dlt.wifiRxBytes.get()); + accDelta.wifiTxBytes = DigitEntry.of(accDelta.wifiTxBytes.get() + delta.dlt.wifiTxBytes.get()); + accDelta.wifiRxPackets = DigitEntry.of(accDelta.wifiRxPackets.get() + delta.dlt.wifiRxPackets.get()); + accDelta.wifiTxPackets = DigitEntry.of(accDelta.wifiTxPackets.get() + delta.dlt.wifiTxPackets.get()); + + accDelta.blueToothPowerMams = DigitEntry.of(accDelta.blueToothPowerMams.get() + delta.dlt.blueToothPowerMams.get()); + accDelta.blueToothIdleMs = DigitEntry.of(accDelta.blueToothIdleMs.get() + delta.dlt.blueToothIdleMs.get()); + accDelta.blueToothRxMs = DigitEntry.of(accDelta.blueToothRxMs.get() + delta.dlt.blueToothRxMs.get()); + accDelta.blueToothTxMs = DigitEntry.of(accDelta.blueToothTxMs.get() + delta.dlt.blueToothTxMs.get()); + + accDelta.wakelocksPartialMs = DigitEntry.of(accDelta.wakelocksPartialMs.get() + delta.dlt.wakelocksPartialMs.get()); + accDelta.wakelocksFullMs = DigitEntry.of(accDelta.wakelocksFullMs.get() + delta.dlt.wakelocksFullMs.get()); + accDelta.wakelocksWindowMs = DigitEntry.of(accDelta.wakelocksWindowMs.get() + delta.dlt.wakelocksWindowMs.get()); + accDelta.wakelocksDrawMs = DigitEntry.of(accDelta.wakelocksDrawMs.get() + delta.dlt.wakelocksDrawMs.get()); + accDelta.wakelocksPidSum = DigitEntry.of(accDelta.wakelocksPidSum.get() + delta.dlt.wakelocksPidSum.get()); + accDelta.gpsMs = DigitEntry.of(accDelta.gpsMs.get() + delta.dlt.gpsMs.get()); + accDelta.sensorsPowerMams = DigitEntry.of(accDelta.sensorsPowerMams.get() + delta.dlt.sensorsPowerMams.get()); + accDelta.cameraMs = DigitEntry.of(accDelta.cameraMs.get() + delta.dlt.cameraMs.get()); + accDelta.flashLightMs = DigitEntry.of(accDelta.flashLightMs.get() + delta.dlt.flashLightMs.get()); + accDelta.audioMs = DigitEntry.of(accDelta.audioMs.get() + delta.dlt.audioMs.get()); + accDelta.videoMs = DigitEntry.of(accDelta.videoMs.get() + delta.dlt.videoMs.get()); + accDelta.jobsMs = DigitEntry.of(accDelta.jobsMs.get() + delta.dlt.jobsMs.get()); + accDelta.syncMs = DigitEntry.of(accDelta.syncMs.get() + delta.dlt.syncMs.get()); + + accDelta.fgActMs = DigitEntry.of(accDelta.fgActMs.get() + delta.dlt.fgActMs.get()); + accDelta.procTopAppMs = DigitEntry.of(accDelta.procTopAppMs.get() + delta.dlt.procTopAppMs.get()); + accDelta.procTopSleepMs = DigitEntry.of(accDelta.procTopSleepMs.get() + delta.dlt.procTopSleepMs.get()); + accDelta.procFgMs = DigitEntry.of(accDelta.procFgMs.get() + delta.dlt.procFgMs.get()); + accDelta.procFgSrvMs = DigitEntry.of(accDelta.procFgSrvMs.get() + delta.dlt.procFgSrvMs.get()); + accDelta.procBgMs = DigitEntry.of(accDelta.procBgMs.get() + delta.dlt.procBgMs.get()); + accDelta.procCacheMs = DigitEntry.of(accDelta.procCacheMs.get() + delta.dlt.procCacheMs.get()); + + for (Map.Entry> entry : delta.dlt.procStatsCpuUsrTimeMs.entrySet()) { + String key = entry.getKey(); + DigitEntry val = entry.getValue(); + DigitEntry acc = accDelta.procStatsCpuUsrTimeMs.get(key); + accDelta.procStatsCpuUsrTimeMs.put(key, DigitEntry.of(val.get() + (acc == null ? 0 : acc.get()))); + } + for (Map.Entry> entry : delta.dlt.procStatsCpuSysTimeMs.entrySet()) { + String key = entry.getKey(); + DigitEntry val = entry.getValue(); + DigitEntry acc = accDelta.procStatsCpuSysTimeMs.get(key); + accDelta.procStatsCpuSysTimeMs.put(key, DigitEntry.of(val.get() + (acc == null ? 0 : acc.get()))); + } + for (Map.Entry> entry : delta.dlt.procStatsCpuFgTimeMs.entrySet()) { + String key = entry.getKey(); + DigitEntry val = entry.getValue(); + DigitEntry acc = accDelta.procStatsCpuFgTimeMs.get(key); + accDelta.procStatsCpuFgTimeMs.put(key, DigitEntry.of(val.get() + (acc == null ? 0 : acc.get()))); + } + for (Map.Entry> entry : delta.dlt.procStatsStartCount.entrySet()) { + String key = entry.getKey(); + DigitEntry val = entry.getValue(); + DigitEntry acc = accDelta.procStatsStartCount.get(key); + accDelta.procStatsStartCount.put(key, DigitEntry.of(val.get() + (acc == null ? 0 : acc.get()))); + } + for (Map.Entry> entry : delta.dlt.tagWakelocksPartialMs.entrySet()) { + String key = entry.getKey(); + DigitEntry val = entry.getValue(); + DigitEntry acc = accDelta.tagWakelocksPartialMs.get(key); + accDelta.tagWakelocksPartialMs.put(key, DigitEntry.of(val.get() + (acc == null ? 0 : acc.get()))); + } + for (Map.Entry> entry : delta.dlt.tagWakelocksFullMs.entrySet()) { + String key = entry.getKey(); + DigitEntry val = entry.getValue(); + DigitEntry acc = accDelta.tagWakelocksFullMs.get(key); + accDelta.tagWakelocksFullMs.put(key, DigitEntry.of(val.get() + (acc == null ? 0 : acc.get()))); + } + + count++; + duringMs += delta.during; + + } + last = curr; + return delta; + } + + public static boolean isHealthStatsNotReset(HealthStatsSnapshot bgn, HealthStatsSnapshot end) { + try { + assertNotNegative("cpuPowerMams", bgn.cpuPowerMams.get(), end.cpuPowerMams.get()); + assertNotNegative("cpuUsrTimeMs", bgn.cpuUsrTimeMs.get(), end.cpuUsrTimeMs.get()); + assertNotNegative("cpuSysTimeMs", bgn.cpuSysTimeMs.get(), end.cpuSysTimeMs.get()); + assertNotNegative("realTimeMs", bgn.realTimeMs.get(), end.realTimeMs.get()); + assertNotNegative("upTimeMs", bgn.upTimeMs.get(), end.upTimeMs.get()); + assertNotNegative("offRealTimeMs", bgn.offRealTimeMs.get(), end.offRealTimeMs.get()); + assertNotNegative("offUpTimeMs", bgn.offUpTimeMs.get(), end.offUpTimeMs.get()); + + assertNotNegative("mobilePowerMams", bgn.mobilePowerMams.get(), end.mobilePowerMams.get()); + assertNotNegative("mobileRadioActiveMs", bgn.mobileRadioActiveMs.get(), end.mobileRadioActiveMs.get()); + assertNotNegative("mobileIdleMs", bgn.mobileIdleMs.get(), end.mobileIdleMs.get()); + assertNotNegative("mobileRxMs", bgn.mobileRxMs.get(), end.mobileRxMs.get()); + assertNotNegative("mobileTxMs", bgn.mobileTxMs.get(), end.mobileTxMs.get()); + assertNotNegative("mobileRxBytes", bgn.mobileRxBytes.get(), end.mobileRxBytes.get()); + assertNotNegative("mobileTxBytes", bgn.mobileTxBytes.get(), end.mobileTxBytes.get()); + assertNotNegative("mobileRxPackets", bgn.mobileRxPackets.get(), end.mobileRxPackets.get()); + assertNotNegative("mobileTxPackets", bgn.mobileTxPackets.get(), end.mobileTxPackets.get()); + + assertNotNegative("wifiPowerMams", bgn.wifiPowerMams.get(), end.wifiPowerMams.get()); + assertNotNegative("wifiIdleMs", bgn.wifiIdleMs.get(), end.wifiIdleMs.get()); + assertNotNegative("wifiRxMs", bgn.wifiRxMs.get(), end.wifiRxMs.get()); + assertNotNegative("wifiTxMs", bgn.wifiTxMs.get(), end.wifiTxMs.get()); + assertNotNegative("wifiRunningMs", bgn.wifiRunningMs.get(), end.wifiRunningMs.get()); + assertNotNegative("wifiLockMs", bgn.wifiLockMs.get(), end.wifiLockMs.get()); + assertNotNegative("wifiScanMs", bgn.wifiScanMs.get(), end.wifiScanMs.get()); + assertNotNegative("wifiMulticastMs", bgn.wifiMulticastMs.get(), end.wifiMulticastMs.get()); + assertNotNegative("wifiRxBytes", bgn.wifiRxBytes.get(), end.wifiRxBytes.get()); + assertNotNegative("wifiTxBytes", bgn.wifiTxBytes.get(), end.wifiTxBytes.get()); + assertNotNegative("wifiRxPackets", bgn.wifiRxPackets.get(), end.wifiRxPackets.get()); + assertNotNegative("wifiTxPackets", bgn.wifiTxPackets.get(), end.wifiTxPackets.get()); + + assertNotNegative("blueToothPowerMams", bgn.blueToothPowerMams.get(), end.blueToothPowerMams.get()); + assertNotNegative("blueToothIdleMs", bgn.blueToothIdleMs.get(), end.blueToothIdleMs.get()); + assertNotNegative("blueToothRxMs", bgn.blueToothRxMs.get(), end.blueToothRxMs.get()); + assertNotNegative("blueToothTxMs", bgn.blueToothTxMs.get(), end.blueToothTxMs.get()); + + assertNotNegative("wakelocksPartialMs", bgn.wakelocksPartialMs.get(), end.wakelocksPartialMs.get()); + assertNotNegative("wakelocksFullMs", bgn.wakelocksFullMs.get(), end.wakelocksFullMs.get()); + assertNotNegative("wakelocksWindowMs", bgn.wakelocksWindowMs.get(), end.wakelocksWindowMs.get()); + assertNotNegative("wakelocksDrawMs", bgn.wakelocksDrawMs.get(), end.wakelocksDrawMs.get()); + assertNotNegative("wakelocksPidSum", bgn.wakelocksPidSum.get(), end.wakelocksPidSum.get()); + assertNotNegative("gpsMs", bgn.gpsMs.get(), end.gpsMs.get()); + assertNotNegative("sensorsPowerMams", bgn.sensorsPowerMams.get(), end.sensorsPowerMams.get()); + assertNotNegative("cameraMs", bgn.cameraMs.get(), end.cameraMs.get()); + assertNotNegative("flashLightMs", bgn.flashLightMs.get(), end.flashLightMs.get()); + assertNotNegative("audioMs", bgn.audioMs.get(), end.audioMs.get()); + assertNotNegative("videoMs", bgn.videoMs.get(), end.videoMs.get()); + assertNotNegative("jobsMs", bgn.jobsMs.get(), end.jobsMs.get()); + assertNotNegative("syncMs", bgn.syncMs.get(), end.syncMs.get()); + + return true; + } catch (Exception e) { + MatrixLog.w(TAG, "skip, " + e.getMessage()); + return false; + } + } + + static void assertNotNegative(String key, long bgn, long end) { + if (bgn > end) { + throw new IllegalStateException("negative stats: " + key); + } + } + } + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/HealthStatsHelper.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/HealthStatsHelper.java new file mode 100644 index 000000000..b4fb3b139 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/HealthStatsHelper.java @@ -0,0 +1,876 @@ +package com.tencent.matrix.batterycanary.stats; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.health.HealthStats; +import android.os.health.SystemHealthManager; +import android.os.health.TimerStat; +import android.os.health.UidHealthStats; + +import com.tencent.matrix.batterycanary.BatteryCanary; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature; +import com.tencent.matrix.batterycanary.monitor.feature.CpuStatFeature.CpuStateSnapshot; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.DigitEntry; +import com.tencent.matrix.batterycanary.monitor.feature.MonitorFeature.Snapshot.Entry.ListEntry; +import com.tencent.matrix.batterycanary.monitor.feature.TrafficMonitorFeature.RadioStatSnapshot; +import com.tencent.matrix.batterycanary.utils.PowerProfile; +import com.tencent.matrix.util.MatrixLog; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.ChecksSdkIntAtLeast; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; + +/** + * totalPowerMah = usagePowerMah + wifiPowerMah + gpsPowerMah + cpuPowerMah + + * sensorPowerMah + mobileRadioPowerMah + wakeLockPowerMah + cameraPowerMah + + * flashlightPowerMah + bluetoothPowerMah + audioPowerMah + videoPowerMah + * + systemServiceCpuPowerMah; + * if (customMeasuredPowerMah != null) { + * for (int idx = 0; idx < customMeasuredPowerMah.length; idx++) { + * totalPowerMah += customMeasuredPowerMah[idx]; + * } + * } + * // powerAttributedToOtherSippersMah is negative or zero + * totalPowerMah = totalPowerMah + powerReattributedToOtherSippersMah; + * totalSmearedPowerMah = totalPowerMah + screenPowerMah + proportionalSmearMah; + * + * @see com.android.internal.os.BatterySipper#sumPower + * @see com.android.internal.os.BatteryStatsHelper + * @see com.android.internal.os.BatteryStatsImpl.Uid + * + * @author Kaede + * @since 6/7/2022 + */ +@SuppressWarnings("JavadocReference") +@SuppressLint("RestrictedApi") +public final class HealthStatsHelper { + public static final String TAG = "HealthStatsHelper"; + + public static class UsageBasedPowerEstimator { + private static final double MILLIS_IN_HOUR = 1000.0 * 60 * 60; + private final double mAveragePowerMahPerMs; + public UsageBasedPowerEstimator(double averagePowerMilliAmp) { + mAveragePowerMahPerMs = averagePowerMilliAmp / MILLIS_IN_HOUR; + } + public boolean isSupported() { + return mAveragePowerMahPerMs != 0; + } + public double calculatePower(long durationMs) { + return mAveragePowerMahPerMs * durationMs; + } + } + + public static double round(double input, int decimalPlace) { + double decimal = Math.pow(10.0, decimalPlace); + return Math.round(input * decimal) / decimal; + } + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N) + public static boolean isSupported() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; + } + + @Nullable + public static HealthStats getCurrStats(Context context) { + if (isSupported()) { + try { + SystemHealthManager shm = (SystemHealthManager) context.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + return shm.takeMyUidSnapshot(); + } catch (Exception e) { + MatrixLog.w(TAG, "takeMyUidSnapshot err: " + e); + } + } + return null; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + static long getMeasure(HealthStats healthStats, int key) { + if (healthStats.hasMeasurement(key)) { + return healthStats.getMeasurement(key); + } + return 0L; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + static long getTimerTime(HealthStats healthStats, int key) { + if (healthStats.hasTimer(key)) { + return healthStats.getTimerTime(key); + } + return 0L; + } + + /** + * @see com.android.internal.os.CpuPowerCalculator + * @see com.android.internal.os.PowerProfile + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcCpuPower(PowerProfile powerProfile, HealthStats healthStats) { + // double mams = getMeasure(healthStats, UidHealthStats.MEASUREMENT_CPU_POWER_MAMS) / (UsageBasedPowerEstimator.MILLIS_IN_HOUR * 1000L); + // if (mams > 0) { + // MatrixLog.i(TAG, "estimate CPU by mams"); + // return mams; + // } + double power = 0; + /* + * POWER_CPU_SUSPEND: Power consumption when CPU is in power collapse mode. + * POWER_CPU_IDLE: Power consumption when CPU is awake (when a wake lock is held). This should + * be zero on devices that can go into full CPU power collapse even when a wake + * lock is held. Otherwise, this is the power consumption in addition to + * POWER_CPU_SUSPEND due to a wake lock being held but with no CPU activity. + * POWER_CPU_ACTIVE: Power consumption when CPU is running, excluding power consumed by clusters + * and cores. + * + * CPU Power Equation (assume two clusters): + * Total power = POWER_CPU_SUSPEND (always added) + * + POWER_CPU_IDLE (skip this and below if in power collapse mode) + * + POWER_CPU_ACTIVE (skip this and below if CPU is not running, but a wakelock + * is held) + * + cluster_power.cluster0 + cluster_power.cluster1 (skip cluster not running) + * + core_power.cluster0 * num running cores in cluster 0 + * + core_power.cluster1 * num running cores in cluster 1 + */ + long cpuTimeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_USER_CPU_TIME_MS) + getMeasure(healthStats, UidHealthStats.MEASUREMENT_SYSTEM_CPU_TIME_MS); + power += estimateCpuActivePower(powerProfile, cpuTimeMs); + CpuStatFeature feat = BatteryCanary.getMonitorFeature(CpuStatFeature.class); + if (feat != null && feat.isSupported()) { + CpuStateSnapshot snapshot = feat.currentCpuStateSnapshot(); + if (snapshot != null) { + power += estimateCpuClustersPower(powerProfile, snapshot, cpuTimeMs, false); + power += estimateCpuCoresPower(powerProfile, snapshot, cpuTimeMs, false); + } + } + if (power > 0) { + return power; + } + return 0; + } + + @VisibleForTesting + public static double estimateCpuActivePower(PowerProfile powerProfile, long cpuTimeMs) { + //noinspection UnnecessaryLocalVariable + long timeMs = cpuTimeMs; + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_CPU_ACTIVE); + return new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + + @VisibleForTesting + public static double estimateCpuClustersPower(PowerProfile powerProfile, CpuStateSnapshot snapshot, long cpuTimeMs, boolean scaled) { + boolean isUidStatsAvailable = false; + for (ListEntry> listEntry : snapshot.procCpuCoreStates) { + for (DigitEntry item : listEntry.getList()) { + if (item.get() > 0) { + isUidStatsAvailable = true; + break; + } + } + } + if (isUidStatsAvailable) { + return estimateCpuClustersPowerByUidStats(powerProfile, snapshot, cpuTimeMs, scaled); + } else { + MatrixLog.i(TAG, "estimate CPU by device stats"); + return estimateCpuClustersPowerByDevStats(powerProfile, snapshot, cpuTimeMs); + } + } + + @VisibleForTesting + public static double estimateCpuCoresPower(PowerProfile powerProfile, CpuStateSnapshot snapshot, long cpuTimeMs, boolean scaled) { + boolean isUidStatsAvailable = false; + for (ListEntry> listEntry : snapshot.procCpuCoreStates) { + for (DigitEntry item : listEntry.getList()) { + if (item.get() > 0) { + isUidStatsAvailable = true; + break; + } + } + } + if (isUidStatsAvailable) { + return estimateCpuCoresPowerByUidStats(powerProfile, snapshot, cpuTimeMs, scaled); + } else { + MatrixLog.i(TAG, "estimate CPU by device stats"); + return estimateCpuCoresPowerByDevStats(powerProfile, snapshot, cpuTimeMs); + } + } + + @VisibleForTesting + public static double estimateCpuClustersPowerByUidStats(PowerProfile powerProfile, CpuStateSnapshot snapshot, long cpuTimeMs, boolean scaled) { + if (cpuTimeMs > 0) { + /* + * procCpuCoreStates + * [ + * [step1Jiffies, step2Jiffies ...], // Cluster 1 + * [step1Jiffies, step2Jiffies ...], // Cluster 2 + * ... + * ] + */ + long jiffySum = 0; + for (int i = 0; i < snapshot.procCpuCoreStates.size(); i++) { + List> stepJiffies = snapshot.procCpuCoreStates.get(i).getList(); + int scale = scaled ? powerProfile.getNumCoresInCpuCluster(i) : 1; + for (DigitEntry item : stepJiffies) { + jiffySum += item.get() * scale; + } + } + double powerMah = 0; + for (int i = 0; i < snapshot.procCpuCoreStates.size(); i++) { + List> stepJiffies = snapshot.procCpuCoreStates.get(i).getList(); + int scale = scaled ? powerProfile.getNumCoresInCpuCluster(i) : 1; + long jiffySumInCluster = 0; + for (int j = 0; j < stepJiffies.size(); j++) { + long jiffy = stepJiffies.get(j).get(); + jiffySumInCluster += jiffy * scale; + } + long figuredCpuTimeMs = (long) ((jiffySumInCluster * 1.0f / jiffySum) * cpuTimeMs); + double powerMa = powerProfile.getAveragePowerForCpuCluster(i); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(figuredCpuTimeMs); + } + return powerMah; + } + return 0; + } + + @VisibleForTesting + public static double estimateCpuCoresPowerByUidStats(PowerProfile powerProfile, CpuStateSnapshot snapshot, long cpuTimeMs, boolean scaled) { + if (cpuTimeMs > 0) { + /* + * procCpuCoreStates + * [ + * [step1Jiffies, step2Jiffies ...], // Cluster 1 + * [step1Jiffies, step2Jiffies ...], // Cluster 2 + * ... + * ] + */ + long jiffySum = 0; + for (int i = 0; i < snapshot.procCpuCoreStates.size(); i++) { + List> stepJiffies = snapshot.procCpuCoreStates.get(i).getList(); + int scale = scaled ? powerProfile.getNumCoresInCpuCluster(i) : 1; + for (DigitEntry item : stepJiffies) { + jiffySum += item.get() * scale; + } + } + double powerMah = 0; + for (int i = 0; i < snapshot.procCpuCoreStates.size(); i++) { + List> stepJiffies = snapshot.procCpuCoreStates.get(i).getList(); + int scale = scaled ? powerProfile.getNumCoresInCpuCluster(i) : 1; + for (int j = 0; j < stepJiffies.size(); j++) { + long jiffy = stepJiffies.get(j).get(); + long figuredCpuTimeMs = (long) ((jiffy * scale * 1.0f / jiffySum) * cpuTimeMs); + double powerMa = powerProfile.getAveragePowerForCpuCore(i, j); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(figuredCpuTimeMs); + } + } + return powerMah; + } + return 0; + } + + @VisibleForTesting + public static double estimateCpuClustersPowerByDevStats(PowerProfile powerProfile, CpuStateSnapshot snapshot, long cpuTimeMs) { + if (cpuTimeMs > 0) { + /* + * cpuCoreStates + * [ + * [step1Jiffies, step2Jiffies ...], // CpuCore 1 + * [step1Jiffies, step2Jiffies ...], // CpuCore 2 + * ... + * ] + */ + long jiffySum = 0; + for (int i = 0; i < snapshot.cpuCoreStates.size(); i++) { + List> stepJiffies = snapshot.cpuCoreStates.get(i).getList(); + for (DigitEntry item : stepJiffies) { + jiffySum += item.get(); + } + } + double powerMah = 0; + for (int i = 0; i < snapshot.cpuCoreStates.size(); i++) { + List> stepJiffies = snapshot.cpuCoreStates.get(i).getList(); + long jiffySumInCluster = 0; + for (int j = 0; j < stepJiffies.size(); j++) { + long jiffy = stepJiffies.get(j).get(); + jiffySumInCluster += jiffy; + } + long figuredCpuTimeMs = (long) ((jiffySumInCluster * 1.0f / jiffySum) * cpuTimeMs); + int clusterNum = powerProfile.getClusterByCpuNum(i); + if (clusterNum >= 0 && clusterNum < powerProfile.getNumCpuClusters()) { + double powerMa = powerProfile.getAveragePowerForCpuCluster(clusterNum); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(figuredCpuTimeMs); + } + } + return powerMah; + } + return 0; + } + + @VisibleForTesting + public static double estimateCpuCoresPowerByDevStats(PowerProfile powerProfile, CpuStateSnapshot snapshot, long cpuTimeMs) { + if (cpuTimeMs > 0) { + /* + * cpuCoreStates + * [ + * [step1Jiffies, step2Jiffies ...], // CpuCore 1 + * [step1Jiffies, step2Jiffies ...], // CpuCore 2 + * ... + * ] + */ + long jiffySum = 0; + for (int i = 0; i < snapshot.cpuCoreStates.size(); i++) { + List> stepJiffies = snapshot.cpuCoreStates.get(i).getList(); + for (DigitEntry item : stepJiffies) { + jiffySum += item.get(); + } + } + double powerMah = 0; + for (int i = 0; i < snapshot.cpuCoreStates.size(); i++) { + List> stepJiffies = snapshot.cpuCoreStates.get(i).getList(); + for (int j = 0; j < stepJiffies.size(); j++) { + long jiffy = stepJiffies.get(j).get(); + long figuredCpuTimeMs = (long) ((jiffy * 1.0f / jiffySum) * cpuTimeMs); + int clusterNum = powerProfile.getClusterByCpuNum(i); + if (clusterNum >= 0 && clusterNum < powerProfile.getNumCpuClusters()) { + double powerMa = powerProfile.getAveragePowerForCpuCore(clusterNum, j); + powerMah += new UsageBasedPowerEstimator(powerMa).calculatePower(figuredCpuTimeMs); + } + } + } + return powerMah; + } + return 0; + } + + /** + * WIP + * Memory TimeStats support needed, see "com.android.internal.os.KernelMemoryBandwidthStats" + * + * @see com.android.internal.os.MemoryPowerCalculator + */ + public static double calcMemoryPower(PowerProfile powerProfile) { + double power = 0; + int numBuckets = powerProfile.getNumElements(PowerProfile.POWER_MEMORY); + for (int i = 0; i < numBuckets; i++) { + long timeMs = 0; + power += new UsageBasedPowerEstimator(powerProfile.getAveragePower(PowerProfile.POWER_MEMORY, i)).calculatePower(timeMs); + } + return power; + } + + /** + * @see com.android.internal.os.WakelockPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcWakelocksPower(PowerProfile powerProfile, HealthStats healthStats) { + double power = 0; + if (healthStats.hasTimers(UidHealthStats.TIMERS_WAKELOCKS_PARTIAL)) { + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_WAKELOCKS_PARTIAL); + long timeMs = 0; + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_CPU_IDLE); + power = new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + return power; + } + + /** + * @see com.android.internal.os.MobileRadioPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcMobilePower(PowerProfile powerProfile, HealthStats healthStats) { + // double mams = getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_POWER_MAMS) / UsageBasedPowerEstimator.MILLIS_IN_HOUR; + // if (mams > 0) { + // MatrixLog.i(TAG, "estimate Mobile by mams"); + // return mams; + // } + double power = calcMobilePowerByRadioActive(powerProfile, healthStats); + if (power > 0) { + MatrixLog.i(TAG, "estimate Mobile by radioActive"); + return power; + } + // power = calcMobilePowerByController(powerProfile, healthStats); + // if (power > 0) { + // MatrixLog.i(TAG, "estimate Mobile by controller"); + // return power; + // } + return 0; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @VisibleForTesting + public static double calcMobilePowerByRadioActive(PowerProfile powerProfile, HealthStats healthStats) { + // calc from radio active + // for some aosp mistakes, radio active timer was given in time unit us: + // https://cs.android.com/android/_/android/platform/frameworks/base/+/bee44ae8e5da109cd8273a057b566dc6925d6a71 + long timeMs = getTimerTime(healthStats, UidHealthStats.TIMER_MOBILE_RADIO_ACTIVE) / 1000; + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_RADIO_ACTIVE); + if (powerMa <= 0) { + double sum = 0; + sum += powerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX); + int num = powerProfile.getNumElements(PowerProfile.POWER_MODEM_CONTROLLER_TX); + for (int i = 0; i < num; i++) { + sum += powerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_TX, i); + } + powerMa = sum / (num + 1); + } + return new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @VisibleForTesting + public static double calcMobilePowerByController(PowerProfile powerProfile, HealthStats healthStats) { + // calc from controller + double power = 0; + { + long timeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_IDLE_MS); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_MODEM_CONTROLLER_IDLE); + power += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + { + long timeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_RX_MS); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_MODEM_CONTROLLER_RX); + power += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + { + long timeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_TX_MS); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_MODEM_CONTROLLER_TX); + power += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + return power; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @VisibleForTesting + public static double calcMobilePowerByPackets(PowerProfile powerProfile, HealthStats healthStats, double rxBps, double txBps) { + double power = 0; + { + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_RADIO_ACTIVE); + if (powerMa <= 0) { + double sum = 0; + sum += powerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX); + int num = powerProfile.getNumElements(PowerProfile.POWER_MODEM_CONTROLLER_TX); + for (int i = 0; i < num; i++) { + sum += powerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_TX, i); + } + powerMa = sum / (num + 1); + } + double mobileBps = rxBps + txBps; + double powerPs = powerMa / 3600; + double mobilePps = ((double) mobileBps) / 8 / 2048; + double powerMaPerPacket = (powerPs / mobilePps) / (60 * 60); + long packets = getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_RX_PACKETS) + getMeasure(healthStats, UidHealthStats.MEASUREMENT_MOBILE_TX_PACKETS); + power += powerMaPerPacket * packets; + } + return power; + } + + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @VisibleForTesting + public static double calcMobilePowerByNetworkStatBytes(PowerProfile powerProfile, RadioStatSnapshot snapshot, double rxBps, double txBps) { + long rxMs = (long) ((snapshot.mobileRxBytes.get() / (rxBps / 8)) * 1000); + long txMs = (long) ((snapshot.mobileTxBytes.get() / (txBps / 8)) * 1000); + double power = 0; + { + double avgPower = powerProfile.getAveragePowerUni(PowerProfile.POWER_MODEM_CONTROLLER_RX); + UsageBasedPowerEstimator estimator = new UsageBasedPowerEstimator(avgPower); + power += estimator.calculatePower(rxMs); + } + { + double avgPower = powerProfile.getAveragePowerUni(PowerProfile.POWER_MODEM_CONTROLLER_TX); + UsageBasedPowerEstimator estimator = new UsageBasedPowerEstimator(avgPower); + power += estimator.calculatePower(txMs); + } + { + double avgPower = powerProfile.getAveragePowerUni(PowerProfile.POWER_MODEM_CONTROLLER_IDLE); + UsageBasedPowerEstimator estimator = new UsageBasedPowerEstimator(avgPower); + power += estimator.calculatePower(txMs + rxMs); + } + return power; + } + + @VisibleForTesting + public static double calcMobilePowerByNetworkStatPackets(PowerProfile powerProfile, RadioStatSnapshot snapshot, double rxBps, double txBps) { + double power = 0; + { + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_RADIO_ACTIVE); + if (powerMa <= 0) { + double sum = 0; + sum += powerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX); + int num = powerProfile.getNumElements(PowerProfile.POWER_MODEM_CONTROLLER_TX); + for (int i = 0; i < num; i++) { + sum += powerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_TX, i); + } + powerMa = sum / (num + 1); + } + double mobileBps = rxBps + txBps; + double powerPs = powerMa / 3600; + double mobilePps = ((double) mobileBps) / 8 / 2048; + double powerMaPerPacket = (powerPs / mobilePps) / (60 * 60); + long packets = snapshot.mobileRxPackets.get() + snapshot.mobileTxPackets.get(); + power += powerMaPerPacket * packets; + } + return power; + } + + + /** + * @see com.android.internal.os.WifiPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcWifiPower(PowerProfile powerProfile, HealthStats healthStats) { + // double mams = getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_POWER_MAMS) / UsageBasedPowerEstimator.MILLIS_IN_HOUR; + // if (mams > 0) { + // MatrixLog.i(TAG, "estimate WIFI by mams"); + // return mams; + // } + double power = calcWifiPowerByController(powerProfile, healthStats); + if (power > 0) { + MatrixLog.i(TAG, "estimate WIFI by controller"); + return power; + } + // power = calcWifiPowerByPackets(powerProfile, healthStats, 500000, 500000); + // if (power > 0) { + // MatrixLog.i(TAG, "estimate WIFI by packets"); + // return power; + // } + return 0; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @VisibleForTesting + public static double calcWifiPowerByController(PowerProfile powerProfile, HealthStats healthStats) { + // calc from controller + double power = 0; + { + double wifiIdlePower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_CONTROLLER_IDLE); + long idleMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_IDLE_MS); + UsageBasedPowerEstimator etmWifiIdlePower = new UsageBasedPowerEstimator(wifiIdlePower); + power += etmWifiIdlePower.calculatePower(idleMs); + } + { + double wifiRxPower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_CONTROLLER_RX); + UsageBasedPowerEstimator etmWifiRxPower = new UsageBasedPowerEstimator(wifiRxPower); + long rxMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_RX_MS); + power += etmWifiRxPower.calculatePower(rxMs); + } + { + double wifiTxPower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_CONTROLLER_TX); + long txMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_TX_MS); + UsageBasedPowerEstimator etmWifiTxPower = new UsageBasedPowerEstimator(wifiTxPower); + power += etmWifiTxPower.calculatePower(txMs); + } + return power; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @VisibleForTesting + public static double calcWifiPowerByPackets(PowerProfile powerProfile, HealthStats healthStats, double rxBps, double txBps) { + // calc from packets + double power = 0; + if (rxBps >= 0 && txBps >= 0) { + if (rxBps == 0 && txBps == 0) { + return power; + } + { + final double wifiBps = rxBps + txBps; + final double averageWifiActivePower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_ACTIVE) / 3600; + double powerMaPerPacket = averageWifiActivePower / (((double) wifiBps) / 8 / 2048); + long packets = getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_RX_PACKETS) + getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_TX_PACKETS); + power += powerMaPerPacket * packets; + } + { + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_ON); + long timeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_WIFI_RUNNING_MS); + power += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + { + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_SCAN); + long timeMs = getTimerTime(healthStats, UidHealthStats.TIMER_WIFI_SCAN); + power += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + } + return power; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @VisibleForTesting + public static double calcWifiPowerByNetworkStatBytes(PowerProfile powerProfile, RadioStatSnapshot snapshot, double rxBps, double txBps) { + long rxMs = (long) ((snapshot.wifiRxBytes.get() / (rxBps / 8)) * 1000); + long txMs = (long) ((snapshot.wifiTxBytes.get() / (txBps / 8)) * 1000); + double power = 0; + { + double avgPower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_CONTROLLER_RX); + UsageBasedPowerEstimator estimator = new UsageBasedPowerEstimator(avgPower); + power += estimator.calculatePower(rxMs); + } + { + double avgPower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_CONTROLLER_TX); + UsageBasedPowerEstimator estimator = new UsageBasedPowerEstimator(avgPower); + power += estimator.calculatePower(txMs); + } + { + double avgPower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_CONTROLLER_IDLE); + UsageBasedPowerEstimator estimator = new UsageBasedPowerEstimator(avgPower); + power += estimator.calculatePower(txMs + rxMs); + } + return power; + } + + @VisibleForTesting + public static double calcWifiPowerByNetworkStatPackets(PowerProfile powerProfile, RadioStatSnapshot snapshot, double rxBps, double txBps) { + double power = 0; + { + final double wifiBps = rxBps + txBps; + final double averageWifiActivePower = powerProfile.getAveragePowerUni(PowerProfile.POWER_WIFI_ACTIVE) / 3600; + double powerMaPerPacket = averageWifiActivePower / (((double) wifiBps) / 8 / 2048); + long packets = snapshot.wifiRxPackets.get() + snapshot.wifiTxPackets.get(); + power += powerMaPerPacket * packets; + } + return power; + } + + /** + * @see com.android.internal.os.BluetoothPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcBlueToothPower(PowerProfile powerProfile, HealthStats healthStats) { + // double mams = getMeasure(healthStats, UidHealthStats.MEASUREMENT_BLUETOOTH_POWER_MAMS) / UsageBasedPowerEstimator.MILLIS_IN_HOUR; + // if (mams > 0) { + // MatrixLog.i(TAG, "etmMobilePower BLE by mams"); + // return mams; + // } + double power = 0; + { + long timeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_BLUETOOTH_IDLE_MS); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE); + power += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + { + long timeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_BLUETOOTH_RX_MS); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_BLUETOOTH_CONTROLLER_RX); + power += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + { + long timeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_BLUETOOTH_TX_MS); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_BLUETOOTH_CONTROLLER_TX); + power += new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + } + if (power > 0) { + return power; + } + return 0; + } + + /** + * @see com.android.internal.os.GnssPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcGpsPower(PowerProfile powerProfile, HealthStats healthStats) { + long timeMs = getTimerTime(healthStats, UidHealthStats.TIMER_GPS_SENSOR); + double powerMa = 0; + if (timeMs > 0) { + powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_GPS_ON); + if (powerMa <= 0) { + int num = powerProfile.getNumElements(PowerProfile.POWER_GPS_SIGNAL_QUALITY_BASED); + double sumMa = 0; + for (int i = 0; i < num; i++) { + sumMa += powerProfile.getAveragePower(PowerProfile.POWER_GPS_SIGNAL_QUALITY_BASED, i); + } + powerMa = sumMa / num; + } + } + double power = new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + if (power > 0) { + return power; + } + return 0; + } + + /** + * @see com.android.internal.os.SensorPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcSensorsPower(Context context, HealthStats healthStats) { + double power = 0; + if (healthStats.hasTimers(UidHealthStats.TIMERS_SENSORS)) { + SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + List sensorList = sm.getSensorList(Sensor.TYPE_ALL); + Map sensorMap = new HashMap<>(); + for (Sensor item : sensorList) { + try { + //noinspection JavaReflectionMemberAccess + @SuppressLint("DiscouragedPrivateApi") + Method method = item.getClass().getDeclaredMethod("getHandle"); + //noinspection ConstantConditions + int handle = (int) method.invoke(item); + sensorMap.put(String.valueOf(handle), item); + } catch (Throwable e) { + MatrixLog.w(TAG, "getSensorHandle err: " + e.getMessage()); + } + } + + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_SENSORS); + for (Map.Entry item : timers.entrySet()) { + String handle = item.getKey(); + long timeMs = item.getValue().getTime(); + if (handle.equals("-10000")) { + continue; // skip GPS Sensors + } + Sensor sensor = sensorMap.get(handle); + if (sensor != null) { + power += new UsageBasedPowerEstimator(sensor.getPower()).calculatePower(timeMs); + } + } + } + if (power > 0) { + return power; + } + return 0; + } + + /** + * WIP + * Calculate camera power usage. Right now, this is a (very) rough estimate based on the + * average power usage for a typical camera application. + * + * @see com.android.internal.os.CameraPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcCameraPower(PowerProfile powerProfile, HealthStats healthStats) { + long timeMs = getTimerTime(healthStats, UidHealthStats.TIMER_CAMERA); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_CAMERA); + double power = new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + if (power > 0) { + return power; + } + return 0; + } + + /** + * @see com.android.internal.os.FlashlightPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcFlashLightPower(PowerProfile powerProfile, HealthStats healthStats) { + long timeMs = getTimerTime(healthStats, UidHealthStats.TIMER_FLASHLIGHT); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_FLASHLIGHT); + double power = new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + if (power > 0) { + return power; + } + return 0; + } + + /** + * @see com.android.internal.os.MediaPowerCalculator + * @see com.android.internal.os.AudioPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcAudioPower(PowerProfile powerProfile, HealthStats healthStats) { + long timeMs = getTimerTime(healthStats, UidHealthStats.TIMER_AUDIO); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_AUDIO); + if (powerMa == 0) { + powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_AUDIO_DSP); + } + double power = new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + if (power > 0) { + return power; + } + return 0; + } + + /** + * @see com.android.internal.os.MediaPowerCalculator + * @see com.android.internal.os.VideoPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcVideoPower(PowerProfile powerProfile, HealthStats healthStats) { + long timeMs = getTimerTime(healthStats, UidHealthStats.TIMER_VIDEO); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_VIDEO); + if (powerMa == 0) { + powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_VIDEO_DSP); + } + double power = new UsageBasedPowerEstimator(powerMa).calculatePower(timeMs); + if (power > 0) { + return power; + } + return 0; + } + + /** + * @see com.android.internal.os.ScreenPowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcScreenPower(PowerProfile powerProfile, HealthStats healthStats) { + long topAppMs = getTimerTime(healthStats, UidHealthStats.TIMER_PROCESS_STATE_TOP_MS); + long fgActivityMs = getTimerTime(healthStats, UidHealthStats.TIMER_FOREGROUND_ACTIVITY); + long screenOnTimeMs = Math.min(topAppMs, fgActivityMs); + double powerMa = powerProfile.getAveragePowerUni(PowerProfile.POWER_SCREEN_ON); + double power = new UsageBasedPowerEstimator(powerMa).calculatePower(screenOnTimeMs); + if (power > 0) { + return power; + } + return 0; + } + + /** + * WIP + * Binder cup time_in_state can not be collected right now + * + * @see com.android.internal.os.SystemServicePowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcSystemServicePower(PowerProfile powerProfile, HealthStats healthStats) { + double power = 0; + long timeMs = 0; + if (healthStats.hasTimers(UidHealthStats.TIMERS_JOBS)) { + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_JOBS); + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + } + if (healthStats.hasTimers(UidHealthStats.TIMERS_SYNCS)) { + Map timers = healthStats.getTimers(UidHealthStats.TIMERS_SYNCS); + for (TimerStat item : timers.values()) { + timeMs += item.getTime(); + } + } + + power += estimateCpuActivePower(powerProfile, timeMs); + CpuStatFeature feat = BatteryCanary.getMonitorFeature(CpuStatFeature.class); + if (feat != null && feat.isSupported()) { + CpuStateSnapshot snapshot = feat.currentCpuStateSnapshot(); + if (snapshot != null) { + power += estimateCpuClustersPower(powerProfile, snapshot, timeMs, false); + power += estimateCpuCoresPower(powerProfile, snapshot, timeMs, false); + } + } + if (power > 0) { + return power; + } + return 0; + } + + /** + * @see com.android.internal.os.IdlePowerCalculator + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static double calcIdlePower(PowerProfile powerProfile, HealthStats healthStats) { + long batteryRealtimeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_REALTIME_BATTERY_MS); + long batteryUptimeMs = getMeasure(healthStats, UidHealthStats.MEASUREMENT_UPTIME_BATTERY_MS); + double suspendPowerMah = new UsageBasedPowerEstimator(powerProfile.getAveragePowerUni(PowerProfile.POWER_CPU_SUSPEND)).calculatePower(batteryRealtimeMs); + double idlePowerMah = new UsageBasedPowerEstimator(powerProfile.getAveragePowerUni(PowerProfile.POWER_CPU_IDLE)).calculatePower(batteryUptimeMs); + double power = suspendPowerMah + idlePowerMah; + if (power > 0) { + return power; + } + return 0; + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsActivity.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsActivity.java new file mode 100644 index 000000000..bbe9c7104 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsActivity.java @@ -0,0 +1,133 @@ +package com.tencent.matrix.batterycanary.stats.ui; + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.widget.PopupMenu; +import android.widget.TextView; + +import com.tencent.matrix.batterycanary.BatteryCanary; +import com.tencent.matrix.batterycanary.R; +import com.tencent.matrix.batterycanary.stats.BatteryStatsFeature; +import com.tencent.matrix.batterycanary.utils.Consumer; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public class BatteryStatsActivity extends AppCompatActivity { + + @NonNull + private BatteryStatsLoader mStatsLoader; + private BatteryStatsAdapter.Item.HeaderItem mCurrHeader; + private boolean mEnd; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_battery_stats); + + Toolbar toolbar = findViewById(R.id.toolbar); + toolbar.setTitle("电量统计报告"); + setSupportActionBar(toolbar); + + final TextView procTv = findViewById(R.id.tv_proc); + String proc = com.tencent.matrix.batterycanary.stats.BatteryRecorder.MMKVRecorder.getProcNameSuffix(); + procTv.setText(":" + proc); + + BatteryCanary.getMonitorFeature(BatteryStatsFeature.class, new Consumer() { + @Override + public void accept(final BatteryStatsFeature batteryStatsFeature) { + procTv.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + PopupMenu menu = new PopupMenu(v.getContext(), procTv); + menu.getMenu().add("Process :main"); + for (String item : batteryStatsFeature.getProcSet()) { + if ("main".equals(item)) { + continue; + } + menu.getMenu().add("Process :" + item); + } + menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + String title = item.getTitle().toString(); + if (title.contains(":")) { + String proc = title.substring(title.lastIndexOf(":") + 1); + procTv.setText(":" + proc); + mStatsLoader.reset(proc); + mStatsLoader.load(); + updateHeader(0); + } + return false; + } + }); + menu.show(); + } + }); + } + }); + + RecyclerView recyclerView = findViewById(R.id.rv_battery_stats); + final LinearLayoutManager layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + BatteryStatsAdapter adapter = new BatteryStatsAdapter(); + recyclerView.setAdapter(adapter); + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + // update header + int topPosition = layoutManager.findFirstVisibleItemPosition(); + updateHeader(topPosition); + } + }); + mStatsLoader = new BatteryStatsLoader(adapter); + // mStatsLoader.setLoadListener(new BatteryStatsLoader.OnLoadListener() { + // @Override + // public void onLoadFinish(BatteryStatsAdapter adapter) { + // + // } + // }); + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + // update header + int topPosition = layoutManager.findFirstVisibleItemPosition(); + updateHeader(topPosition); + + // load more + if (layoutManager.findLastVisibleItemPosition() == mStatsLoader.mStatsAdapter.getDataList().size() - 1) { + if (!mStatsLoader.loadMore()) { + if (!mEnd) { + mEnd = true; + } + } + } + } + }); + + // load today's data + mStatsLoader.reset(proc); + mStatsLoader.load(); + + // load mocking data + // loadMockingData(); + } + + private void updateHeader(final int topPosition) { + BatteryStatsAdapter.Item.HeaderItem currHeader = mStatsLoader.getFirstHeader(topPosition); + if (currHeader != null) { + if (mCurrHeader == null || mCurrHeader != currHeader) { + mCurrHeader = currHeader; + View headerView = findViewById(R.id.header_pinned); + headerView.setVisibility(View.VISIBLE); + TextView tv = headerView.findViewById(R.id.tv_title); + tv.setText(currHeader.date); + } + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsAdapter.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsAdapter.java new file mode 100644 index 000000000..96e137ec1 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsAdapter.java @@ -0,0 +1,653 @@ +package com.tencent.matrix.batterycanary.stats.ui; + +import android.annotation.SuppressLint; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.tencent.matrix.batterycanary.R; +import com.tencent.matrix.batterycanary.monitor.feature.CompositeMonitors; +import com.tencent.matrix.batterycanary.stats.BatteryRecord; +import com.tencent.matrix.batterycanary.utils.ThreadSafeReference; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; + +import static com.tencent.matrix.batterycanary.stats.BatteryRecord.ReportRecord.EXTRA_APP_FOREGROUND; +import static com.tencent.matrix.batterycanary.stats.BatteryRecord.ReportRecord.EXTRA_JIFFY_OVERHEAT; + +/** + * @author Kaede + * @since 2021/12/10 + */ +public class BatteryStatsAdapter extends RecyclerView.Adapter { + + public static final int VIEW_TYPE_HEADER = 0; + public static final int VIEW_TYPE_EVENT_DUMP = 1; + public static final int VIEW_TYPE_EVENT_LEVEL_1 = 2; + public static final int VIEW_TYPE_EVENT_LEVEL_2 = 3; + public static final int VIEW_TYPE_NO_DATA = 4; + public static final int VIEW_TYPE_EVENT_SIMPLE = 5; + public static final int VIEW_TYPE_EVENT_BATTERY = 6; + + protected final List dataList = new ArrayList<>(); + + public List getDataList() { + return dataList; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_HEADER: + return new ViewHolder.HeaderHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item_header, parent, false)); + case VIEW_TYPE_NO_DATA: + return new ViewHolder.NoDataHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item_no_data, parent, false)); + case VIEW_TYPE_EVENT_DUMP: + return new ViewHolder.EventDumpHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item_event_dump, parent, false)); + case VIEW_TYPE_EVENT_SIMPLE: + return new ViewHolder.EventSimpleHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item_event_simple, parent, false)); + case VIEW_TYPE_EVENT_BATTERY: + return new ViewHolder.EventBatteryHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item_event_battery, parent, false)); + case VIEW_TYPE_EVENT_LEVEL_1: + return new ViewHolder.EventLevel1Holder(LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item_event_1, parent, false)); + case VIEW_TYPE_EVENT_LEVEL_2: + return new ViewHolder.EventLevel2Holder(LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item_event_2, parent, false)); + } + throw new IllegalStateException("Unknown view type: " + viewType); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + //noinspection unchecked + ((ViewHolder) holder).bind(dataList.get(position)); + } + + @Override + public int getItemCount() { + return dataList.size(); + } + + @Override + public int getItemViewType(int position) { + return dataList.get(position).viewType(); + } + + public interface Item { + int viewType(); + + class HeaderItem implements Item { + public String date; + + @Override + public int viewType() { + return VIEW_TYPE_HEADER; + } + } + + class NoDataItem implements Item { + public String text; + + @Override + public int viewType() { + return VIEW_TYPE_NO_DATA; + } + } + + class EventDumpItem extends BatteryRecord.ReportRecord implements Item { + public final ReportRecord record; + public boolean expand = false; + public String desc; + + public EventDumpItem(ReportRecord record) { + this.record = record; + this.millis = record.millis; + this.id = record.id; + this.event = record.event; + this.extras = record.extras; + this.scope = record.scope; + this.windowMillis = record.windowMillis; + this.threadInfoList = record.threadInfoList; + this.entryList = record.entryList; + } + + @Override + public int viewType() { + return VIEW_TYPE_EVENT_DUMP; + } + } + + class EventSimpleItem extends BatteryRecord.EventStatRecord implements Item { + public final EventStatRecord record; + + public EventSimpleItem(EventStatRecord record) { + this.millis = record.millis; + this.record = record; + this.id = record.id; + this.event = record.event; + } + + @Override + public int viewType() { + return VIEW_TYPE_EVENT_SIMPLE; + } + } + + class EventBatteryItem extends BatteryRecord.EventStatRecord implements Item { + public final EventStatRecord record; + + public EventBatteryItem(EventStatRecord record) { + this.millis = record.millis; + this.record = record; + this.id = record.id; + this.event = record.event; + } + + @Override + public int viewType() { + return VIEW_TYPE_EVENT_BATTERY; + } + } + + @SuppressLint("ParcelCreator") + class EventLevel1Item extends BatteryRecord implements Item { + public String text; + + public EventLevel1Item(BatteryRecord record) { + this.millis = record.millis; + } + + @Override + public int viewType() { + return VIEW_TYPE_EVENT_LEVEL_1; + } + } + + @SuppressLint("ParcelCreator") + class EventLevel2Item extends BatteryRecord implements Item { + public String text; + + public EventLevel2Item(BatteryRecord record) { + this.millis = record.millis; + } + + @Override + public int viewType() { + return VIEW_TYPE_EVENT_LEVEL_2; + } + } + } + + + public abstract static class ViewHolder extends RecyclerView.ViewHolder { + public static final int COLOR_FG_MAIN = R.color.FG_0; + public static final int COLOR_FG_SUB = R.color.FG_2; + public static final int COLOR_FG_ALERT = R.color.Red_80_CARE; + + protected static final ThreadSafeReference sTimeFormatRef = new ThreadSafeReference() { + @NonNull + @Override + public DateFormat onCreate() { + return new SimpleDateFormat("HH:mm", Locale.getDefault()); + } + }; + + @SuppressWarnings("NotNullFieldNotInitialized") + @NonNull + protected ITEM mItem; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + } + + public void bind(ITEM item) { + } + + + public static class HeaderHolder extends ViewHolder { + private final TextView mTitleTv; + + public HeaderHolder(@NonNull View itemView) { + super(itemView); + mTitleTv = itemView.findViewById(R.id.tv_title); + } + + @Override + public void bind(Item.HeaderItem item) { + mItem = item; + mTitleTv.setText(item.date); + } + } + + public static class NoDataHolder extends ViewHolder { + private final TextView mTitleTv; + + public NoDataHolder(@NonNull View itemView) { + super(itemView); + mTitleTv = itemView.findViewById(R.id.tv_title); + } + + @Override + public void bind(Item.NoDataItem item) { + mItem = item; + if (!TextUtils.isEmpty(item.text)) { + mTitleTv.setText(item.text); + } + } + } + + public static class EventDumpHolder extends ViewHolder { + + private final TextView mTimeTv; + private final TextView mTitleTv; + private final TextView mTitleSub1; + private final TextView mTitleSub2; + private final TextView mMoreTv; + private final ImageView mIndicatorIv; + private final View mExpandView; + + private final View mHeaderEntryView; + private final TextView mHeaderLeftTv; + private final TextView mHeaderRightTv; + private final TextView mHeaderDescTv; + + private final View mEntryViewThread; + private final View mEntryView1; + private final View mEntryView2; + private final View mEntryView3; + private final View mEntryView4; + + public EventDumpHolder(@NonNull View itemView) { + super(itemView); + mTimeTv = itemView.findViewById(R.id.tv_time); + mTitleTv = itemView.findViewById(R.id.tv_title); + mTitleSub1 = itemView.findViewById(R.id.tv_title_sub_1); + mTitleSub2 = itemView.findViewById(R.id.tv_title_sub_2); + mMoreTv = itemView.findViewById(R.id.tv_more); + mIndicatorIv = itemView.findViewById(R.id.iv_indicator); + mExpandView = itemView.findViewById(R.id.layout_expand); + + mHeaderEntryView = itemView.findViewById(R.id.layout_expand_entry_header); + mHeaderLeftTv = mHeaderEntryView.findViewById(R.id.tv_header_left); + mHeaderRightTv = mHeaderEntryView.findViewById(R.id.tv_header_right); + mHeaderDescTv = mHeaderEntryView.findViewById(R.id.tv_desc); + + mEntryViewThread = itemView.findViewById(R.id.layout_expand_entry_thread); + mEntryView1 = itemView.findViewById(R.id.layout_expand_entry_1); + mEntryView2 = itemView.findViewById(R.id.layout_expand_entry_2); + mEntryView3 = itemView.findViewById(R.id.layout_expand_entry_3); + mEntryView4 = itemView.findViewById(R.id.layout_expand_entry_4); + + View.OnClickListener onClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + mItem.expand = !mItem.expand; + updateView(mItem); + } + }; + itemView.findViewById(R.id.layout_title).setOnClickListener(onClickListener); + itemView.findViewById(R.id.layout_right).setOnClickListener(onClickListener); + + mEntryViewThread.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + long endMillis = mItem.record.millis; + StringBuilder sb = new StringBuilder(); + long bgnMillis = mItem.record.millis - mItem.record.windowMillis; + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + + sb.append("线程异常: ").append(mItem.record.getBoolean(EXTRA_JIFFY_OVERHEAT, false)) + .append("\n统计时长: ").append(Math.max((mItem.record.windowMillis) / (60 * 1000L), 1)).append("min") + .append("\n时间窗口: ").append(dateFormat.format(new Date(bgnMillis))).append(" ~ ").append(dateFormat.format(new Date(endMillis))); + + for (int i = 0; i < mItem.record.threadInfoList.size(); i++) { + BatteryRecord.ReportRecord.ThreadInfo threadInfo = mItem.record.threadInfoList.get(i); + if (threadInfo != null) { + String status = threadInfo.stat; + switch (threadInfo.stat) { + case "R": + status = "Running"; + break; + case "S": + status = "Sleep"; + break; + case "D": + status = "Dead"; + break; + default: + break; + } + sb.append("\n\n线程 TOP ").append(i + 1).append(":").append(threadInfo.name) + .append("\ntid: ").append(threadInfo.tid) + .append("\n状态: ").append(status) + .append("\nJiffy 开销: ").append(threadInfo.jiffies).append(", ").append(threadInfo.jiffies / Math.max(mItem.windowMillis / (60 * 1000L), 1)).append("/min") + .append("\n运行时间: ").append(Math.max((threadInfo.jiffies * 10L) / (60 * 1000L), 1)).append("min").append(", 占整体统计时间 ").append(String.format(Locale.US, "%s", (threadInfo.jiffies * 10L * 100) / mItem.windowMillis)).append("%") + .append("\nStackTrace: \n").append(threadInfo.extraInfo.get(BatteryRecord.ReportRecord.EXTRA_THREAD_STACK)); + } + } + + View layout = LayoutInflater.from(v.getContext()).inflate(R.layout.stats_battery_report, null); + TextView tv = layout.findViewById(R.id.tv_report); + tv.setText(sb.toString()); + AlertDialog dialog = new AlertDialog.Builder(v.getContext()) + .setTitle("线程详细信息") + .setPositiveButton("确定", null) + .setCancelable(true) + .setView(layout) + .create(); + dialog.show(); + } + }); + } + + @Override + public void bind(final Item.EventDumpItem item) { + mItem = item; + resetView(); + updateView(item); + } + + private void resetView() { + mExpandView.setVisibility(View.GONE); + mHeaderEntryView.setVisibility(View.VISIBLE); + mEntryViewThread.setVisibility(View.GONE); + mEntryView1.setVisibility(View.GONE); + mEntryView2.setVisibility(View.GONE); + mEntryView3.setVisibility(View.GONE); + mEntryView4.setVisibility(View.GONE); + } + + @SuppressLint({"SetTextI18n", "CutPasteId"}) + private void updateView(Item.EventDumpItem item) { + // Basic + String title = "", desc = ""; + switch (TextUtils.isEmpty(item.record.scope) ? "" : item.record.scope) { + case CompositeMonitors.SCOPE_CANARY: + if (item.record.getBoolean(EXTRA_APP_FOREGROUND, false)) { + title += "前台 Polling 监控"; + desc = "App 在前台时, 周期性地执行电量统计 (具体周期见时长)"; + } else { + title += "待机功耗监控"; + desc = "App 进入后台并持续一段时间后 (待机), 再次切换到前台时执行一次电量统计。"; + } + break; + case CompositeMonitors.SCOPE_INTERNAL: + title += "Matrix 内部监控"; + desc = "Matrix 自身电量开销的监控, 避免电量监控框架自身导致的耗电问题"; + break; + case CompositeMonitors.SCOPE_OVERHEAT: + title += "Runnable 任务监控"; + desc = "ThreadPool 等需要执行大量零碎 Runnable 的专项电量统计。"; + break; + default: + title += ": " + item.record.scope; + desc = "缺乏描述"; + break; + } + + mTimeTv.setText(sTimeFormatRef.safeGet().format(new Date(item.millis))); + mMoreTv.setRotation(item.expand ? 180 : 0); + mExpandView.setVisibility(item.expand ? View.VISIBLE : View.GONE); + mTitleTv.setText(title); + mTitleSub1.setText(sTimeFormatRef.safeGet().format(new Date(item.millis - item.windowMillis)) + " ~ " + sTimeFormatRef.safeGet().format(new Date(item.millis))); + if (item.record.isOverHeat()) { + mIndicatorIv.setImageLevel(4); + mTitleSub2.setText("#OVERHEAT"); + } else { + mIndicatorIv.setImageLevel(2); + mTitleSub2.setText("正常"); + } + if (!item.expand) { + return; + } + + // Header + mHeaderLeftTv.setText("模式: " + item.scope); + mHeaderRightTv.setText("时长: " + Math.max(1, (item.windowMillis) / (60 * 1000L)) + "min"); + mHeaderDescTv.setText(TextUtils.isEmpty(item.desc) ? desc : item.desc); + + // Thread Entry + mEntryViewThread.setVisibility(!item.threadInfoList.isEmpty() ? View.VISIBLE : View.GONE); + if (!item.threadInfoList.isEmpty()) { + boolean overHeat = item.record.getBoolean(EXTRA_JIFFY_OVERHEAT, false); + TextView tvTitle = mEntryViewThread.findViewById(R.id.tv_header_left); + tvTitle.setTextColor(tvTitle.getResources().getColor(overHeat ? COLOR_FG_ALERT : COLOR_FG_SUB)); + + LinearLayout entryGroup = mEntryViewThread.findViewById(R.id.layout_entry_group); + int reusableCount = entryGroup.getChildCount(); + for (int i = 0; i < reusableCount; i++) { + entryGroup.getChildAt(i).setVisibility(View.GONE); + } + for (int i = 0; i < 5; i++) { + if (i < item.threadInfoList.size()) { + Item.EventDumpItem.ThreadInfo threadInfo = item.threadInfoList.get(i); + View entryItemView; + if (i < reusableCount) { + // 1. reuse existing entry view + entryItemView = entryGroup.getChildAt(i); + } else { + // 2. add new entry view + entryItemView = LayoutInflater.from(entryGroup.getContext()).inflate(R.layout.stats_item_event_dump_entry_subentry, entryGroup, false); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, entryGroup.getContext().getResources().getDisplayMetrics()); + entryGroup.addView(entryItemView, layoutParams); + } + // 3. update content + entryItemView.setVisibility(View.VISIBLE); + TextView left = entryItemView.findViewById(R.id.tv_key); + TextView right = entryItemView.findViewById(R.id.tv_value); + left.setVisibility(threadInfo != null ? View.VISIBLE : View.GONE); + right.setVisibility(threadInfo != null ? View.VISIBLE : View.GONE); + + if (threadInfo != null) { + left.setText(threadInfo.name); + right.setText(threadInfo.tid + " / " + Math.max(1, (threadInfo.jiffies * 10) / (60 * 1000L)) + "min / " + threadInfo.stat); + } + } + } + } + + // Entry 1 - Entry 4: + // 1. entry section + for (int i = 1; i <= 4; i++) { + final View entryView; + switch (i) { + case 1: + entryView = mEntryView1; + break; + case 2: + entryView = mEntryView2; + break; + case 3: + entryView = mEntryView3; + break; + case 4: + entryView = mEntryView4; + break; + default: + throw new IndexOutOfBoundsException("entryList section out of bound: " + i); + } + + Item.EventDumpItem.EntryInfo entryInfo = i <= item.entryList.size() ? item.entryList.get(i - 1) : null; + entryView.setVisibility(entryInfo != null ? View.VISIBLE : View.GONE); + if (entryInfo != null) { + TextView left = entryView.findViewById(R.id.tv_header_left); + left.setText(entryInfo.name); + + // 2. entry list + LinearLayout entryGroup = entryView.findViewById(R.id.layout_entry_group); + int reusableCount = entryGroup.getChildCount(); + for (int j = 0; j < reusableCount; j++) { + entryGroup.getChildAt(j).setVisibility(View.GONE); + } + + int entryIdx = 0, entryLimit = 6; + for (Map.Entry entry : entryInfo.entries.entrySet()) { + entryIdx++; + if (entryIdx > entryLimit) { + break; + } + View entryItemView; + if (entryIdx < reusableCount) { + // 1. reuse existing entry view + entryItemView = entryGroup.getChildAt(entryIdx); + } else { + // 2. add new entry view + entryItemView = LayoutInflater.from(entryGroup.getContext()).inflate(R.layout.stats_item_event_dump_entry_subentry, entryGroup, false); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, entryGroup.getContext().getResources().getDisplayMetrics()); + entryGroup.addView(entryItemView, layoutParams); + } + + entryItemView.setVisibility(View.VISIBLE); + TextView keyTv = entryItemView.findViewById(R.id.tv_key); + TextView valTv = entryItemView.findViewById(R.id.tv_value); + keyTv.setText(entry.getKey()); + valTv.setText(entry.getValue()); + } + } + } + } + } + + public static class EventLevel1Holder extends ViewHolder { + + private final TextView mTimeTv; + private final TextView mTitleTv; + + public EventLevel1Holder(@NonNull View itemView) { + super(itemView); + mTimeTv = itemView.findViewById(R.id.tv_time); + mTitleTv = itemView.findViewById(R.id.tv_title); + } + + @Override + public void bind(Item.EventLevel1Item item) { + mItem = item; + mTimeTv.setText(sTimeFormatRef.safeGet().format(new Date(item.millis))); + mTitleTv.setText(item.text); + } + } + + public static class EventLevel2Holder extends ViewHolder { + + private final TextView mTimeTv; + private final TextView mTitleTv; + + public EventLevel2Holder(@NonNull View itemView) { + super(itemView); + mTimeTv = itemView.findViewById(R.id.tv_time); + mTitleTv = itemView.findViewById(R.id.tv_title); + } + + @Override + public void bind(Item.EventLevel2Item item) { + mItem = item; + mTimeTv.setText(sTimeFormatRef.safeGet().format(new Date(item.millis))); + mTitleTv.setText(item.text); + } + } + + public static class EventSimpleHolder extends ViewHolder implements View.OnClickListener { + + private final TextView mTimeTv; + private final TextView mTitleTv; + + public EventSimpleHolder(@NonNull View itemView) { + super(itemView); + mTimeTv = itemView.findViewById(R.id.tv_time); + mTitleTv = itemView.findViewById(R.id.tv_title); + itemView.findViewById(R.id.layout_title).setOnClickListener(this); + } + + @Override + public void bind(Item.EventSimpleItem item) { + mItem = item; + mTimeTv.setText(sTimeFormatRef.safeGet().format(new Date(item.millis))); + mTitleTv.setText(item.event); + } + + @Override + public void onClick(View v) { + View layout = LayoutInflater.from(v.getContext()).inflate(R.layout.stats_battery_report, null); + TextView tv = layout.findViewById(R.id.tv_report); + tv.setText(getDetailInfo()); + AlertDialog dialog = new AlertDialog.Builder(v.getContext()) + .setTitle(mItem.event) + .setPositiveButton("确定", null) + .setCancelable(true) + .setView(layout) + .create(); + dialog.show(); + } + + protected String getDetailInfo() { + StringBuilder sb = new StringBuilder(); + sb.append("EVENT_ID: ").append(mItem.record.id).append("\n\n"); + for (String key : mItem.record.extras.keySet()) { + sb.append(key).append(" = ").append(mItem.record.extras.get(key)).append("\n\n"); + } + return sb.toString(); + } + } + + public static class EventBatteryHolder extends ViewHolder { + + private final TextView mTimeTv; + private final ImageView mIndicatorIv; + private final TextView mTitleTv; + + public EventBatteryHolder(@NonNull View itemView) { + super(itemView); + mTimeTv = itemView.findViewById(R.id.tv_time); + mIndicatorIv = itemView.findViewById(R.id.iv_indicator); + mTitleTv = itemView.findViewById(R.id.tv_title); + } + + @SuppressLint("SetTextI18n") + @Override + public void bind(Item.EventBatteryItem item) { + mItem = item; + mTimeTv.setText(sTimeFormatRef.safeGet().format(new Date(item.millis))); + mTitleTv.setText(item.event); + + mIndicatorIv.setImageLevel(1); + if (item.record.extras.containsKey("battery-low")) { + boolean lowBattery = item.record.getBoolean("battery-low", false); + mIndicatorIv.setImageLevel(lowBattery ? 4 : 2); + long pct = item.record.getDigit("battery-pct", -1); + mTitleTv.setText((lowBattery ? "电量低" : "电量恢复") + ((pct > 0 ? " (" + pct + "%)" : ""))); + return; + } + if (item.record.extras.containsKey("battery-temp")) { + long temp = item.record.getDigit("battery-temp", -1); + if (temp != -1) { + mIndicatorIv.setImageLevel(3); + } + long pct = item.record.getDigit("battery-pct", -1); + mTitleTv.setText("电池温度: " + (temp > 0 ? temp / 10f : "/") + "°C" + ((pct > 0 ? " (" + pct + "%)" : ""))); + return; + } + if (item.record.extras.containsKey("battery-pct")) { + long pct = item.record.getDigit("battery-pct", -1); + mTitleTv.setText("电量变化: " + (pct > 0 ? pct : "/") + "%"); + } + } + } + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsLoader.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsLoader.java new file mode 100644 index 000000000..cae2eea66 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/stats/ui/BatteryStatsLoader.java @@ -0,0 +1,286 @@ +package com.tencent.matrix.batterycanary.stats.ui; + +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; + +import com.tencent.matrix.batterycanary.BatteryCanary; +import com.tencent.matrix.batterycanary.monitor.AppStats; +import com.tencent.matrix.batterycanary.stats.BatteryRecord; +import com.tencent.matrix.batterycanary.stats.BatteryStatsFeature; +import com.tencent.matrix.batterycanary.stats.BatteryStatsFeature.BatteryRecords; +import com.tencent.matrix.batterycanary.stats.ui.BatteryStatsAdapter.Item; +import com.tencent.matrix.batterycanary.utils.Consumer; +import com.tencent.matrix.util.MatrixLog; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.Nullable; + +/** + * @author Kaede + * @since 2021/12/10 + */ +public class BatteryStatsLoader { + private static final String TAG = "Matrix.battery.loader"; + private static final int DAY_LIMIT = 7; + + protected final BatteryStatsAdapter mStatsAdapter; + protected final int mDayLimit; + protected final Handler mUiHandler = new Handler(Looper.getMainLooper()); + protected int mDayOffset = 0; + protected String mProc = ""; + + @Nullable + protected Filter mFilter; + + public BatteryStatsLoader(BatteryStatsAdapter statsAdapter) { + this(statsAdapter, DAY_LIMIT); + } + + public BatteryStatsLoader(BatteryStatsAdapter statsAdapter, int dayLimit) { + mStatsAdapter = statsAdapter; + mDayLimit = dayLimit; + } + + public BatteryStatsAdapter getAdapter() { + return mStatsAdapter; + } + + public List getDateSet() { + return mStatsAdapter.getDataList(); + } + + public void setFilter(Filter filter) { + mFilter = filter; + } + + public void removeFilter() { + mFilter = null; + } + + public void reset(String proc) { + mProc = proc; + reset(); + } + + public void reset() { + mDayOffset = 0; + postClearDataSet(); + } + + public void load() { + if (TextUtils.isEmpty(mProc)) { + MatrixLog.w(TAG, "Call #reset first!"); + return; + } + load(mDayOffset); + } + + public boolean loadMore() { + if (Math.abs(mDayOffset) >= mDayLimit) { + return false; + } + mDayOffset--; + load(mDayOffset); + return true; + } + + public Item.HeaderItem getFirstHeader(int topPosition) { + Item.HeaderItem currHeader = null; + List dataList = mStatsAdapter.getDataList(); + for (int i = topPosition; i >= 0; i--) { + if (topPosition < dataList.size()) { + if (dataList.get(i) instanceof Item.HeaderItem) { + currHeader = (Item.HeaderItem) dataList.get(i); + break; + } + } + } + return currHeader; + } + + protected void load(final int dayOffset) { + BatteryCanary.getMonitorFeature(BatteryStatsFeature.class, new Consumer() { + @Override + public void accept(BatteryStatsFeature batteryStatsFeature) { + List records = batteryStatsFeature.readRecords(dayOffset, mProc); + if (mFilter != null) { + records = mFilter.filtering(records); + } + BatteryRecords batteryRecords = new BatteryRecords(); + batteryRecords.date = BatteryStatsFeature.getDateString(dayOffset); + batteryRecords.records = records; + add(batteryRecords); + } + }); + } + + public void add(BatteryRecords batteryRecords) { + List dataList = onCreateDataSet(batteryRecords); + + // Footer + if (Math.abs(mDayOffset) >= mDayLimit) { + Item.HeaderItem headerItem = new Item.HeaderItem(); + headerItem.date = "END"; + dataList.add(headerItem); + Item.NoDataItem footerItem = new Item.NoDataItem(); + footerItem.text = "Only keep last " + mDayLimit + " days' data"; + dataList.add(footerItem); + } + + // Notify + postUpdateDataSet(dataList); + } + + private void postUpdateDataSet(final List dataList) { + mUiHandler.post(new Runnable() { + @Override + public void run() { + int start = mStatsAdapter.getDataList().size() - 1; + int length = dataList.size(); + mStatsAdapter.getDataList().addAll(dataList); + mStatsAdapter.notifyItemRangeChanged(Math.max(start, 0), length); + } + }); + } + + private void postClearDataSet() { + mUiHandler.post(new Runnable() { + @Override + public void run() { + int length = mStatsAdapter.getDataList().size(); + mStatsAdapter.getDataList().clear(); + mStatsAdapter.notifyItemRangeRemoved(0, length); + } + }); + } + + protected List onCreateDataSet(BatteryRecords batteryRecords) { + List dataList = new ArrayList<>(batteryRecords.records.size() + 1); + + // Records + if (batteryRecords.records.isEmpty()) { + // N0 DATA + Item.NoDataItem item = new Item.NoDataItem(); + item.text = "NO DATA"; + dataList.add(0, item); + + } else { + // Convert records to list items + for (BatteryRecord record : batteryRecords.records) { + Item item = onCreateDataItem(record); + dataList.add(0, item); + } + } + + // Date + Item.HeaderItem headerItem = new Item.HeaderItem(); + headerItem.date = batteryRecords.date; + dataList.add(0, headerItem); + + return dataList; + } + + protected Item onCreateDataItem(BatteryRecord record) { + if (record instanceof BatteryRecord.ProcStatRecord) { + Item.EventLevel1Item item = new Item.EventLevel1Item(record); + String title; + switch (((BatteryRecord.ProcStatRecord) record).procStat) { + case BatteryRecord.ProcStatRecord.STAT_PROC_LAUNCH: + title = "PROCESS_INIT"; + break; + case BatteryRecord.ProcStatRecord.STAT_PROC_OFF: + title = "PROCESS_QUIT"; + break; + default: + title = "PROCESS_ID_" + ((BatteryRecord.ProcStatRecord) record).procStat; + break; + } + item.text = title + " (pid " + ((BatteryRecord.ProcStatRecord) record).pid + ")"; + return item; + } + + if (record instanceof BatteryRecord.AppStatRecord) { + Item.EventLevel1Item item = new Item.EventLevel1Item(record); + switch (((BatteryRecord.AppStatRecord) record).appStat) { + case AppStats.APP_STAT_FOREGROUND: + item.text = "App 切换到前台"; + break; + case AppStats.APP_STAT_BACKGROUND: + item.text = "App 切换到后台"; + break; + case AppStats.APP_STAT_FOREGROUND_SERVICE: + item.text = "App 切换到后台 (有前台服务)"; + break; + case AppStats.APP_STAT_FLOAT_WINDOW: + item.text = "App 切换到后台 (有浮窗)"; + break; + default: + item.text = "App 状态变化: " + ((BatteryRecord.AppStatRecord) record).appStat; + break; + } + return item; + } + + if (record instanceof BatteryRecord.DevStatRecord) { + Item.EventLevel2Item item = new Item.EventLevel2Item(record); + switch (((BatteryRecord.DevStatRecord) record).devStat) { + case AppStats.DEV_STAT_CHARGING: + item.text = "CHARGE_ON"; + break; + case AppStats.DEV_STAT_UN_CHARGING: + item.text = "CHARGE_OFF"; + break; + case AppStats.DEV_STAT_DOZE_MODE_ON: + item.text = "低电耗模式(Doze) ON"; + break; + case AppStats.DEV_STAT_DOZE_MODE_OFF: + item.text = "低电耗模式(Doze) OFF"; + break; + case AppStats.DEV_STAT_SAVE_POWER_MODE_ON: + item.text = "待机模式(Standby) ON"; + break; + case AppStats.DEV_STAT_SAVE_POWER_MODE_OFF: + item.text = "待机模式(Standby) OFF"; + break; + case AppStats.DEV_STAT_SCREEN_ON: + item.text = "SCREEN_ON"; + break; + case AppStats.DEV_STAT_SCREEN_OFF: + item.text = "SCREEN_OFF"; + break; + default: + item.text = "设备状态变化: " + ((BatteryRecord.DevStatRecord) record).devStat; + break; + } + return item; + } + + if (record instanceof BatteryRecord.SceneStatRecord) { + Item.EventLevel2Item item = new Item.EventLevel2Item(record); + item.text = "UI: " + ((BatteryRecord.SceneStatRecord) record).scene; + return item; + } + + if (record instanceof BatteryRecord.ReportRecord) { + return new Item.EventDumpItem((BatteryRecord.ReportRecord) record); + } + + if (record instanceof BatteryRecord.EventStatRecord) { + if (BatteryRecord.EventStatRecord.EVENT_BATTERY_STAT.equals(((BatteryRecord.EventStatRecord) record).event)) { + return new Item.EventBatteryItem((BatteryRecord.EventStatRecord) record); + } + return new Item.EventSimpleItem((BatteryRecord.EventStatRecord) record); + } + + Item.EventLevel2Item item = new Item.EventLevel2Item(record); + item.text = "Unknown: " + record.getClass().getName(); + return item; + } + + public interface Filter { + List filtering(List input); + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/BatteryCanaryUtil.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/BatteryCanaryUtil.java index cf51100b2..f1f609d7d 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/BatteryCanaryUtil.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/BatteryCanaryUtil.java @@ -16,6 +16,7 @@ package com.tencent.matrix.batterycanary.utils; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.AlarmManager; import android.content.Context; @@ -30,27 +31,38 @@ import com.tencent.matrix.Matrix; import com.tencent.matrix.batterycanary.BatteryMonitorPlugin; import com.tencent.matrix.batterycanary.monitor.AppStats; +import com.tencent.matrix.lifecycle.owners.OverlayWindowLifecycleOwner; import com.tencent.matrix.util.MatrixLog; import java.io.File; import java.io.FileFilter; import java.io.RandomAccessFile; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; +import java.util.Map; import java.util.regex.Pattern; +import androidx.annotation.IntRange; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import static android.content.Context.ACTIVITY_SERVICE; import static com.tencent.matrix.batterycanary.monitor.AppStats.APP_STAT_BACKGROUND; +import static com.tencent.matrix.batterycanary.monitor.AppStats.APP_STAT_FLOAT_WINDOW; import static com.tencent.matrix.batterycanary.monitor.AppStats.APP_STAT_FOREGROUND; import static com.tencent.matrix.batterycanary.monitor.AppStats.APP_STAT_FOREGROUND_SERVICE; import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_CHARGING; -import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_SAVE_POWER_MODE; +import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_DOZE_MODE_OFF; +import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_DOZE_MODE_ON; +import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_SAVE_POWER_MODE_OFF; +import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_SAVE_POWER_MODE_ON; import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_SCREEN_OFF; +import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_SCREEN_ON; import static com.tencent.matrix.batterycanary.monitor.AppStats.DEV_STAT_UN_CHARGING; /** @@ -69,6 +81,7 @@ public final class BatteryCanaryUtil { public static final int JIFFY_MILLIS = 1000 / JIFFY_HZ; public interface Proxy { + String getProcessName(); String getPackageName(); int getBatteryTemperature(Context context); @@ -76,13 +89,17 @@ public interface Proxy { @AppStats.DevStatusDef int getDevStat(Context context); void updateAppStat(int value); void updateDevStat(int value); + int getBatteryPercentage(Context context); + int getBatteryCapacity(Context context); + long getBatteryCurrency(Context context); + int getCpuCoreNum(); - final class ExpireRef { - final int value; + final class ExpireRef { + final T value; final long aliveMillis; final long lastMillis; - ExpireRef(int value, long aliveMillis) { + ExpireRef(T value, long aliveMillis) { this.value = value; this.aliveMillis = aliveMillis; this.lastMillis = SystemClock.uptimeMillis(); @@ -94,12 +111,17 @@ boolean isExpired() { } } + @SuppressWarnings("SpellCheckingInspection") static Proxy sCacheStub = new Proxy() { private String mProcessName; private String mPackageName; - private ExpireRef mBatteryTemp; - private ExpireRef mLastAppStat; - private ExpireRef mLastDevStat; + private ExpireRef mBatteryTemp; + private ExpireRef mLastAppStat; + private ExpireRef mLastDevStat; + private ExpireRef mLastBattPct; + private ExpireRef mLastBattCap; + private ExpireRef mLastBattCur; + private ExpireRef mLastCpuCoreNum; @Override public String getProcessName() { @@ -133,7 +155,7 @@ public int getBatteryTemperature(Context context) { return mBatteryTemp.value; } int tmp = getBatteryTemperatureImmediately(context); - mBatteryTemp = new ExpireRef(tmp, DEFAULT_AMS_CACHE_MILLIS); + mBatteryTemp = new ExpireRef<>(tmp, DEFAULT_AMS_CACHE_MILLIS); return mBatteryTemp.value; } @@ -144,7 +166,7 @@ public int getAppStat(Context context, boolean isForeground) { return mLastAppStat.value; } int value = getAppStatImmediately(context, false); - mLastAppStat = new ExpireRef(value, DEFAULT_AMS_CACHE_MILLIS); + mLastAppStat = new ExpireRef<>(value, DEFAULT_AMS_CACHE_MILLIS); return mLastAppStat.value; } @@ -154,22 +176,65 @@ public int getDevStat(Context context) { return mLastDevStat.value; } int value = getDeviceStatImmediately(context); - mLastDevStat = new ExpireRef(value, DEFAULT_AMS_CACHE_MILLIS); + mLastDevStat = new ExpireRef<>(value, DEFAULT_AMS_CACHE_MILLIS); return mLastDevStat.value; } @Override public void updateAppStat(int value) { synchronized (this) { - mLastAppStat = new ExpireRef(value, DEFAULT_AMS_CACHE_MILLIS); + mLastAppStat = new ExpireRef<>(value, DEFAULT_AMS_CACHE_MILLIS); } } @Override public void updateDevStat(int value) { synchronized (this) { - mLastDevStat = new ExpireRef(value, DEFAULT_AMS_CACHE_MILLIS); + mLastDevStat = new ExpireRef<>(value, DEFAULT_AMS_CACHE_MILLIS); + } + } + + @Override + public int getBatteryPercentage(Context context) { + if (mLastBattPct != null && !mLastBattPct.isExpired()) { + return mLastBattPct.value; + } + int val = getBatteryPercentageImmediately(context); + mLastBattPct = new ExpireRef<>(val, ONE_MIN); + return mLastBattPct.value; + } + + @Override + public int getBatteryCapacity(Context context) { + if (mLastBattCap != null && !mLastBattCap.isExpired()) { + return mLastBattCap.value; + } + int val = getBatteryCapacityImmediately(context); + mLastBattCap = new ExpireRef<>(val, ONE_MIN); + return mLastBattCap.value; + } + + @Override + public long getBatteryCurrency(Context context) { + if (mLastBattCur != null && !mLastBattCur.isExpired()) { + return mLastBattCur.value; } + long val = getBatteryCurrencyImmediately(context); + mLastBattCur = new ExpireRef<>(val, ONE_MIN); + return mLastBattCur.value; + } + + @Override + public int getCpuCoreNum() { + if (mLastCpuCoreNum != null && !mLastCpuCoreNum.isExpired()) { + return mLastCpuCoreNum.value; + } + int val = getCpuCoreNumImmediately(); + if (val <= 1) { + return val; + } + mLastCpuCoreNum = new ExpireRef<>(val, ONE_HOR); + return mLastCpuCoreNum.value; } }; @@ -190,10 +255,13 @@ public static String getPackageName() { } public static String stackTraceToString(final StackTraceElement[] arr) { + return stackTraceToString(arr, false); + } + + public static String stackTraceToString(final StackTraceElement[] arr, boolean trim) { if (arr == null) { return ""; } - ArrayList stacks = new ArrayList<>(arr.length); for (StackTraceElement traceElement : arr) { String className = traceElement.getClassName(); @@ -208,21 +276,24 @@ public static String stackTraceToString(final StackTraceElement[] arr) { stacks.add(traceElement); } // stack still too large - String pkg = getPackageName(); - if (stacks.size() > DEFAULT_MAX_STACK_LAYER && !TextUtils.isEmpty(pkg)) { - ListIterator iterator = stacks.listIterator(stacks.size()); - // from backward to forward - while (iterator.hasPrevious()) { - StackTraceElement stack = iterator.previous(); - String className = stack.getClassName(); - if (!className.contains(pkg)) { - iterator.remove(); - } - if (stacks.size() <= DEFAULT_MAX_STACK_LAYER) { - break; + if (trim) { + String pkg = getPackageName(); + if (stacks.size() > DEFAULT_MAX_STACK_LAYER && !TextUtils.isEmpty(pkg)) { + ListIterator iterator = stacks.listIterator(stacks.size()); + // from backward to forward + while (iterator.hasPrevious()) { + StackTraceElement stack = iterator.previous(); + String className = stack.getClassName(); + if (!className.contains(pkg)) { + iterator.remove(); + } + if (stacks.size() <= DEFAULT_MAX_STACK_LAYER) { + break; + } } } } + StringBuilder sb = new StringBuilder(); for (StackTraceElement traceElement : stacks) { sb.append("\n").append("at ").append(traceElement); @@ -234,7 +305,7 @@ public static String getThrowableStack(Throwable throwable) { if (throwable == null) { return ""; } - return stackTraceToString(throwable.getStackTrace()); + return stackTraceToString(throwable.getStackTrace(), true); } public static long getUTCTriggerAtMillis(final long triggerAtMillis, final int type) { @@ -267,8 +338,9 @@ public static String getAlarmTypeString(final int type) { } public static int[] getCpuCurrentFreq() { - int[] output = new int[getCpuCoreNum()]; - for (int i = 0; i < getCpuCoreNum(); i++) { + int cpuCoreNum = getCpuCoreNum(); + int[] output = new int[cpuCoreNum]; + for (int i = 0; i < cpuCoreNum; i++) { output[i] = 0; String path = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq"; String cat = cat(path); @@ -283,7 +355,35 @@ public static int[] getCpuCurrentFreq() { return output; } + public static List getCpuFreqSteps() { + int cpuCoreNum = getCpuCoreNum(); + List output = new ArrayList<>(cpuCoreNum); + for (int i = 0; i < cpuCoreNum; i++) { + String path = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_available_frequencies"; + String cat = cat(path); + if (!TextUtils.isEmpty(cat)) { + //noinspection ConstantConditions + String[] split = cat.split(" "); + int[] steps = new int[split.length]; + for (int j = 0, splitLength = split.length; j < splitLength; j++) { + try { + String item = split[j]; + steps[j] = Integer.parseInt(item) / 1000; + } catch (Exception ignored) { + steps[j] = 0; + } + } + output.add(steps); + } + } + return output; + } + public static int getCpuCoreNum() { + return sCacheStub.getCpuCoreNum(); + } + + public static int getCpuCoreNumImmediately() { try { // Get directory containing CPU info File dir = new File("/sys/devices/system/cpu/"); @@ -295,13 +395,19 @@ public boolean accept(File pathname) { } }); // Return the number of cores (virtual CPU devices) + // noinspection ConstantConditions return files.length; } catch (Exception ignored) { // Default to return 1 core - return 1; + return getCpuCoreNumFromRuntime(); } } + public static int getCpuCoreNumFromRuntime() { + // fastest + return Runtime.getRuntime().availableProcessors(); + } + @Nullable public static String cat(String path) { if (TextUtils.isEmpty(path)) return null; @@ -319,14 +425,66 @@ public static int getBatteryTemperature(Context context) { public static int getBatteryTemperatureImmediately(Context context) { try { - Intent batIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + Intent batIntent = getBatteryStickyIntent(context); if (batIntent == null) return 0; return batIntent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); - } catch (Throwable ignored) { + } catch (Exception e) { + MatrixLog.w(TAG, "get EXTRA_TEMPERATURE failed: " + e.getMessage()); return 0; } } + public static int getThermalStat(Context context) { + return getThermalStatImmediately(context); + } + + public static int getThermalStatImmediately(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return powerManager.getCurrentThermalStatus(); + } catch (Exception e) { + MatrixLog.w(TAG, "getCurrentThermalStatus failed: " + e.getMessage()); + } + } + return -1; + } + + public static float getThermalHeadroom(Context context, @IntRange(from = 0, to = 60) int forecastSeconds) { + return getThermalHeadroomImmediately(context, forecastSeconds); + } + + public static float getThermalHeadroomImmediately(Context context, int forecastSeconds) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return powerManager.getThermalHeadroom(forecastSeconds); + } catch (Exception e) { + MatrixLog.w(TAG, "getThermalHeadroom failed: " + e.getMessage()); + } + } + return -1f; + } + + public static int getChargingWatt(Context context) { + return getChargingWattImmediately(context); + } + + public static int getChargingWattImmediately(Context context) { + // @See com.android.settingslib.fuelgauge.BatteryStatus + // Calculating muW = muA * muV / (10^6 mu^2 / mu); splitting up the divisor + // to maintain precision equally on both factors. + Intent intent = getBatteryStickyIntent(context); + if (intent != null) { + int maxCurrent = intent.getIntExtra("max_charging_current", -1); + int maxVoltage = intent.getIntExtra("max_charging_voltage", -1); + if (maxCurrent > 0 && maxVoltage > 0) { + return (maxCurrent / 1000) * (maxVoltage / 1000) / 1000000; + } + } + return -1; + } + @AppStats.AppStatusDef public static int getAppStat(Context context, boolean isForeground) { return sCacheStub.getAppStat(context, isForeground); @@ -338,6 +496,9 @@ public static int getAppStatImmediately(Context context, boolean isForeground) { if (hasForegroundService(context)) { return APP_STAT_FOREGROUND_SERVICE; // 后台(有前台服务) } + if (OverlayWindowLifecycleOwner.INSTANCE.hasOverlayWindow()) { + return APP_STAT_FLOAT_WINDOW; // 浮窗 + } return APP_STAT_BACKGROUND; // 后台 } @@ -357,7 +518,7 @@ public static int getDeviceStatImmediately(Context context) { return DEV_STAT_SCREEN_OFF; // 息屏 } if (isDeviceOnPowerSave(context)) { - return DEV_STAT_SAVE_POWER_MODE; // 省电模式开启 + return DEV_STAT_SAVE_POWER_MODE_ON; // 省电模式开启 } return DEV_STAT_UN_CHARGING; } @@ -370,6 +531,8 @@ public static String convertAppStat(@AppStats.AppStatusDef int appStat) { return "bg"; case APP_STAT_FOREGROUND_SERVICE: return "fgSrv"; + case APP_STAT_FLOAT_WINDOW: + return "float"; default: return "unknown"; } @@ -381,25 +544,28 @@ public static String convertDevStat(@AppStats.DevStatusDef int devStat) { return "charging"; case DEV_STAT_UN_CHARGING: return "non_charge"; + case DEV_STAT_SCREEN_ON: + return "screen_on"; case DEV_STAT_SCREEN_OFF: return "screen_off"; - case DEV_STAT_SAVE_POWER_MODE: - return "doze"; + case DEV_STAT_DOZE_MODE_ON: + return "doze_on"; + case DEV_STAT_DOZE_MODE_OFF: + return "doze_off"; + case DEV_STAT_SAVE_POWER_MODE_ON: + return "standby_on"; + case DEV_STAT_SAVE_POWER_MODE_OFF: + return "standby_off"; default: return "unknown"; } } - public static boolean isDeviceChargingV1(Context context) { - try { - Intent batIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - if (batIntent == null) return false; - int status = batIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); - return (status == BatteryManager.BATTERY_STATUS_CHARGING) || (status == BatteryManager.BATTERY_STATUS_FULL); - } catch (Throwable ignored) { - return false; - } + Intent batIntent = getBatteryStickyIntent(context); + if (batIntent == null) return false; + int status = batIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + return (status == BatteryManager.BATTERY_STATUS_CHARGING) || (status == BatteryManager.BATTERY_STATUS_FULL); } public static boolean isDeviceChargingV2(Context context) { @@ -410,7 +576,7 @@ public static boolean isDeviceChargingV2(Context context) { } } try { - Intent batIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + Intent batIntent = getBatteryStickyIntent(context); if (batIntent == null) return false; int plugged = batIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); return plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS; @@ -434,6 +600,25 @@ public static boolean isDeviceScreenOn(Context context) { return false; } + /** + * System Doze Mode + */ + public static boolean isDeviceOnIdleMode(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (pm != null) { + return pm.isDeviceIdleMode(); + } + } catch (Exception ignored) { + } + } + return false; + } + + /** + * App Standby Mode + */ public static boolean isDeviceOnPowerSave(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { @@ -447,6 +632,96 @@ public static boolean isDeviceOnPowerSave(Context context) { return false; } + @Nullable + static Intent getBatteryStickyIntent(Context context) { + try { + return context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + } catch (Exception e) { + MatrixLog.w(TAG, "get ACTION_BATTERY_CHANGED failed: " + e.getMessage()); + return null; + } + } + + public static boolean isLowBattery(Context context) { + Intent batIntent = getBatteryStickyIntent(context); + if (batIntent != null) { + batIntent.getBooleanExtra(Intent.ACTION_BATTERY_LOW, false); + } + return false; + } + + public static int getBatteryPercentage(Context context) { + return sCacheStub.getBatteryPercentage(context); + } + + public static int getBatteryPercentageImmediately(Context context) { + Intent batIntent = getBatteryStickyIntent(context); + if (batIntent != null) { + int level = batIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int scale = batIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + if (scale > 0) { + return level * 100 / scale; + } + } + return -1; + } + + public static int getBatteryCapacity(Context context) { + return sCacheStub.getBatteryCapacity(context); + } + + @SuppressWarnings("ConstantConditions") + @SuppressLint("PrivateApi") + public static int getBatteryCapacityImmediately(Context context) { + /* + * Matrix PowerProfile (static) >> OS PowerProfile (static) >> BatteryManager (dynamic) + */ + try { + if (PowerProfile.getResType().equals("framework") || PowerProfile.getResType().equals("custom")) { + return (int) PowerProfile.init(context).getBatteryCapacity(); + } + } catch (Throwable ignored) { + } + + try { + Class profileClass = Class.forName("com.android.internal.os.PowerProfile"); + Object profileObject = profileClass.getConstructor(Context.class).newInstance(context); + Method method; + try { + method = profileClass.getMethod("getAveragePower", String.class); + double capacity = (double) method.invoke(profileObject, PowerProfile.POWER_BATTERY_CAPACITY); + return (int) capacity; + } catch (Throwable e) { + MatrixLog.w(TAG, "get PowerProfile failed: " + e.getMessage()); + } + method = profileClass.getMethod("getBatteryCapacity"); + return (int) method.invoke(profileObject); + } catch (Throwable ignored) { + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + BatteryManager mBatteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); + int chargeCounter = mBatteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER); + int capacity = mBatteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); + if (chargeCounter > 0 && capacity > 0) { + return (int) (((chargeCounter / (float) capacity) * 100) / 1000); + } + } + return -1; + } + + public static long getBatteryCurrency(Context context) { + return sCacheStub.getBatteryCurrency(context); + } + + public static long getBatteryCurrencyImmediately(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + BatteryManager mBatteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); + return mBatteryManager.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW); + } + return -1; + } + public static boolean hasForegroundService(Context context) { try { ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); @@ -519,4 +794,15 @@ public static long computeAvgByMinute(long input, long millis) { return input / Math.max(1, (millis) / ONE_MIN); } } + + public static Map sortMapByValue(Map map, Comparator> comparator) { + List> list = new ArrayList<>(map.entrySet()); + Collections.sort(list, comparator); + + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : list) { + result.put(entry.getKey(), entry.getValue()); + } + return result; + } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/BatteryCurrencyInspector.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/BatteryCurrencyInspector.java new file mode 100644 index 000000000..c3186b072 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/BatteryCurrencyInspector.java @@ -0,0 +1,67 @@ +package com.tencent.matrix.batterycanary.utils; + +import android.content.Context; +import android.os.BatteryManager; + +import androidx.annotation.Nullable; + +/** + * Since {@link BatteryManager#BATTERY_PROPERTY_CURRENT_NOW} will return microAmp/millisAmp & + * positive/negative value in difference devices, here we figure out the unit somehow. + * + * @author Kaede + * @since 9/9/2022 + */ +public class BatteryCurrencyInspector { + + /** + * To get a high currency value, you are supposed to call this API within a high cpu-load + * activity & without charging. (When the device's cpu-load is very low, we can not tell + * microAmp or millisAmp.) + */ + @Nullable + public static Boolean isMicroAmpCurr(Context context, int threshold) { + if (BatteryCanaryUtil.isDeviceCharging(context)) { + // Currency might be very low in charge + return null; + } + long val = BatteryCanaryUtil.getBatteryCurrencyImmediately(context); + if (val == -1) { + return null; + } + return isMicroAmp(val, threshold); + } + + @Nullable + public static Boolean isMicroAmpCurr(Context context) { + return isMicroAmpCurr(context, 1000); + } + + public static boolean isMicroAmp(long amp, int threshold) { + return amp > threshold; + } + + @Nullable + public static Boolean isPositiveInChargeCurr(Context context) { + if (!BatteryCanaryUtil.isDeviceCharging(context)) { + return null; + } + long val = BatteryCanaryUtil.getBatteryCurrencyImmediately(context); + if (val == -1) { + return null; + } + return val > 0; + } + + @Nullable + public static Boolean isPositiveOutOfChargeCurr(Context context) { + if (BatteryCanaryUtil.isDeviceCharging(context)) { + return null; + } + long val = BatteryCanaryUtil.getBatteryCurrencyImmediately(context); + if (val == -1) { + return null; + } + return val > 0; + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/CallStackCollector.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/CallStackCollector.java new file mode 100644 index 000000000..0018dfce0 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/CallStackCollector.java @@ -0,0 +1,41 @@ +package com.tencent.matrix.batterycanary.utils; + +import android.os.Looper; +import android.os.Process; + +/** + * @author Kaede + * @since 2021/12/17 + */ +public class CallStackCollector { + + public String collectCurr() { + return collect(new Throwable()); + } + + public String collectUiThread() { + return collect(Looper.getMainLooper().getThread()); + } + + public String collect(Throwable throwable) { + return collect(throwable.getStackTrace()); + } + + public String collect(Thread thread) { + return collect(thread.getStackTrace()); + } + + public String collect(StackTraceElement[] elements) { + return BatteryCanaryUtil.stackTraceToString(elements); + } + + public String collect(int tid) { + if (tid == Process.myTid()) { + return collectCurr(); + } + if (tid == Process.myPid()) { + return collectUiThread(); + } + return ""; // Unwind needed + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/Function.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/Function.java new file mode 100644 index 000000000..0886b57dd --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/Function.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.tencent.matrix.batterycanary.utils; + +/** + * Compat version of {@link java.util.function.Function} + * + * @param the type of the input to the function + * @param the type of the result of the function + * @since 1.8 + */ +public interface Function { + R apply(T t); +} + diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/KernelCpuSpeedReader.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/KernelCpuSpeedReader.java index cd927965a..010aa133e 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/KernelCpuSpeedReader.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/KernelCpuSpeedReader.java @@ -39,7 +39,7 @@ public void smoke() throws IOException { } } - public long readTotoal() throws IOException { + public long readTotal() throws IOException { long sum = 0; for (long item : readAbsolute()) { sum += item; diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/KernelCpuUidFreqTimeReader.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/KernelCpuUidFreqTimeReader.java index 248afed47..2d4d3e0c6 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/KernelCpuUidFreqTimeReader.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/KernelCpuUidFreqTimeReader.java @@ -54,7 +54,7 @@ public void smoke() throws IOException { } } - public List readTotoal() throws IOException { + public List readTotal() throws IOException { List cpuCoreStepJiffies = readAbsolute(); List cpuCoreJiffies = new ArrayList<>(cpuCoreStepJiffies.size()); for (long[] stepJiffies : cpuCoreStepJiffies) { diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/PowerProfile.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/PowerProfile.java index 09f6881b2..6b38c364b 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/PowerProfile.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/PowerProfile.java @@ -3,16 +3,29 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; +import android.os.Build; +import android.system.Os; + +import com.tencent.matrix.util.MatrixLog; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; +import java.util.concurrent.Callable; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; +import androidx.annotation.StringDef; +import androidx.annotation.VisibleForTesting; /** * @see com.android.internal.os.PowerProfile @@ -20,6 +33,17 @@ @RestrictTo(RestrictTo.Scope.LIBRARY) @SuppressWarnings({"JavadocReference", "ConstantConditions", "TryFinallyCanBeTryWithResources"}) public class PowerProfile { + @StringDef(value = { + "unknown", + "framework", + "custom", + "test" + }) + @Retention(RetentionPolicy.SOURCE) + @interface ResType { + } + + private static final String TAG = "PowerProfile"; private static PowerProfile sInstance = null; @Nullable @@ -30,15 +54,17 @@ public static PowerProfile getInstance() { public static PowerProfile init(Context context) throws IOException { synchronized (sLock) { try { - sInstance = new PowerProfile(context).smoke(); + if (sInstance == null) { + sInstance = new PowerProfile(context).smoke(); + } return sInstance; } catch (Throwable e) { - throw new IOException(e); + throw new IOException("Compat err: " + e.getMessage(), e); } } } - public PowerProfile smoke() throws IOException { + PowerProfile smoke() throws IOException { if (getNumCpuClusters() <= 0) { throw new IOException("Invalid cpu clusters: " + getNumCpuClusters()); } @@ -66,6 +92,21 @@ public boolean isSupported() { } } + public double getAveragePowerUni(String type) { + int num = getNumElements(type); + if (num > 0) { + // Array + double sum = 0; + for (int i = 0; i < num; i++) { + sum += getAveragePower(type, i); + } + return sum / num; + } else { + // Item + return getAveragePower(type); + } + } + public int getCpuCoreNum() { int cpuCoreNumInProfile = 0; for (int i = 0; i < getNumCpuClusters(); i++) { @@ -203,12 +244,14 @@ public int getClusterByCpuNum(int cpuCoreNum) { * to the CPU power, probably due to a DSP and / or amplifier. */ public static final String POWER_AUDIO = "audio"; + public static final String POWER_AUDIO_DSP = "dsp.audio"; /** * Power consumed by any media hardware when playing back video content. This is in addition * to the CPU power, probably due to a DSP. */ public static final String POWER_VIDEO = "video"; + public static final String POWER_VIDEO_DSP = "dsp.video"; /** * Average power consumption when camera flashlight is on. @@ -257,22 +300,154 @@ public int getClusterByCpuNum(int cpuCoreNum) { private static final Object sLock = new Object(); - PowerProfile(Context context) { // Read the XML file for the given profile (normally only one per device) synchronized (sLock) { if (sPowerItemMap.size() == 0 && sPowerArrayMap.size() == 0) { - readPowerValuesFromXml(context); + try { + readPowerValuesCompat(context); + } catch (IOException e) { + MatrixLog.w(TAG, "Failed to read power values: " + e); + } } initCpuClusters(); } } + @VisibleForTesting + public static HashMap getPowerItemMap() { + return sPowerItemMap; + } + + @VisibleForTesting + public static HashMap getPowerArrayMap() { + return sPowerArrayMap; + } + + private static String mResType = "unknown"; + + @ResType + public static String getResType() { + return mResType; + } + + private void readPowerValuesCompat(Context context) throws IOException { + Exception exception = null; + try { + readPowerValuesFromRes(context, "power_profile"); + initCpuClusters(); + smoke(); + mResType = "framework"; + } catch (Exception e) { + MatrixLog.w(TAG, "read from framework failed: " + e); + clear(); + exception = e; + } + + if (exception != null) { + Callable findBlock = new Callable() { + @SuppressWarnings("checkstyle:RegexpSingleline") + @Override + public File call() throws FileNotFoundException { + String targetFileName = "/xml/power_profile.xml"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String customDirs = Os.getenv("CUST_POLICY_DIRS"); + for (String dir : customDirs.split(":")) { + // example: /hw_product/etc/xml/power_profile.xml + File file = new File(dir, targetFileName); + if (file.exists() && file.canRead()) { + MatrixLog.i(TAG, "find profile xml: " + file); + return file; + } + } + } + throw new FileNotFoundException(targetFileName); + } + }; + try { + exception = null; + readPowerValuesFromFilePath(context, findBlock.call()); + initCpuClusters(); + smoke(); + mResType = "custom"; + } catch (Exception e) { + MatrixLog.w(TAG, "read from custom failed: " + e); + clear(); + exception = e; + } + } + + if (exception != null) { + try { + exception = null; + readPowerValuesFromRes(context, "power_profile_test"); + initCpuClusters(); + smoke(); + mResType = "test"; + } catch (Exception e) { + MatrixLog.w(TAG, "read from test failed: " + e); + clear(); + exception = e; + } + } + + if (exception != null) { + throw new IOException("readPowerValuesCompat failed", exception); + } + } + + @VisibleForTesting + public void readPowerValuesFromRes(Context context, String fileName) { + XmlResourceParser parser = null; + try { + final int id = context.getResources().getIdentifier(fileName, "xml", "android"); + final Resources resources = context.getResources(); + parser = resources.getXml(id); + readPowerValuesFromXml(context, parser); + } catch (Exception e) { + throw new RuntimeException("Error reading res " + fileName + ": " + e.getMessage(), e); + } finally { + if (parser != null) { + try { + parser.close(); + } catch (Exception e) { + // ignore + } + } + } + } + + @VisibleForTesting + public void readPowerValuesFromFilePath(Context context, File xmlFile) { + FileInputStream is = null; + try { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + XmlPullParser parser = factory.newPullParser(); + is = new FileInputStream(xmlFile); + parser.setInput(is, null); + readPowerValuesFromXml(context, parser); + } catch (Exception e) { + throw new RuntimeException("Error reading file " + xmlFile + ": " + e.getMessage(), e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + @VisibleForTesting + public void clear() { + sPowerItemMap.clear(); + sPowerArrayMap.clear(); + } + @SuppressWarnings({"ToArrayCallWithZeroLengthArrayArgument", "UnnecessaryBoxing", "CatchMayIgnoreException", "TryWithIdenticalCatches"}) - private void readPowerValuesFromXml(Context context) { - final int id = context.getResources().getIdentifier("power_profile", "xml", "android"); - final Resources resources = context.getResources(); - XmlResourceParser parser = resources.getXml(id); + private void readPowerValuesFromXml(Context context, XmlPullParser parser) { boolean parsingArray = false; ArrayList array = new ArrayList<>(); String arrayName = null; @@ -320,8 +495,6 @@ private void readPowerValuesFromXml(Context context) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); - } finally { - parser.close(); } // Now collect other config variables. @@ -346,7 +519,7 @@ private void readPowerValuesFromXml(Context context) { if ((sPowerItemMap.containsKey(key) && sPowerItemMap.get(key) > 0)) { continue; } - int value = resources.getInteger(configResIds[i]); + int value = context.getResources().getInteger(configResIds[i]); if (value > 0) { sPowerItemMap.put(key, (double) value); } @@ -360,7 +533,7 @@ private void readPowerValuesFromXml(Context context) { private static final String CPU_CORE_SPEED_PREFIX = "cpu.core_speeds.cluster"; private static final String CPU_CORE_POWER_PREFIX = "cpu.core_power.cluster"; - private void initCpuClusters() { + void initCpuClusters() { if (sPowerArrayMap.containsKey(CPU_PER_CLUSTER_CORE_COUNT)) { final Double[] data = sPowerArrayMap.get(CPU_PER_CLUSTER_CORE_COUNT); mCpuClusters = new CpuClusterKey[data.length]; diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/ProcStatUtil.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/ProcStatUtil.java index bd39ba55b..3aec48917 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/ProcStatUtil.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/ProcStatUtil.java @@ -39,6 +39,14 @@ static byte[] getLocalBuffers() { ProcStatUtil() { } + public static boolean exists(int pid) { + return new File("/proc/" + pid + "/stat").exists(); + } + + public static boolean exists(int pid, int tid) { + return new File("/proc/" + pid + "/task/" + tid + "/stat").exists(); + } + @Nullable public static ProcStat currentPid() { return of(Process.myPid()); diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/RadioStatUtil.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/RadioStatUtil.java index a8afc57be..1f4dddecf 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/RadioStatUtil.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/RadioStatUtil.java @@ -1,15 +1,25 @@ package com.tencent.matrix.batterycanary.utils; +import android.Manifest; +import android.annotation.SuppressLint; import android.app.usage.NetworkStats; import android.app.usage.NetworkStatsManager; import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.Network; import android.net.NetworkCapabilities; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; +import android.net.NetworkInfo; +import android.os.Build; import com.tencent.matrix.batterycanary.BuildConfig; import com.tencent.matrix.util.MatrixLog; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.core.util.Pair; + /** * @author Kaede * @since 2020/12/8 @@ -20,6 +30,7 @@ public final class RadioStatUtil { private static final String TAG = "Matrix.battery.ProcStatUtil"; static final long MIN_QUERY_INTERVAL = BuildConfig.DEBUG ? 0L : 2000L; static long sLastQueryMillis; + static RadioStat sLastRef; private static boolean checkIfFrequently() { long currentTimeMillis = System.currentTimeMillis(); @@ -35,10 +46,10 @@ public static RadioStat getCurrentStat(Context context) { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) { return null; } - if (checkIfFrequently()) { - MatrixLog.i(TAG, "over frequently just return"); - return null; - } + + // if (checkIfFrequently()) { + // return sLastRef; + // } try { NetworkStatsManager network = (NetworkStatsManager) context.getSystemService(Context.NETWORK_STATS_SERVICE); @@ -53,6 +64,8 @@ public static RadioStat getCurrentStat(Context context) { if (bucket.getUid() == android.os.Process.myUid()) { stat.wifiRxBytes += bucket.getRxBytes(); stat.wifiTxBytes += bucket.getTxBytes(); + stat.wifiRxPackets += bucket.getRxPackets(); + stat.wifiTxPackets += bucket.getTxPackets(); } } } @@ -64,23 +77,85 @@ public static RadioStat getCurrentStat(Context context) { if (bucket.getUid() == android.os.Process.myUid()) { stat.mobileRxBytes += bucket.getRxBytes(); stat.mobileTxBytes += bucket.getTxBytes(); + stat.mobileRxPackets += bucket.getRxPackets(); + stat.mobileTxPackets += bucket.getTxPackets(); } } } } - + sLastRef = stat; return stat; } catch (Throwable e) { MatrixLog.w(TAG, "querySummary fail: " + e.getMessage()); + sLastRef = null; + return null; + } + } + + @Nullable + public static RadioBps getCurrentBps(Context context) { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return null; + } + try { + RadioBps stat = new RadioBps(); + Pair wifi = getCurrentBps(context, "WIFI"); + stat.wifiRxBps = wifi.first == null ? 0 : wifi.first; + stat.wifiTxBps = wifi.second == null ? 0 : wifi.second; + + Pair mobile = getCurrentBps(context, "MOBILE"); + stat.mobileRxBps = mobile.first == null ? 0 : mobile.first; + stat.mobileTxBps = mobile.second == null ? 0 : mobile.second; + return stat; + } catch (Exception e) { + MatrixLog.w(TAG, "getBps err: " + e.getMessage()); return null; } } + @SuppressLint("MissingPermission") + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private static Pair getCurrentBps(Context context, String typeName) { + long rxBwBps = 0, txBwBps = 0; + if (context.checkCallingOrSelfPermission(Manifest.permission.ACCESS_NETWORK_STATE) == PackageManager.PERMISSION_GRANTED) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + for (Network item : manager.getAllNetworks()) { + NetworkInfo networkInfo = manager.getNetworkInfo(item); + if (networkInfo != null + && (networkInfo.isConnected() || networkInfo.isConnectedOrConnecting()) + && networkInfo.getTypeName().equalsIgnoreCase(typeName)) { + NetworkCapabilities capabilities = manager.getNetworkCapabilities(item); + if (capabilities != null) { + rxBwBps = capabilities.getLinkDownstreamBandwidthKbps() * 1024L; + txBwBps = capabilities.getLinkUpstreamBandwidthKbps() * 1024L; + if (rxBwBps > 0 || txBwBps > 0) { + break; + } + } + } + } + } + return new Pair<>(rxBwBps, txBwBps); + } + public static final class RadioStat { public long wifiRxBytes; public long wifiTxBytes; + public long wifiRxPackets; + public long wifiTxPackets; + public long mobileRxBytes; public long mobileTxBytes; + public long mobileRxPackets; + public long mobileTxPackets; + } + + public static final class RadioBps { + public long wifiRxBps; + public long wifiTxBps; + + public long mobileRxBps; + public long mobileTxBps; } } diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/ThreadSafeReference.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/ThreadSafeReference.java new file mode 100644 index 000000000..39881876d --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/ThreadSafeReference.java @@ -0,0 +1,34 @@ +package com.tencent.matrix.batterycanary.utils; + +import java.lang.ref.WeakReference; + +import androidx.annotation.NonNull; + +/** + * @author Kaede + * @since 2022/1/4 + */ +public abstract class ThreadSafeReference { + ThreadLocal> mThreadLocalRef; + + @NonNull + public abstract T onCreate(); + + @NonNull + public T safeGet() { + if (mThreadLocalRef != null) { + WeakReference ref = mThreadLocalRef.get(); + if (ref != null) { + T target = ref.get(); + if (target != null) { + return target; + } + } + } + T target = onCreate(); + WeakReference ref = new WeakReference<>(target); + mThreadLocalRef = new ThreadLocal<>(); + mThreadLocalRef.set(ref); + return target; + } +} diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/TimeBreaker.java b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/TimeBreaker.java index 4b797953a..5e477b7ca 100644 --- a/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/TimeBreaker.java +++ b/matrix/matrix-android/matrix-battery-canary/src/main/java/com/tencent/matrix/batterycanary/utils/TimeBreaker.java @@ -11,6 +11,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; /** * Configure timeline portions & ratio for the given stamps and return split-portions with each weight. @@ -185,6 +186,22 @@ public Stamp(String key, long upTime) { this.upTime = upTime; this.statMillis = System.currentTimeMillis(); } + + @VisibleForTesting + public Stamp(String key, long upTime, long statMillis) { + this.key = key; + this.upTime = upTime; + this.statMillis = statMillis; + } + + @Override + public String toString() { + return "Stamp{" + + "key='" + key + '\'' + + ", upTime=" + upTime + + ", statMillis=" + statMillis + + '}'; + } } public static final class TimePortions { diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_1.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_1.xml new file mode 100644 index 000000000..2e20ec746 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_1.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_2.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_2.xml new file mode 100644 index 000000000..8dcf4bd78 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_2.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_3.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_3.xml new file mode 100644 index 000000000..0e7b01014 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_3.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_4.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_4.xml new file mode 100644 index 000000000..4866c5ae4 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_4.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_list.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_list.xml new file mode 100644 index 000000000..fca00bb24 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_battery_list.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_1.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_1.xml new file mode 100644 index 000000000..24327b345 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_1.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_2.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_2.xml new file mode 100644 index 000000000..c087eca65 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_2.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_3.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_3.xml new file mode 100644 index 000000000..dd38e389b --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_3.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_4.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_4.xml new file mode 100644 index 000000000..016afb35f --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_4.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_list.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_list.xml new file mode 100644 index 000000000..12ceb8188 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_circle_list.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_triangle.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_triangle.xml new file mode 100644 index 000000000..750c0e38a --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/drawable/stats_icon_triangle.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/activity_battery_stats.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/activity_battery_stats.xml new file mode 100644 index 000000000..0ae0f757c --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/activity_battery_stats.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_power_entry.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_power_entry.xml new file mode 100644 index 000000000..c04f18b50 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_power_entry.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_proc_entry.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_proc_entry.xml new file mode 100644 index 000000000..7ac8b48c1 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_proc_entry.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_thread_entry.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_thread_entry.xml new file mode 100644 index 000000000..15d545f8e --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_item_thread_entry.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_top_thread.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_top_thread.xml new file mode 100644 index 000000000..de6771a4d --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_top_thread.xml @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_top_thread_container.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_top_thread_container.xml new file mode 100644 index 000000000..d2bd9366b --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/float_top_thread_container.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_battery_report.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_battery_report.xml new file mode 100644 index 000000000..1c928b153 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_battery_report.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_1.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_1.xml new file mode 100644 index 000000000..1546c66cc --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_1.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_2.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_2.xml new file mode 100644 index 000000000..d9319f187 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_2.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_battery.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_battery.xml new file mode 100644 index 000000000..9d40d8c02 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_battery.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump.xml new file mode 100644 index 000000000..7a9495f78 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_general.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_general.xml new file mode 100644 index 000000000..78af7555d --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_general.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_header.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_header.xml new file mode 100644 index 000000000..2f5507336 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_header.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_subentry.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_subentry.xml new file mode 100644 index 000000000..1dc55a39f --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_subentry.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_thread.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_thread.xml new file mode 100644 index 000000000..2dbf05d84 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_dump_entry_thread.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_simple.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_simple.xml new file mode 100644 index 000000000..618e9f26d --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_event_simple.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_header.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_header.xml new file mode 100644 index 000000000..077765307 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_header.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_no_data.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_no_data.xml new file mode 100644 index 000000000..62564535a --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/layout/stats_item_no_data.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/matrix-battery-canary/src/main/res/values/weui_color.xml b/matrix/matrix-android/matrix-battery-canary/src/main/res/values/weui_color.xml new file mode 100644 index 000000000..27e84be22 --- /dev/null +++ b/matrix/matrix-android/matrix-battery-canary/src/main/res/values/weui_color.xml @@ -0,0 +1,261 @@ + + + + + @color/BW_0_Alpha_0_9 + @color/BW_0_Alpha_0_9 + @color/BW_0_Alpha_0_5 + @color/BW_0_Alpha_0_3 + @color/BW_0_Alpha_0_1 + @color/BW_0_Alpha_0_1_5 + @color/BW_93 + @color/BW_97 + @color/BW_100 + @color/BW_97 + @color/BW_BG_30 + #FFFFFF + + + + #00000000 + #FFFFFF + #000000 + @color/BW_100 + #FFFFFF + @color/BW_97 + #F7F7F7 + #E6F7F7F7 + @color/BW_93 + @color/BW_93 + #EDEDED + #E5E5E5 + #B2B2B2 + #808080 + @color/BW_97 + @color/BW_93 + #4C4C4C + #5a5a5a + #191919 + #804C4C4C + + #08000000 + #0D000000 + #1A000000 + #21000000 + #26000000 + #33000000 + #40000000 + #4D000000 + #66000000 + #8C000000 + #CC000000 + #E6000000 + #CCFFFFFF + + #D9F5F5F5 + #A6AAAAAA + #A65B5B5B + #CC333333 + #CC0C0C0C + + + #FA5151 + #FEEDED + #FDCACA + #FB7373 + #FA5151 + #E14949 + #C84040 + #DD847E + #D3625A + #CF5148 + #B94840 + #33FA5151 + + #DC3636 + #C84040 + #AE3838 + #33DC3636 + + #4dFA5151 + + + + #FA9D3B + #FDE1C3 + #FBB062 + #FA9D3B + #E08C34 + #C87D2F + #F0A04D + #EC8519 + #EA7800 + #D26B00 + + #E17719 + #C87D2F + #AE6D29 + + + #FFC300 + #FFECB2 + #FFCF33 + #FFC300 + #E6AF00 + #CC9C00 + #F3CC4D + #F0BD19 + #EFB600 + #D7A400 + + #BB8E00 + #CC9C00 + #B28800 + #BB8E00 + + + #91D300 + #DEF1B3 + #A7DB33 + #91D300 + #82BD00 + #74A800 + #B5D179 + #A0C452 + #96BE40 + #86AA39 + + #4F8400 + #74A800 + #659300 + + + #95EC69 + #DEF9D1 + #AAEF87 + #95EC69 + #85D35E + #77BC54 + #9CDD90 + #80D370 + #72CF60 + #66B956 + + #2E8800 + #77BC54 + #6AA84B + + + #07C160 + #B4ECCE + #69c694 + #07C160 + #06AE56 + #059A4C + #69C694 + #3EB575 + #2AAE67 + #259C5C + #1A07C160 + #3307C160 + #4D07C160 + #8007C160 + + #018942 + #059A4C + #048A44 + #018942 + #33018942 + + + #576B95 + #CCD2DE + #7888AA + #576B95 + #4E6085 + #455577 + #99576B95 + #4D576B95 + + #4E6085 + #455577 + #3D4C6A + + + #10AEFF + #B7E6FF + #3FBEFF + #10AEFF + #0E9CE6 + #0C8BCC + #7FC0EA + #5AAFE4 + #48A6E2 + #4095CB + + #007DBB + #0C8BCC + #0A7CB7 + + + #1485EE + #B8DAF9 + #439DF1 + #1485EE + #1277D6 + #106ABE + #0C4F8E + #6BA0D2 + #3F84C5 + #2B77BF + #266AAB + + #1277D6 + #106ABE + #0E5FAA + + + #6467F0 + #D0D1FA + #8385F3 + #6467F0 + #595CD7 + #5052C0 + #9496CE + #7678C1 + #6769BA + #5C5EA7 + + #595CD7 + #5052C0 + #4749AC + + + + @color/BW_0_Alpha_0_9 + @color/BW_0_Alpha_0_5 + @color/BW_0_Alpha_0_3 + @color/BW_0_Alpha_0_2 + @color/BW_0_Alpha_0_9 + @color/BW_0_Alpha_0_5 + #06AE56 + #FFFFFF + @color/Brand + @color/Link_100 + @color/Link_Alpha_0_6 + @color/Link_Alpha_0_3 + @color/BW_0_Alpha_0_9 + @color/BW_0_Alpha_0_2 + @color/Red_90 + @color/brand_text_color + @color/BW_0_Alpha_0_5 + @color/normal_text_color + @color/BW_0_Alpha_0_3 + @color/BW_0_Alpha_0_3 + @color/BW_0_Alpha_0_5 + @color/BW_0_Alpha_0_5 + @color/BW_0_Alpha_0_9 + @color/BW_0_Alpha_0_3 + @color/BW_0_Alpha_0_5 + #33000000 + diff --git a/matrix/matrix-android/matrix-fd/build.gradle b/matrix/matrix-android/matrix-fd/build.gradle index 664937ec2..28ecca8d2 100644 --- a/matrix/matrix-android/matrix-fd/build.gradle +++ b/matrix/matrix-android/matrix-fd/build.gradle @@ -47,7 +47,6 @@ if("External" == rootProject.ext.PUBLISH_CHANNEL) { } else { apply from: rootProject.file('gradle/WeChatPublish.gradle') - apply from: rootProject.file('gradle/WeChatNativeDepend.gradle') wechatPublish { artifactId = POM_ARTIFACT_ID } diff --git a/matrix/matrix-android/matrix-gradle-plugin/src/main/java/com/tencent/matrix/shrinker/ApkUtil.java b/matrix/matrix-android/matrix-gradle-plugin/src/main/java/com/tencent/matrix/shrinker/ApkUtil.java index b24f9dcfc..fe787fe02 100644 --- a/matrix/matrix-android/matrix-gradle-plugin/src/main/java/com/tencent/matrix/shrinker/ApkUtil.java +++ b/matrix/matrix-android/matrix-gradle-plugin/src/main/java/com/tencent/matrix/shrinker/ApkUtil.java @@ -20,7 +20,9 @@ import com.tencent.matrix.javalib.util.Pair; import com.tencent.matrix.javalib.util.Util; +import com.tencent.matrix.resguard.ResguardMapping; import org.gradle.api.GradleException; +import org.jetbrains.annotations.Nullable; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -61,11 +63,25 @@ public static String parseResourceId(String resId) { return ""; } - public static String entryToResourceName(String entry) { + public static String entryToResourceName(String entry, @Nullable ResguardMapping resguardMapping) { String resourceName = ""; if (!Util.isNullOrNil(entry)) { - String typeName = parseEntryResourceType(entry); + String typeName; + if (resguardMapping != null) { + int lastIndex = entry.lastIndexOf('/'); + if (lastIndex == -1) return ""; + String obfuscatedPath = entry.substring(0, lastIndex); + String originPath = resguardMapping.originPath(obfuscatedPath); + typeName = parseEntryResourceType(originPath + "/"); + } else { + typeName = parseEntryResourceType(entry); + } + String resName = entry.substring(entry.lastIndexOf('/') + 1, entry.indexOf('.')); + if (resguardMapping != null) { + resName = resguardMapping.originID(typeName, resName); + } + if (!Util.isNullOrNil(typeName) && !Util.isNullOrNil(resName)) { resourceName = "R." + typeName + "." + resName; } @@ -74,8 +90,10 @@ public static String entryToResourceName(String entry) { } public static String parseEntryResourceType(String entry) { - if (!Util.isNullOrNil(entry) && entry.length() > 4) { - String typeName = entry.substring(4, entry.lastIndexOf('/')); + int prefixLength = entry.indexOf('/'); + if (prefixLength == -1) return ""; + if (!Util.isNullOrNil(entry)) { + String typeName = entry.substring(prefixLength + 1, entry.lastIndexOf('/')); if (!Util.isNullOrNil(typeName)) { int index = typeName.indexOf('-'); if (index >= 0) { @@ -87,7 +105,7 @@ public static String parseEntryResourceType(String entry) { return ""; } - public static boolean isSameResourceType(Set entries) { + public static boolean isSameResourceType(Set entries, @Nullable String obfuscatedDirName) { String resType = ""; for (String entry : entries) { if (!Util.isNullOrNil(entry)) { @@ -198,7 +216,7 @@ public static void sevenZipFile(String sevenZipPath, String inputFile, String ou new File(sevenZipPath).setExecutable(true); } ProcessBuilder processBuilder = new ProcessBuilder(); - processBuilder.command(sevenZipPath, "a", "-tzip", outputFile, inputFile, deflated ? "-mx9" : "-mx0"); + processBuilder.command(sevenZipPath, "a", "-tzip", outputFile, inputFile, deflated ? "-mx5" : "-mx0"); //Log.i(TAG, "%s", processBuilder.command()) Process process = processBuilder.start(); // process.waitForProcessOutput(System.out, System.err); diff --git a/matrix/matrix-android/matrix-gradle-plugin/src/main/java/com/tencent/matrix/trace/extension/MatrixTraceExtension.java b/matrix/matrix-android/matrix-gradle-plugin/src/main/java/com/tencent/matrix/trace/extension/MatrixTraceExtension.java index f23e1d1fd..b4467e553 100644 --- a/matrix/matrix-android/matrix-gradle-plugin/src/main/java/com/tencent/matrix/trace/extension/MatrixTraceExtension.java +++ b/matrix/matrix-android/matrix-gradle-plugin/src/main/java/com/tencent/matrix/trace/extension/MatrixTraceExtension.java @@ -11,27 +11,26 @@ public class MatrixTraceExtension { public void setEnable(boolean enable) { this.enable = enable; - onTraceEnabled(enable); } public void setBlackListFile(String blackListFile) { - this.blackListFile = blackListFile + this.blackListFile = blackListFile; } public void setCustomDexTransformName(String customDexTransformName) { - this.customDexTransformName = customDexTransformName + this.customDexTransformName = customDexTransformName; } public void setBaseMethodMapFile(String baseMethodMapFile) { - this.baseMethodMapFile = baseMethodMapFile + this.baseMethodMapFile = baseMethodMapFile; } public void setTransformInjectionForced(boolean transformInjectionForced) { - this.transformInjectionForced = transformInjectionForced + this.transformInjectionForced = transformInjectionForced; } public void setSkipCheckClass(boolean skipCheckClass) { - this.skipCheckClass = skipCheckClass + this.skipCheckClass = skipCheckClass; } public String getBaseMethodMapFile() { diff --git a/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/extension/MatrixRemoveUnusedResExtension.kt b/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/extension/MatrixRemoveUnusedResExtension.kt index 2ad288ee7..4deb5364e 100644 --- a/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/extension/MatrixRemoveUnusedResExtension.kt +++ b/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/extension/MatrixRemoveUnusedResExtension.kt @@ -29,6 +29,8 @@ open class MatrixRemoveUnusedResExtension( var variant: String = "", + var obfuscatedResourcesDirectoryName: String? = null, + // WIP. Should not use these options yet. var use7zip: Boolean = false, var zipAlign: Boolean = false, @@ -36,6 +38,7 @@ open class MatrixRemoveUnusedResExtension( var embedResGuard: Boolean = false, var sevenZipPath: String = "", var zipAlignPath: String = "", + var report: String? = null, // Deprecated var unusedResources: HashSet = HashSet() @@ -55,6 +58,8 @@ open class MatrixRemoveUnusedResExtension( | sevenZipPath = ${sevenZipPath} | zipAlignPath = ${zipAlignPath} | ignoreResources = ${ignoreResources} + | obfuscatedResourcesDirectoryName = ${obfuscatedResourcesDirectoryName} + | """.trimMargin() } } diff --git a/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/task/RemoveUnusedResourcesTaskV2.kt b/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/task/RemoveUnusedResourcesTaskV2.kt index 9285b3ac0..778a052fb 100644 --- a/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/task/RemoveUnusedResourcesTaskV2.kt +++ b/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/task/RemoveUnusedResourcesTaskV2.kt @@ -27,6 +27,7 @@ import com.tencent.matrix.javalib.util.Util import com.tencent.matrix.plugin.compat.AgpCompat import com.tencent.matrix.plugin.compat.CreationConfig import com.tencent.matrix.plugin.extension.MatrixRemoveUnusedResExtension +import com.tencent.matrix.resguard.ResguardMapping import com.tencent.matrix.shrinker.ApkUtil import com.tencent.matrix.shrinker.ProguardStringBuilder import com.tencent.mm.arscutil.ArscUtil @@ -69,6 +70,11 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { private var is7zipEnabled: Boolean = false private var isResGuardEnabled: Boolean = false + private var obfuscatedResourcesDirectoryName: String? = null + private var overrideInputApkFiles: List = emptyList() + private var resguardMappingFile: File? = null + private var resguardMapping: ResguardMapping? = null + private lateinit var pathOfApkChecker: String private lateinit var pathOfApkSigner: String private lateinit var pathOfZipAlign: String @@ -76,20 +82,33 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { private lateinit var rulesOfIgnore: Set + private var reportFile: File? = null + + fun overrideInputApkFiles(inputApkFiles: List) { + this.overrideInputApkFiles = inputApkFiles.map { File(it) } + } + + fun withResguardMapping(path: String) { + resguardMappingFile = File(path) + resguardMapping = ResguardMapping(resguardMappingFile!!) + } + @TaskAction fun removeResources() { - - variant.outputs.forEach { output -> - val startTime = System.currentTimeMillis() - removeResourcesV2( + overrideInputApkFiles + .ifEmpty { + variant.outputs.map { it.outputFile } + } + .forEach { apk -> + val startTime = System.currentTimeMillis() + removeResourcesV2( project = project, - originalApkFile = output.outputFile, + originalApkFile = apk, signingConfig = AgpCompat.getSigningConfig(variant), nameOfSymbolDirectory = AgpCompat.getIntermediatesSymbolDirName() - ) - Log.i(TAG, "cost time %f s", (System.currentTimeMillis() - startTime) / 1000.0f) - - } + ) + Log.i(TAG, "cost time %f s", (System.currentTimeMillis() - startTime) / 1000.0f) + } } class CreationAction( @@ -113,12 +132,23 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { task.is7zipEnabled = use7zip task.isResGuardEnabled = embedResGuard + task.obfuscatedResourcesDirectoryName = obfuscatedResourcesDirectoryName?.let { + if (it.endsWith("/")) it else "${it}/" + } + task.pathOfApkChecker = apkCheckerPath task.pathOfApkSigner = apksignerPath task.pathOfZipAlign = zipAlignPath task.pathOfSevenZip = sevenZipPath task.rulesOfIgnore = ignoreResources + + task.reportFile = report?.let { + File(it).apply { + parentFile.mkdirs() + if (exists()) delete() + } + } } task.parametersInvalidation() @@ -190,10 +220,10 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { val replaceIterator = mapOfDuplicatesReplacements.keys.iterator() while (replaceIterator.hasNext()) { val sourceFile = replaceIterator.next() - val sourceRes = ApkUtil.entryToResourceName(sourceFile) + val sourceRes = ApkUtil.entryToResourceName(sourceFile, resguardMapping) val sourceId = mapOfResources[sourceRes]!! val targetFile = mapOfDuplicatesReplacements[sourceFile] - val targetRes = ApkUtil.entryToResourceName(targetFile) + val targetRes = ApkUtil.entryToResourceName(targetFile, resguardMapping) val targetId = mapOfResources[targetRes]!! val success = ArscUtil.replaceFileResource(resTable, sourceId, sourceFile, targetId, targetFile) if (!success) { @@ -225,17 +255,18 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { val compressedEntry = HashSet() for (zipEntry in zipInputFile.entries()) { + if (zipEntry.isDirectory) continue var destFile = unzipDir.canonicalPath + File.separator + zipEntry.name.replace('/', File.separatorChar) - if (zipEntry.name.startsWith("res/")) { - val resourceName = ApkUtil.entryToResourceName(zipEntry.name) + if (zipEntry.name.startsWith(obfuscatedResourcesDirectoryName ?: "res/")) { + val resourceName = ApkUtil.entryToResourceName(zipEntry.name, resguardMapping) if (!Util.isNullOrNil(resourceName)) { if (mapOfResourcesGonnaRemoved.containsKey(resourceName)) { - Log.i(TAG, "remove unused resource %s file %s", resourceName, zipEntry.name) + Log.d(TAG, "remove unused resource %s file %s", resourceName, zipEntry.name) continue } else if (mapOfDuplicatesReplacements.containsKey(zipEntry.name)) { - Log.i(TAG, "remove duplicated resource file %s", zipEntry.name) + Log.d(TAG, "remove duplicated resource file %s", zipEntry.name) continue } else { if (arsc != null && isResGuardEnabled) { @@ -247,7 +278,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { val proguardDir = dirProguard.generateNextProguardFileName() resultOfObfuscatedDirs[dir] = "$RES_DIR_PROGUARD_NAME/$proguardDir" dirFileProguard[dir] = ProguardStringBuilder() - Log.i(TAG, "dir %s, proguard builder", dir) + Log.d(TAG, "dir %s, proguard builder", dir) } resultOfObfuscatedFiles[zipEntry.name] = resultOfObfuscatedDirs[dir] + "/" + dirFileProguard[dir]!!.generateNextProguardFileName() + suffix val success = ArscUtil.replaceResFileName(resTable, mapOfResources[resourceName]!!, zipEntry.name, resultOfObfuscatedFiles[zipEntry.name]) @@ -294,7 +325,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { ApkUtil.sevenZipFile(pathOfSevenZip, unzipDir.canonicalPath + "${File.separator}*", toShrunkApkFile.canonicalPath, false) if (compressedEntry.isNotEmpty()) { - Log.i(TAG, "7zip %d DEFLATED files to apk", compressedEntry.size) + Log.d(TAG, "7zip %d DEFLATED files to apk", compressedEntry.size) val deflateDir = File(fromOriginalApkFile.parentFile, fromOriginalApkFile.name.substring(0, fromOriginalApkFile.name.lastIndexOf(".")) + "_deflated") FileUtils.deleteRecursivelyIfExists(deflateDir) deflateDir.mkdir() @@ -336,7 +367,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { } else { val zipEntry = ZipEntry(file.canonicalPath.substring(rootDir.length + 1)) val method = if (compressedEntry.contains(file.canonicalPath)) ZipEntry.DEFLATED else ZipEntry.STORED - Log.i(TAG, "zip file %s -> entry %s, DEFLATED %s", file.canonicalPath, zipEntry.name, method == ZipEntry.DEFLATED) + Log.d(TAG, "zip file %s -> entry %s, DEFLATED %s", file.canonicalPath, zipEntry.name, method == ZipEntry.DEFLATED) zipEntry.method = method ApkUtil.addZipEntry(zipOutputStream, zipEntry, file) } @@ -352,6 +383,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { pathOfOriginalApk: String, pathOfMapping: String, pathOfRTxt: String, + resguardMappingFile: File?, resultOfUnused: MutableSet, resultOfDuplicates: MutableMap> ) { @@ -374,6 +406,11 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { parameters.add(pathOfMapping) } + if (resguardMappingFile?.exists() == true) { + parameters.add("--resMappingTxt") + parameters.add(resguardMappingFile.absolutePath) + } + parameters.add("--format") parameters.add("json") parameters.add("-unusedResources") @@ -483,7 +520,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { resultOfResourcesGonnaRemoved[resName] = resId } } else { - Log.i(TAG, "ignore remove unused resources %s", resName) + Log.d(TAG, "ignore remove unused resources %s", resName) } } @@ -495,17 +532,17 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { val duplicatesNames = HashMap() if (duplicatesEntries != null) { for (entry in duplicatesEntries) { - if (!entry.startsWith("res/")) { + if (!entry.startsWith(obfuscatedResourcesDirectoryName ?: "res/")) { Log.w(TAG, " %s is not resource file!", entry) continue } else { - duplicatesNames[entry] = ApkUtil.entryToResourceName(entry) + duplicatesNames[entry] = ApkUtil.entryToResourceName(entry, resguardMapping) } } } if (duplicatesNames.size > 0) { - if (!ApkUtil.isSameResourceType(duplicatesNames.keys)) { + if (!ApkUtil.isSameResourceType(duplicatesNames.keys, obfuscatedResourcesDirectoryName)) { Log.w(TAG, "the type of duplicated resources %s are not same!", duplicatesEntries) continue } else { @@ -523,7 +560,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { val replace = it.next() while (it.hasNext()) { val dup = it.next() - Log.i(TAG, "replace %s with %s", dup, replace) + Log.d(TAG, "replace %s with %s", dup, replace) resultOfDuplicatesReplacements[dup] = replace } } @@ -551,7 +588,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { if (!checkIfIgnored(resName, regexpRules)) { resultOfObfuscatedNames[resName] = "R." + resType + "." + resTypeBuilder.generateNextProguard() } else { - Log.i(TAG, "ignore proguard resource name %s", resName) + Log.d(TAG, "ignore proguard resource name %s", resName) } } } @@ -581,7 +618,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { } } if (attrs.isNotEmpty() && j == attrs.size) { - Log.i(TAG, "removed styleable $styleable") + Log.d(TAG, "removed styleable $styleable") styleableIterator.remove() } } @@ -645,9 +682,9 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { var startTime = System.currentTimeMillis() val rTxtFile = File(project.buildDir, "intermediates") - .resolve(nameOfSymbolDirectory) - .resolve(variant.name) - .resolve("R.txt") + .resolve(nameOfSymbolDirectory) + .resolve(variant.name) + .resolve("R.txt") val mappingTxtFile = File(project.buildDir, "outputs") .resolve("mapping") @@ -666,6 +703,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { pathOfOriginalApk = originalApkPath, pathOfMapping = mappingTxtFile.absolutePath, pathOfRTxt = rTxtFile.absolutePath, + resguardMappingFile = resguardMappingFile, // result resultOfUnused = setOfUnusedResources, @@ -675,7 +713,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { Log.i(TAG, "find out %d unused resources:\n %s", setOfUnusedResources.size, setOfUnusedResources) Log.i(TAG, "find out %d duplicated resources:", mapOfDuplicatedResources.size) for (md5 in mapOfDuplicatedResources.keys) { - Log.i(TAG, "> md5:%s, files:%s", md5, mapOfDuplicatedResources[md5]) + Log.d(TAG, "> md5:%s, files:%s", md5, mapOfDuplicatedResources[md5]) } Log.i(TAG, "find unused resources cost time %fs ", (System.currentTimeMillis() - startTime) / 1000.0f) @@ -734,7 +772,7 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { // Zip align if (isZipAlignEnabled) { val alignedApk = originalApkFile.parentFile.canonicalPath + File.separator + originalApkFile.name.substring(0, originalApkFile.name.indexOf(".")) + "_aligned.apk" - Log.i(TAG, "Zipalign apk...") + Log.d(TAG, "Zipalign apk...") ApkUtil.zipAlignApk(shrunkApkPath, alignedApk, pathOfZipAlign) shrunkApkFile.delete() FileUtils.copyFile(File(alignedApk), shrunkApkFile) @@ -743,10 +781,27 @@ abstract class RemoveUnusedResourcesTaskV2 : DefaultTask() { // Signing apk if (isSigningEnabled) { - Log.i(TAG, "Signing apk...") + Log.d(TAG, "Signing apk...") ApkUtil.signApk(shrunkApkPath, pathOfApkSigner, signingConfig) } + reportFile?.bufferedWriter()?.use { writer -> + writer.write("removed resources:") + writer.newLine() + mapOfResourcesGonnaRemoved.keys.forEach { name -> + writer.write(" $name") + writer.newLine() + } + writer.newLine() + + writer.write("duplicated resources:") + writer.newLine() + mapOfDuplicatesReplacements.forEach { (origin, replacement) -> + writer.write(" $origin -> $replacement") + writer.newLine() + } + } + // Backup original apk and swap shrunk apk FileUtils.copyFile(originalApkFile, File(fileOfBackup, "backup.apk")) originalApkFile.delete() diff --git a/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/transform/MatrixTraceTransform.kt b/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/transform/MatrixTraceTransform.kt index c024bc988..43cf08862 100644 --- a/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/transform/MatrixTraceTransform.kt +++ b/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/plugin/transform/MatrixTraceTransform.kt @@ -151,7 +151,7 @@ class MatrixTraceTransform( } private fun toOutputFile(outputDir: File, inputDir: File, inputFile: File): File { - return File(outputDir, FileUtils.relativePossiblyNonExistingPath(inputFile, inputDir)) + return File(outputDir, inputFile.toRelativeString(inputDir)) } private fun configure(transformInvocation: TransformInvocation): Configuration { diff --git a/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/resguard/ResguardMapping.kt b/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/resguard/ResguardMapping.kt new file mode 100644 index 000000000..b320bb3df --- /dev/null +++ b/matrix/matrix-android/matrix-gradle-plugin/src/main/kotlin/com/tencent/matrix/resguard/ResguardMapping.kt @@ -0,0 +1,59 @@ +package com.tencent.matrix.resguard + +import java.io.File + +internal class ResguardMapping(mappingFile: File) { + + private enum class ParseState { + None, Path, ID; + } + + private val pathMapping: Map + + private val idMapping: Map> + + companion object { + private val parsePathRegex = "^\\s+(.*) -> (.*)\$".toRegex() + private val parseIDRegex = "^\\s+(\\S+)?R\\.(\\S+?)\\.(\\S+) -> (\\S+)?R\\.(\\S+?)\\.(\\S+)\$".toRegex() + } + + init { + var state = ParseState.None + val pathMappingRaw = mutableMapOf() + val idMappingRaw = mutableMapOf>() + mappingFile.forEachLine { line -> + when { + line == "res path mapping:" -> { + state = ParseState.Path + } + line == "res id mapping:" -> { + state = ParseState.ID + } + line.isEmpty() -> { + state = ParseState.None + } + else -> { + if (state == ParseState.Path) { + parsePathRegex.matchEntire(line)?.groupValues?.let { + pathMappingRaw[it[2]] = it[1] + } + } else if (state == ParseState.ID) { + parseIDRegex.matchEntire(line)?.groupValues?.let { + idMappingRaw.getOrPut(it[2]) { mutableMapOf() }[it[6]] = it[3] + } + } + } + } + } + pathMapping = pathMappingRaw + idMapping = idMappingRaw + } + + fun originPath(path: String): String { + return pathMapping[path] ?: path + } + + fun originID(type: String, name: String): String { + return idMapping[type]?.get(name) ?: name + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/CMakeLists.txt b/matrix/matrix-android/matrix-hooks/CMakeLists.txt index 0d4509572..293ab5e25 100644 --- a/matrix/matrix-android/matrix-hooks/CMakeLists.txt +++ b/matrix/matrix-android/matrix-hooks/CMakeLists.txt @@ -47,9 +47,10 @@ target_link_libraries( PUBLIC -Wl,--gc-sections PUBLIC cJSON PUBLIC ${log-lib} + PRIVATE -Wl,--whole-archive fastunwind -Wl,--no-whole-archive PUBLIC ${EXT_DEP}/lib/${ANDROID_ABI}/libwechatbacktrace.so - PRIVATE -Wl,--whole-archive ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a -Wl,--no-whole-archive PRIVATE -Wl,--whole-archive ${EXT_DEP}/lib/${ANDROID_ABI}/libsemi_dlfcn.a -Wl,--no-whole-archive + PRIVATE -Wl,--whole-archive ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a -Wl,--no-whole-archive PRIVATE -Wl,--version-script=${SOURCE_DIR}/common/common.ver ) ################################################################################# @@ -96,91 +97,19 @@ target_link_libraries( ) ################################################################################# -################################### MemGuard #################################### -set(TARGET memguard_base) +################################# ART Misc ################################## +set(TARGET matrix-artmisc) add_library( - ${TARGET} - STATIC - ${SOURCE_DIR}/memguard/port/Hook.cpp - ${SOURCE_DIR}/memguard/port/Log.cpp - ${SOURCE_DIR}/memguard/port/Memory.cpp - ${SOURCE_DIR}/memguard/port/Mutex.cpp - ${SOURCE_DIR}/memguard/port/Paths.cpp - ${SOURCE_DIR}/memguard/port/Random.cpp - ${SOURCE_DIR}/memguard/port/Unwind.cpp - ${SOURCE_DIR}/memguard/port/FdSanWrapper.cpp - ${SOURCE_DIR}/memguard/util/SignalHandler.cpp - ${SOURCE_DIR}/memguard/util/Interception.cpp - ${SOURCE_DIR}/memguard/util/PagePool.cpp - ${SOURCE_DIR}/memguard/util/Allocation.cpp - ${SOURCE_DIR}/memguard/util/Thread.cpp - ${SOURCE_DIR}/memguard/util/Issue.cpp - ${SOURCE_DIR}/memguard/MemGuard.cpp -) - -target_include_directories( - ${TARGET} - PRIVATE ${SOURCE_DIR}/memguard - PUBLIC ${SOURCE_DIR} - PUBLIC ${EXT_DEP}/include - PUBLIC ${EXT_DEP}/include/backtrace - PUBLIC ${EXT_DEP}/include/backtrace/common -) - -target_compile_options( - ${TARGET} - PRIVATE -Wall -Wextra -Werror -Wno-unused-function - PRIVATE $<$:-std=c17> - PRIVATE $<$:-std=c++17> - PUBLIC -fvisibility=hidden -fno-exceptions -fno-rtti -fdata-sections -ffunction-sections - PUBLIC -DMEMGUARD_LOG_LEVEL=4 -) - -target_link_libraries( - ${TARGET} - PUBLIC -Wl,--gc-sections - PRIVATE ${log-lib} - PUBLIC matrix-hookcommon - PUBLIC wechatbacktrace - PUBLIC ${EXT_DEP}/lib/${ANDROID_ABI}/libunwindstack.a - PUBLIC fastunwind -) - - -set(TARGET matrix-memguard) - -add_library( - ${TARGET} - SHARED - ${SOURCE_DIR}/memguard/jni/com_tencent_mm_tools_memguard_MemGuard.cpp - ${SOURCE_DIR}/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.cpp - ${SOURCE_DIR}/memguard/jni/JNIAux.cpp - ${SOURCE_DIR}/memguard/jni/C2Java.cpp -) - -target_include_directories( - ${TARGET} - PRIVATE ${SOURCE_DIR}/memguard - PRIVATE ${EXT_DEP}/include - PRIVATE ${EXT_DEP}/include/backtrace - PRIVATE ${EXT_DEP}/include/backtrace/common -) - -target_compile_options( - ${TARGET} - PRIVATE -Wall -Wextra -Werror -Wno-unused-function - PRIVATE -fvisibility=hidden -fno-exceptions -fno-rtti -fdata-sections -ffunction-sections - PRIVATE $<$:-std=c17> - PRIVATE $<$:-std=c++17> + ${TARGET} + SHARED + ${SOURCE_DIR}/art/RuntimeVerifyMuteJNI.cpp + ${SOURCE_DIR}/art/RuntimeVerifyMute.cpp ) target_link_libraries( - ${TARGET} - PRIVATE -Wl,--gc-sections - PRIVATE -Wl,--version-script=${SOURCE_DIR}/memguard/memguard.map - PRIVATE ${log-lib} - PRIVATE matrix-hookcommon - PRIVATE memguard_base + ${TARGET} + PRIVATE matrix-hookcommon + PRIVATE -Wl,--version-script=${SOURCE_DIR}/art/art_misc.ver ) ################################################################################# \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/build.gradle b/matrix/matrix-android/matrix-hooks/build.gradle index 567994ed3..5749ccb96 100644 --- a/matrix/matrix-android/matrix-hooks/build.gradle +++ b/matrix/matrix-android/matrix-hooks/build.gradle @@ -27,6 +27,10 @@ android { include '**/*.h' include '*.h' } + + from('src/main/cpp/external/fastunwind') { + include '**/*.h' + } } } } diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMute.cpp b/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMute.cpp new file mode 100644 index 000000000..748f4e524 --- /dev/null +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMute.cpp @@ -0,0 +1,69 @@ +// +// Created by tomystang on 2022/11/17. +// + +#include +#include +#include +#include +#include +#include +#include +#include "RuntimeVerifyMute.h" + +#define LOG_TAG "Matrix.RuntimeVerifyMute" + +static bool sInitialized = false; +static std::mutex sInitLock; + +bool matrix::art_misc::Install(JNIEnv* env) { + if (sInitialized) { + LOGI(LOG_TAG, "[!] Already installed."); + return true; + } + std::lock_guard lock(sInitLock); + if (sInitialized) { + LOGI(LOG_TAG, "[!] Already installed."); + return true; + } + + int sdk_ver = android_get_device_api_level(); + if (sdk_ver == -1) { + LOGE(LOG_TAG, "[-] Fail to get sdk version."); + return false; + } + if (sdk_ver < __ANDROID_API_P__) { + LOGE(LOG_TAG, "[-] SDK version is lower than P."); + return false; + } + + void* h_libart = semi_dlopen("libart.so"); + if (h_libart == nullptr) { + LOGE(LOG_TAG, "[-] Fail to open libart.so."); + return false; + } + auto h_libart_cleaner = MakeScopedCleaner([&h_libart]() { + if (h_libart != nullptr) { + semi_dlclose(h_libart); + } + }); + + void** art_runtime_instance_ptr = + reinterpret_cast(semi_dlsym(h_libart, "_ZN3art7Runtime9instance_E")); + if (art_runtime_instance_ptr == nullptr) { + LOGE(LOG_TAG, "[-] Fail to find Runtime::instance_."); + return false; + } + void (*art_runtime_disable_verifier_fn)(void*) = + reinterpret_cast(semi_dlsym(h_libart, "_ZN3art7Runtime15DisableVerifierEv")); + if (art_runtime_disable_verifier_fn == nullptr) { + LOGE(LOG_TAG, "[-] Fail to find Runtime::DisableVerifier()."); + return false; + } + + art_runtime_disable_verifier_fn(*art_runtime_instance_ptr); + + LOGI(LOG_TAG, "[+] Runtime::DisableVerifier() was invoked."); + + return true; +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMute.h b/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMute.h new file mode 100644 index 000000000..60c62426c --- /dev/null +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMute.h @@ -0,0 +1,16 @@ +// +// Created by tomystang on 2022/11/17. +// + +#ifndef MATRIX_ANDROID_RUNTIMEVERIFYMUTE_H +#define MATRIX_ANDROID_RUNTIMEVERIFYMUTE_H + + +namespace matrix { + namespace art_misc { + bool Install(JNIEnv* env); + } +} + + +#endif //MATRIX_ANDROID_RUNTIMEVERIFYMUTE_H diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMuteJNI.cpp b/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMuteJNI.cpp new file mode 100644 index 000000000..0111fb7c8 --- /dev/null +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/art/RuntimeVerifyMuteJNI.cpp @@ -0,0 +1,11 @@ +// +// Created by tomystang on 2022/11/17. +// + +#include +#include "RuntimeVerifyMute.h" + +extern "C" jboolean JNIEXPORT +Java_com_tencent_matrix_hook_art_RuntimeVerifyMute_nativeInstall(JNIEnv* env, jobject) { + return matrix::art_misc::Install(env) ? JNI_TRUE : JNI_FALSE; +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/art/art_misc.ver b/matrix/matrix-android/matrix-hooks/src/main/cpp/art/art_misc.ver new file mode 100644 index 000000000..9490940fd --- /dev/null +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/art/art_misc.ver @@ -0,0 +1,6 @@ +{ + global: + JNI_OnLoad; + JNI_OnUnload; + Java_*; +}; \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/common/HookCommon.h b/matrix/matrix-android/matrix-hooks/src/main/cpp/common/HookCommon.h index ef5e78e09..885014dec 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/common/HookCommon.h +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/common/HookCommon.h @@ -27,12 +27,15 @@ #include #include "JNICommon.h" #include "Macros.h" +#include "EnhanceDlsym.h" // 0x01 was occupied by thread priority trace hook in MatrixTracer.cc. #define HOOK_REQUEST_GROUPID_DLOPEN_MON 0x02 #define HOOK_REQUEST_GROUPID_MEMORY 0x03 #define HOOK_REQUEST_GROUPID_PTHREAD 0x04 #define HOOK_REQUEST_GROUPID_MEMGUARD 0x05 +#define HOOK_REQUEST_GROUPID_MEMGUARD_2 0x06 +#define HOOK_REQUEST_GROUPID_EGL_HOOK 0x07 #define GET_CALLER_ADDR(__caller_addr) \ void * __caller_addr = __builtin_return_address(0) @@ -48,34 +51,30 @@ extern ORIGINAL_FUNC_PTR(sym); \ ret HANDLER_FUNC_NAME(sym)(params); +#define DECLARE_HOOK_ORIG_ATTR(ret, sym, params...) \ + typedef ret (*FUNC_TYPE(sym))(params); \ + extern ORIGINAL_FUNC_PTR(sym); \ + ret HANDLER_FUNC_NAME(sym)(params) + #define DEFINE_HOOK_FUN(ret, sym, params...) \ ORIGINAL_FUNC_PTR(sym); \ ret HANDLER_FUNC_NAME(sym)(params) -#define FETCH_ORIGIN_FUNC(sym) \ - if (!ORIGINAL_FUNC_NAME(sym)) { \ - void *handle = dlopen(ORIGINAL_LIB, RTLD_LAZY); \ +#define FETCH_ORIGIN_FUNC_OF_SO(sym, target_so) \ + if (!ORIGINAL_FUNC_NAME(sym)) { \ + void *handle = dlopen(target_so, RTLD_LAZY); \ if (handle) { \ ORIGINAL_FUNC_NAME(sym) = (FUNC_TYPE(sym))dlsym(handle, #sym); \ } \ - } + }\ -#define CALL_ORIGIN_FUNC_RET(retType, ret, sym, params...) \ - if (!ORIGINAL_FUNC_NAME(sym)) { \ - void *handle = dlopen(ORIGINAL_LIB, RTLD_LAZY); \ - if (handle) { \ - ORIGINAL_FUNC_NAME(sym) = (FUNC_TYPE(sym))dlsym(handle, #sym); \ - } \ - } \ +#define FETCH_ORIGIN_FUNC(sym) \ + FETCH_ORIGIN_FUNC_OF_SO(sym, ORIGINAL_LIB) + +#define CALL_ORIGIN_FUNC_RET(handle, retType, ret, sym, params...) \ retType ret = ORIGINAL_FUNC_NAME(sym)(params) -#define CALL_ORIGIN_FUNC_VOID(sym, params...) \ - if (!ORIGINAL_FUNC_NAME(sym)) { \ - void *handle = dlopen(ORIGINAL_LIB, RTLD_LAZY); \ - if (handle) { \ - ORIGINAL_FUNC_NAME(sym) = (FUNC_TYPE(sym))dlsym(handle, #sym); \ - } \ - } \ +#define CALL_ORIGIN_FUNC_VOID(handle, sym, params...) \ ORIGINAL_FUNC_NAME(sym)(params) #define NOTIFY_COMMON_IGNORE_LIBS(group_id) \ @@ -99,20 +98,13 @@ xhook_grouped_ignore(group_id, ".*/libmatrix-opengl-leak\\.so$", NULL); \ xhook_grouped_ignore(group_id, ".*/libmatrix-memguard\\.so$", NULL);\ xhook_grouped_ignore(group_id, ".*/libTcpOptimizer\\.mobiledata\\.samsung\\.so$", NULL); \ + xhook_grouped_ignore(group_id, ".*/libmatrix-traffic\\.so$", NULL);\ } while (0) -#include - #ifdef __cplusplus extern "C" { #endif -typedef struct { - const char *name; - void *handler_ptr; - void **origin_ptr; -} HookFunction; - EXPORT bool get_java_stacktrace(char *stack_dst, size_t size); #ifdef __cplusplus diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/common/SoLoadMonitor.cpp b/matrix/matrix-android/matrix-hooks/src/main/cpp/common/SoLoadMonitor.cpp index 7762f4c06..2672616c2 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/common/SoLoadMonitor.cpp +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/common/SoLoadMonitor.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +76,7 @@ namespace matrix { std::lock_guard lock(sDlOpenHandlerMutex); void* hLib = dlopen_fn(args); if (hLib != nullptr && (args->flags & RTLD_NOLOAD) == 0) { + xh_maps_invalidate(); NotifySoLoad(args->filename); } return hLib; @@ -146,8 +148,8 @@ namespace matrix { } #ifdef __LP64__ -#define LINKER_NAME "linker64" -#define LINKER_NAME_PATTERN ".*/linker64$" + #define LINKER_NAME "linker64" + #define LINKER_NAME_PATTERN ".*/linker64$" #else #define LINKER_NAME "linker" #define LINKER_NAME_PATTERN ".*/linker$" @@ -161,6 +163,7 @@ namespace matrix { int sdkVer = android_get_device_api_level(); + // fixme: remove if (sdkVer == 24 || sdkVer == 25) { LOGE(LOG_TAG, "Does not support N and N_MR1 so far."); return false; @@ -186,10 +189,14 @@ namespace matrix { if (UNLIKELY(orig_dlopen_N == nullptr)) { orig_dlopen_N = reinterpret_cast( semi_dlsym(hLinker, "__dl__Z8__dlopenPKciPKv")); - if (UNLIKELY(orig_dlopen_N == nullptr)) { - LOGE(LOG_TAG, "Fail to find original dlopen."); - return false; - } + } + if (UNLIKELY(orig_dlopen_N == nullptr)) { + orig_dlopen_N = reinterpret_cast( + semi_dlsym(hLinker, "__loader_dlopen")); + } + if (UNLIKELY(orig_dlopen_N == nullptr)) { + LOGE(LOG_TAG, "Fail to find original dlopen."); + return false; } if (UNLIKELY(xhook_grouped_register(HOOK_REQUEST_GROUPID_DLOPEN_MON, ".*\\.so$", "dlopen", reinterpret_cast(dlopen_handler_N), nullptr) != 0)) { @@ -202,10 +209,14 @@ namespace matrix { if (UNLIKELY(orig_android_dlopen_ext_N == nullptr)) { orig_android_dlopen_ext_N = reinterpret_cast( semi_dlsym(hLinker, "__dl__Z20__android_dlopen_extPKciPK17android_dlextinfoPKv")); - if (UNLIKELY(orig_android_dlopen_ext_N == nullptr)) { - LOGE(LOG_TAG, "Fail to find original android_dlopen_ext."); - return false; - } + } + if (UNLIKELY(orig_android_dlopen_ext_N == nullptr)) { + orig_android_dlopen_ext_N = reinterpret_cast( + semi_dlsym(hLinker, "__loader_android_dlopen_ext")); + } + if (UNLIKELY(orig_android_dlopen_ext_N == nullptr)) { + LOGE(LOG_TAG, "Fail to find original android_dlopen_ext."); + return false; } if (UNLIKELY(xhook_grouped_register(HOOK_REQUEST_GROUPID_DLOPEN_MON, ".*\\.so$", "android_dlopen_ext", reinterpret_cast(android_dlopen_ext_handler_N), nullptr) != 0)) { @@ -216,10 +227,13 @@ namespace matrix { orig_dlclose = reinterpret_cast(semi_dlsym(hLinker, "__dl___loader_dlclose")); if (UNLIKELY(orig_dlclose == nullptr)) { orig_dlclose = reinterpret_cast(semi_dlsym(hLinker, "__dl__Z9__dlclosePv")); - if (UNLIKELY(orig_dlclose == nullptr)) { - LOGE(LOG_TAG, "Fail to find original dlclose."); - return false; - } + } + if (UNLIKELY(orig_dlclose == nullptr)) { + orig_dlclose = reinterpret_cast(semi_dlsym(hLinker, "__loader_dlclose")); + } + if (UNLIKELY(orig_dlclose == nullptr)) { + LOGE(LOG_TAG, "Fail to find original dlclose."); + return false; } if (UNLIKELY(xhook_grouped_register(HOOK_REQUEST_GROUPID_DLOPEN_MON, ".*\\.so$", "dlclose", reinterpret_cast(dlclose_handler), nullptr) != 0)) { diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/common/common.ver b/matrix/matrix-android/matrix-hooks/src/main/cpp/common/common.ver index 55f6226fa..2165247bc 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/common/common.ver +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/common/common.ver @@ -1,7 +1,8 @@ { global: set_fake_backtrace_vectors*; - local: xh_*; semi_*; + flogger0; + rp_*; }; \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/external/fastunwind/FastUnwind.h b/matrix/matrix-android/matrix-hooks/src/main/cpp/external/fastunwind/FastUnwind.h index 463afa87f..70cdf1b04 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/external/fastunwind/FastUnwind.h +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/external/fastunwind/FastUnwind.h @@ -8,8 +8,15 @@ #include namespace fastunwind { - extern __attribute__((no_sanitize("address", "hwaddress"))) int Unwind(void** pcs, size_t max_count); - extern __attribute__((no_sanitize("address", "hwaddress"))) int Unwind(void* ucontext, void** pcs, size_t max_count); + extern + __attribute__((no_sanitize("address", "hwaddress"))) + __attribute__ ((visibility ("default"))) + int Unwind(void** pcs, size_t max_count); + + extern + __attribute__((no_sanitize("address", "hwaddress"))) + __attribute__ ((visibility ("default"))) + int Unwind(void* ucontext, void** pcs, size_t max_count); } diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookFunctions.cpp b/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookFunctions.cpp index 6755b58e3..ae025fb7e 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookFunctions.cpp +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookFunctions.cpp @@ -26,6 +26,7 @@ #include "MemoryHook.h" #define ORIGINAL_LIB "libc.so" +static void *libc_handle = nullptr; #define DO_HOOK_ACQUIRE(p, size) \ GET_CALLER_ADDR(caller); \ @@ -35,21 +36,21 @@ on_free_memory(p) DEFINE_HOOK_FUN(void *, malloc, size_t __byte_count) { - CALL_ORIGIN_FUNC_RET(void*, p, malloc, __byte_count); + CALL_ORIGIN_FUNC_RET(libc_handle, void*, p, malloc, __byte_count); LOGI(TAG, "+ malloc %p", p); DO_HOOK_ACQUIRE(p, __byte_count); return p; } DEFINE_HOOK_FUN(void *, calloc, size_t __item_count, size_t __item_size) { - CALL_ORIGIN_FUNC_RET(void *, p, calloc, __item_count, __item_size); + CALL_ORIGIN_FUNC_RET(libc_handle, void *, p, calloc, __item_count, __item_size); LOGI(TAG, "+ calloc %p", p); DO_HOOK_ACQUIRE(p, __item_count * __item_size); return p; } DEFINE_HOOK_FUN(void *, realloc, void *__ptr, size_t __byte_count) { - CALL_ORIGIN_FUNC_RET(void *, p, realloc, __ptr, __byte_count); + CALL_ORIGIN_FUNC_RET(libc_handle, void *, p, realloc, __ptr, __byte_count); GET_CALLER_ADDR(caller); @@ -76,14 +77,14 @@ DEFINE_HOOK_FUN(void *, realloc, void *__ptr, size_t __byte_count) { } DEFINE_HOOK_FUN(void *, memalign, size_t __alignment, size_t __byte_count) { - CALL_ORIGIN_FUNC_RET(void *, p, memalign, __alignment, __byte_count); + CALL_ORIGIN_FUNC_RET(libc_handle, void *, p, memalign, __alignment, __byte_count); LOGI(TAG, "+ memalign %p", p); DO_HOOK_ACQUIRE(p, __byte_count); return p; } DEFINE_HOOK_FUN(int, posix_memalign, void** __memptr, size_t __alignment, size_t __size) { - CALL_ORIGIN_FUNC_RET(int, ret, posix_memalign, __memptr, __alignment, __size); + CALL_ORIGIN_FUNC_RET(libc_handle, int, ret, posix_memalign, __memptr, __alignment, __size); if (ret == 0) { LOGI(TAG, "+ posix_memalign %p", *__memptr); DO_HOOK_ACQUIRE(*__memptr, __size); @@ -94,7 +95,7 @@ DEFINE_HOOK_FUN(int, posix_memalign, void** __memptr, size_t __alignment, size_t DEFINE_HOOK_FUN(void, free, void *__ptr) { LOGI(TAG, "- free %p", __ptr); DO_HOOK_RELEASE(__ptr); - CALL_ORIGIN_FUNC_VOID(free, __ptr); + CALL_ORIGIN_FUNC_VOID(libc_handle, free, __ptr); } #if defined(__USE_FILE_OFFSET64) @@ -108,7 +109,7 @@ void*h_mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_ DEFINE_HOOK_FUN(void *, mmap, void *__addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset) { - CALL_ORIGIN_FUNC_RET(void *, p, mmap, __addr, __size, __prot, __flags, __fd, __offset); + CALL_ORIGIN_FUNC_RET(libc_handle, void *, p, mmap, __addr, __size, __prot, __flags, __fd, __offset); if (p == MAP_FAILED) { return p;// just return } @@ -122,9 +123,8 @@ DEFINE_HOOK_FUN(void *, mmap, void *__addr, size_t __size, int __prot, int __fla #endif #if __ANDROID_API__ >= __ANDROID_API_L__ - -void *h_mmap64(void *__addr, size_t __size, int __prot, int __flags, int __fd, - off64_t __offset) __INTRODUCED_IN(21) { +DEFINE_HOOK_FUN(void *, mmap64, void *__addr, size_t __size, int __prot, int __flags, int __fd, + off64_t __offset) { void *p = mmap64(__addr, __size, __prot, __flags, __fd, __offset); if (p == MAP_FAILED) { return p;// just return @@ -134,7 +134,6 @@ void *h_mmap64(void *__addr, size_t __size, int __prot, int __flags, int __fd, on_mmap_memory(caller, p, __size); return p; } - #endif DEFINE_HOOK_FUN(void *, mremap, void *__old_addr, size_t __old_size, size_t __new_size, int __flags, @@ -167,14 +166,14 @@ DEFINE_HOOK_FUN(int, munmap, void *__addr, size_t __size) { } DEFINE_HOOK_FUN(char*, strdup, const char *str) { - CALL_ORIGIN_FUNC_RET(char *, p, strdup, str); + CALL_ORIGIN_FUNC_RET(libc_handle, char *, p, strdup, str); LOGI(TAG, "+ strdup %p", (void *)p); DO_HOOK_ACQUIRE(p, sizeof(str)); return p; } DEFINE_HOOK_FUN(char*, strndup, const char *str, size_t n) { - CALL_ORIGIN_FUNC_RET(char *, p, strndup, str, n); + CALL_ORIGIN_FUNC_RET(libc_handle, char *, p, strndup, str, n); LOGI(TAG, "+ strndup %p", (void *)p); DO_HOOK_ACQUIRE(p, sizeof(str) < n ? sizeof(str) : n); return p; @@ -182,18 +181,19 @@ DEFINE_HOOK_FUN(char*, strndup, const char *str, size_t n) { #undef ORIGINAL_LIB #define ORIGINAL_LIB "libc++_shared.so" +static void *libcxx_handle = nullptr; #ifndef __LP64__ DEFINE_HOOK_FUN(void*, _Znwj, size_t size) { - CALL_ORIGIN_FUNC_RET(void*, p, _Znwj, size); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _Znwj, size); LOGI(TAG, "+ _Znwj %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _ZnwjSt11align_val_t, size_t size, std::align_val_t align_val) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnwjSt11align_val_t, size, align_val); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnwjSt11align_val_t, size, align_val); LOGI(TAG, "- _ZnwjSt11align_val_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; @@ -201,28 +201,28 @@ DEFINE_HOOK_FUN(void*, _ZnwjSt11align_val_t, size_t size, std::align_val_t align DEFINE_HOOK_FUN(void*, _ZnwjSt11align_val_tRKSt9nothrow_t, size_t size, std::align_val_t align_val, std::nothrow_t const& nothrow) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnwjSt11align_val_tRKSt9nothrow_t, size, align_val, nothrow); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnwjSt11align_val_tRKSt9nothrow_t, size, align_val, nothrow); LOGI(TAG, "+ _ZnwjSt11align_val_tRKSt9nothrow_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _ZnwjRKSt9nothrow_t, size_t size, std::nothrow_t const& nothrow) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnwjRKSt9nothrow_t, size, nothrow); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnwjRKSt9nothrow_t, size, nothrow); LOGI(TAG, "+ _ZnwjRKSt9nothrow_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _Znaj, size_t size) { - CALL_ORIGIN_FUNC_RET(void*, p, _Znaj, size); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _Znaj, size); LOGI(TAG, "+ _Znaj %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _ZnajSt11align_val_t, size_t size, std::align_val_t align_val) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnajSt11align_val_t, size, align_val); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnajSt11align_val_t, size, align_val); LOGI(TAG, "+ _ZnajSt11align_val_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; @@ -230,14 +230,14 @@ DEFINE_HOOK_FUN(void*, _ZnajSt11align_val_t, size_t size, std::align_val_t align DEFINE_HOOK_FUN(void*, _ZnajSt11align_val_tRKSt9nothrow_t, size_t size, std::align_val_t align_val, std::nothrow_t const& nothrow) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnajSt11align_val_tRKSt9nothrow_t, size, align_val, nothrow); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnajSt11align_val_tRKSt9nothrow_t, size, align_val, nothrow); LOGI(TAG, "+ _ZnajSt11align_val_tRKSt9nothrow_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _ZnajRKSt9nothrow_t, size_t size, std::nothrow_t const& nothrow) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnajRKSt9nothrow_t, size, nothrow); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnajRKSt9nothrow_t, size, nothrow); LOGI(TAG, "+ _ZnajRKSt9nothrow_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; @@ -246,40 +246,40 @@ DEFINE_HOOK_FUN(void*, _ZnajRKSt9nothrow_t, size_t size, std::nothrow_t const& n DEFINE_HOOK_FUN(void, _ZdaPvj, void* ptr, size_t size) { LOGI(TAG, "- _ZdaPvj %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdaPvj, ptr, size); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdaPvj, ptr, size); } DEFINE_HOOK_FUN(void, _ZdaPvjSt11align_val_t, void* ptr, size_t size, std::align_val_t align_val) { LOGI(TAG, "- _ZdaPvjSt11align_val_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdaPvjSt11align_val_t, ptr, size, align_val); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdaPvjSt11align_val_t, ptr, size, align_val); } DEFINE_HOOK_FUN(void, _ZdlPvj, void* ptr, size_t size) { LOGI(TAG, "- _ZdlPvj %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdlPvj, ptr, size); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdlPvj, ptr, size); } DEFINE_HOOK_FUN(void, _ZdlPvjSt11align_val_t, void* ptr, size_t size, std::align_val_t align_val) { LOGI(TAG, "- _ZdlPvjSt11align_val_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdlPvjSt11align_val_t, ptr, size, align_val); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdlPvjSt11align_val_t, ptr, size, align_val); } #else DEFINE_HOOK_FUN(void*, _Znwm, size_t size) { - CALL_ORIGIN_FUNC_RET(void*, p, _Znwm, size); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _Znwm, size); LOGI(TAG, "+ _Znwm %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _ZnwmSt11align_val_t, size_t size, std::align_val_t align_val) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnwmSt11align_val_t, size, align_val); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnwmSt11align_val_t, size, align_val); LOGI(TAG, "+ _ZnwmSt11align_val_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; @@ -288,28 +288,28 @@ DEFINE_HOOK_FUN(void*, _ZnwmSt11align_val_t, size_t size, std::align_val_t align DEFINE_HOOK_FUN(void*, _ZnwmSt11align_val_tRKSt9nothrow_t, size_t size, std::align_val_t align_val, std::nothrow_t const ¬hrow) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnwmSt11align_val_tRKSt9nothrow_t, size, align_val, nothrow); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnwmSt11align_val_tRKSt9nothrow_t, size, align_val, nothrow); LOGI(TAG, "+ _ZnwmSt11align_val_tRKSt9nothrow_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _ZnwmRKSt9nothrow_t, size_t size, std::nothrow_t const ¬hrow) { - CALL_ORIGIN_FUNC_RET(void*, p, _Znwm, size); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _Znwm, size); LOGI(TAG, "+ _ZnwmRKSt9nothrow_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _Znam, size_t size) { - CALL_ORIGIN_FUNC_RET(void*, p, _Znam, size); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _Znam, size); LOGI(TAG, "+ _Znam %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _ZnamSt11align_val_t, size_t size, std::align_val_t align_val) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnamSt11align_val_t, size, align_val); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnamSt11align_val_t, size, align_val); LOGI(TAG, "+ _ZnamSt11align_val_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; @@ -318,14 +318,14 @@ DEFINE_HOOK_FUN(void*, _ZnamSt11align_val_t, size_t size, std::align_val_t align DEFINE_HOOK_FUN(void*, _ZnamSt11align_val_tRKSt9nothrow_t, size_t size, std::align_val_t align_val, std::nothrow_t const ¬hrow) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnamSt11align_val_tRKSt9nothrow_t, size, align_val, nothrow); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnamSt11align_val_tRKSt9nothrow_t, size, align_val, nothrow); LOGI(TAG, "+ _ZnamSt11align_val_tRKSt9nothrow_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; } DEFINE_HOOK_FUN(void*, _ZnamRKSt9nothrow_t, size_t size, std::nothrow_t const ¬hrow) { - CALL_ORIGIN_FUNC_RET(void*, p, _ZnamRKSt9nothrow_t, size, nothrow); + CALL_ORIGIN_FUNC_RET(libcxx_handle, void*, p, _ZnamRKSt9nothrow_t, size, nothrow); LOGI(TAG, "+ _ZnamRKSt9nothrow_t %p", p); DO_HOOK_ACQUIRE(p, size); return p; @@ -334,27 +334,27 @@ DEFINE_HOOK_FUN(void*, _ZnamRKSt9nothrow_t, size_t size, std::nothrow_t const &n DEFINE_HOOK_FUN(void, _ZdlPvm, void *ptr, size_t size) { LOGI(TAG, "- _ZdlPvm %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdlPvm, ptr, size); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdlPvm, ptr, size); } DEFINE_HOOK_FUN(void, _ZdlPvmSt11align_val_t, void *ptr, size_t size, std::align_val_t align_val) { LOGI(TAG, "- _ZdlPvmSt11align_val_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdlPvmSt11align_val_t, ptr, size, align_val); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdlPvmSt11align_val_t, ptr, size, align_val); } DEFINE_HOOK_FUN(void, _ZdaPvm, void *ptr, size_t size) { LOGI(TAG, "- _ZdaPvm %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdaPvm, ptr, size); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdaPvm, ptr, size); } DEFINE_HOOK_FUN(void, _ZdaPvmSt11align_val_t, void *ptr, size_t size, std::align_val_t align_val) { LOGI(TAG, "- _ZdaPvmSt11align_val_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdaPvmSt11align_val_t, ptr, size, align_val); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdaPvmSt11align_val_t, ptr, size, align_val); } #endif @@ -362,13 +362,13 @@ DEFINE_HOOK_FUN(void, _ZdaPvmSt11align_val_t, void *ptr, size_t size, DEFINE_HOOK_FUN(void, _ZdlPv, void *p) { LOGI(TAG, "- _ZdlPv %p", p); DO_HOOK_RELEASE(p); - CALL_ORIGIN_FUNC_VOID(_ZdlPv, p); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdlPv, p); } DEFINE_HOOK_FUN(void, _ZdlPvSt11align_val_t, void *ptr, std::align_val_t align_val) { LOGI(TAG, "- _ZdlPvSt11align_val_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdlPvSt11align_val_t, ptr, align_val); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdlPvSt11align_val_t, ptr, align_val); } DEFINE_HOOK_FUN(void, _ZdlPvSt11align_val_tRKSt9nothrow_t, void *ptr, @@ -376,25 +376,25 @@ DEFINE_HOOK_FUN(void, _ZdlPvSt11align_val_tRKSt9nothrow_t, void *ptr, std::nothrow_t const ¬hrow) { LOGI(TAG, "- _ZdlPvSt11align_val_tRKSt9nothrow_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdlPvSt11align_val_tRKSt9nothrow_t, ptr, align_val, nothrow); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdlPvSt11align_val_tRKSt9nothrow_t, ptr, align_val, nothrow); } DEFINE_HOOK_FUN(void, _ZdlPvRKSt9nothrow_t, void *ptr, std::nothrow_t const ¬hrow) { LOGI(TAG, "- _ZdlPvRKSt9nothrow_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdlPvRKSt9nothrow_t, ptr, nothrow); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdlPvRKSt9nothrow_t, ptr, nothrow); } DEFINE_HOOK_FUN(void, _ZdaPv, void *ptr) { LOGI(TAG, "- _ZdaPv %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdaPv, ptr); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdaPv, ptr); } DEFINE_HOOK_FUN(void, _ZdaPvSt11align_val_t, void *ptr, std::align_val_t align_val) { LOGI(TAG, "- _ZdaPvSt11align_val_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdaPvSt11align_val_t, ptr, align_val); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdaPvSt11align_val_t, ptr, align_val); } DEFINE_HOOK_FUN(void, _ZdaPvSt11align_val_tRKSt9nothrow_t, void *ptr, @@ -402,13 +402,13 @@ DEFINE_HOOK_FUN(void, _ZdaPvSt11align_val_tRKSt9nothrow_t, void *ptr, std::nothrow_t const ¬hrow) { LOGI(TAG, "- _ZdaPvSt11align_val_tRKSt9nothrow_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdaPvSt11align_val_tRKSt9nothrow_t, ptr, align_val, nothrow); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdaPvSt11align_val_tRKSt9nothrow_t, ptr, align_val, nothrow); } DEFINE_HOOK_FUN(void, _ZdaPvRKSt9nothrow_t, void *ptr, std::nothrow_t const ¬hrow) { LOGI(TAG, "- _ZdaPvRKSt9nothrow_t %p", ptr); DO_HOOK_RELEASE(ptr); - CALL_ORIGIN_FUNC_VOID(_ZdaPvRKSt9nothrow_t, ptr, nothrow); + CALL_ORIGIN_FUNC_VOID(libcxx_handle, _ZdaPvRKSt9nothrow_t, ptr, nothrow); } #undef ORIGINAL_LIB \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookFunctions.h b/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookFunctions.h index 11ec15d7e..9dd9c26f7 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookFunctions.h +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookFunctions.h @@ -47,16 +47,14 @@ DECLARE_HOOK_ORIG(void *, memalign, size_t __alignment, size_t __byte_count); DECLARE_HOOK_ORIG(int, posix_memalign, void** __memptr, size_t __alignment, size_t __size); #if defined(__USE_FILE_OFFSET64) -// DECLARE_HOOK_ORIG not supports attrbute -void *h_mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset) __RENAME(mmap64); +DECLARE_HOOK_ORIG_ATTR(void *, mmap, void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset) __RENAME(mmap64); #else DECLARE_HOOK_ORIG(void *, mmap, void *__addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset); #endif #if __ANDROID_API__ >= __ANDROID_API_L__ -// DECLARE_HOOK_ORIG not supports attrbute -void *h_mmap64(void *__addr, size_t __size, int __prot, int __flags, int __fd, - off64_t __offset) __INTRODUCED_IN(21); +DECLARE_HOOK_ORIG_ATTR(void *, mmap64, void *__addr, size_t __size, int __prot, int __flags, int __fd, + off64_t __offset) __INTRODUCED_IN(21); #endif DECLARE_HOOK_ORIG(void *, mremap, void*, size_t, size_t, int, ...) diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookJNI.cpp b/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookJNI.cpp index 3fad302ab..f48858978 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookJNI.cpp +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/MemoryHookJNI.cpp @@ -29,83 +29,88 @@ #ifdef __cplusplus extern "C" { #endif -// @formatter:off -const HookFunction HOOK_MALL_FUNCTIONS[] = { - {"malloc", (void *) h_malloc, NULL}, - {"calloc", (void *) h_calloc, NULL}, - {"realloc", (void *) h_realloc, NULL}, - {"free", (void *) h_free, NULL}, - {"memalign", (void *) HANDLER_FUNC_NAME(memalign), NULL}, - {"posix_memalign", (void *) HANDLER_FUNC_NAME(posix_memalign), NULL}, - // CXX functions -#ifndef __LP64__ - {"_Znwj", (void*) HANDLER_FUNC_NAME(_Znwj), NULL}, - {"_ZnwjSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZnwjSt11align_val_t), NULL}, - {"_ZnwjSt11align_val_tRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZnwjSt11align_val_tRKSt9nothrow_t), NULL}, - {"_ZnwjRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZnwjRKSt9nothrow_t), NULL}, - - {"_Znaj", (void*) HANDLER_FUNC_NAME(_Znaj), NULL}, - {"_ZnajSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZnajSt11align_val_t), NULL}, - {"_ZnajSt11align_val_tRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZnajSt11align_val_tRKSt9nothrow_t), NULL}, - {"_ZnajRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZnajRKSt9nothrow_t), NULL}, - - {"_ZdlPvj", (void*) HANDLER_FUNC_NAME(_ZdlPvj), NULL}, - {"_ZdlPvjSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZdlPvjSt11align_val_t), NULL}, - {"_ZdaPvj", (void*) HANDLER_FUNC_NAME(_ZdaPvj), NULL}, - {"_ZdaPvjSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZdaPvjSt11align_val_t), NULL}, -#else - {"_Znwm", (void*) HANDLER_FUNC_NAME(_Znwm), NULL}, - {"_ZnwmSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZnwmSt11align_val_t), NULL}, - {"_ZnwmSt11align_val_tRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZnwmSt11align_val_tRKSt9nothrow_t), NULL}, - {"_ZnwmRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZnwmRKSt9nothrow_t), NULL}, - - {"_Znam", (void*) HANDLER_FUNC_NAME(_Znam), NULL}, - {"_ZnamSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZnamSt11align_val_t), NULL}, - {"_ZnamSt11align_val_tRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZnamSt11align_val_tRKSt9nothrow_t), NULL}, - {"_ZnamRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZnamRKSt9nothrow_t), NULL}, - - {"_ZdlPvm", (void*) HANDLER_FUNC_NAME(_ZdlPvm), NULL}, - {"_ZdlPvmSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZdlPvmSt11align_val_t), NULL}, - {"_ZdaPvm", (void*) HANDLER_FUNC_NAME(_ZdaPvm), NULL}, - {"_ZdaPvmSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZdaPvmSt11align_val_t), NULL}, -#endif - {"_ZdlPv", (void*) HANDLER_FUNC_NAME(_ZdlPv), NULL}, - {"_ZdlPvSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZdlPvSt11align_val_t), NULL}, - {"_ZdlPvSt11align_val_tRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZdlPvSt11align_val_tRKSt9nothrow_t), NULL}, - {"_ZdlPvRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZdlPvRKSt9nothrow_t), NULL}, - - {"_ZdaPv", (void*) HANDLER_FUNC_NAME(_ZdaPv), NULL}, - {"_ZdaPvSt11align_val_t", (void*) HANDLER_FUNC_NAME(_ZdaPvSt11align_val_t), NULL}, - {"_ZdaPvSt11align_val_tRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZdaPvSt11align_val_tRKSt9nothrow_t), NULL}, - {"_ZdaPvRKSt9nothrow_t", (void*) HANDLER_FUNC_NAME(_ZdaPvRKSt9nothrow_t), NULL}, - - {"strdup", (void*) HANDLER_FUNC_NAME(strdup), (void **) ORIGINAL_FUNC_NAME(strdup)}, - {"strndup", (void*) HANDLER_FUNC_NAME(strndup), (void **) ORIGINAL_FUNC_NAME(strndup)}, -}; - -static const HookFunction HOOK_MMAP_FUNCTIONS[] = { - {"mmap", (void *) h_mmap, NULL}, - {"munmap", (void *) h_munmap, NULL}, - {"mremap", (void *) h_mremap, NULL}, -#if __ANDROID_API__ >= __ANDROID_API_L__ - {"mmap64", (void *) h_mmap64, NULL}, -#endif -}; -// @formatter:on + +#define HOOK_REGISTER(regex, target_sym, target_so) \ + do { \ + FETCH_ORIGIN_FUNC_OF_SO(target_sym, target_so); \ + if(!ORIGINAL_FUNC_NAME(target_sym)) { \ + LOGE(TAG, "hook failed: fetch origin func failed: %s", #target_sym); \ + break; \ + } \ + int ret = xhook_grouped_register(HOOK_REQUEST_GROUPID_MEMORY, regex, #target_sym, (void*) HANDLER_FUNC_NAME(target_sym), nullptr); \ + LOGD(TAG, "hook fn, regex: %s, sym: %s, ret: %d", regex, #target_sym, ret); \ + } while(0); + +#define HOOK_REGISTER_REGEX_LIBC(target_sym) \ + HOOK_REGISTER(regex, target_sym, "libc.so") + +#define HOOK_REGISTER_REGEX_LIBCXX(target_sym) \ + HOOK_REGISTER(regex, target_sym, "libc++_shared.so") + bool enable_mmap_hook = false; static void hook(const char *regex) { + HOOK_REGISTER_REGEX_LIBC(malloc) + HOOK_REGISTER_REGEX_LIBC(calloc) + HOOK_REGISTER_REGEX_LIBC(realloc) + HOOK_REGISTER_REGEX_LIBC(free) + + HOOK_REGISTER_REGEX_LIBC(memalign) + HOOK_REGISTER_REGEX_LIBC(posix_memalign) + HOOK_REGISTER_REGEX_LIBC(strdup) + HOOK_REGISTER_REGEX_LIBC(strndup) + + // CXX functions +#ifndef __LP64__ + HOOK_REGISTER_REGEX_LIBCXX(_Znwj) + HOOK_REGISTER_REGEX_LIBCXX(_ZnwjSt11align_val_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZnwjSt11align_val_tRKSt9nothrow_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZnwjRKSt9nothrow_t) + + HOOK_REGISTER_REGEX_LIBCXX(_Znaj) + HOOK_REGISTER_REGEX_LIBCXX(_ZnajSt11align_val_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZnajSt11align_val_tRKSt9nothrow_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZnajRKSt9nothrow_t) + + HOOK_REGISTER_REGEX_LIBCXX(_ZdlPvj) + HOOK_REGISTER_REGEX_LIBCXX(_ZdlPvjSt11align_val_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZdaPvj) + HOOK_REGISTER_REGEX_LIBCXX(_ZdaPvjSt11align_val_t) +#else + HOOK_REGISTER_REGEX_LIBCXX(_Znwm) + HOOK_REGISTER_REGEX_LIBCXX(_ZnwmSt11align_val_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZnwmSt11align_val_tRKSt9nothrow_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZnwmRKSt9nothrow_t) + + HOOK_REGISTER_REGEX_LIBCXX(_Znam) + HOOK_REGISTER_REGEX_LIBCXX(_ZnamSt11align_val_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZnamSt11align_val_tRKSt9nothrow_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZnamRKSt9nothrow_t) + + HOOK_REGISTER_REGEX_LIBCXX(_ZdlPvm) + HOOK_REGISTER_REGEX_LIBCXX(_ZdlPvmSt11align_val_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZdaPvm) + HOOK_REGISTER_REGEX_LIBCXX(_ZdaPvmSt11align_val_t) +#endif + HOOK_REGISTER_REGEX_LIBCXX(_ZdlPv) + HOOK_REGISTER_REGEX_LIBCXX(_ZdlPvSt11align_val_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZdlPvSt11align_val_tRKSt9nothrow_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZdlPvRKSt9nothrow_t) + + HOOK_REGISTER_REGEX_LIBCXX(_ZdaPv) + HOOK_REGISTER_REGEX_LIBCXX(_ZdaPvSt11align_val_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZdaPvSt11align_val_tRKSt9nothrow_t) + HOOK_REGISTER_REGEX_LIBCXX(_ZdaPvRKSt9nothrow_t) - for (auto f : HOOK_MALL_FUNCTIONS) { - int ret = xhook_grouped_register(HOOK_REQUEST_GROUPID_MEMORY, regex, f.name, f.handler_ptr, f.origin_ptr); - LOGD(TAG, "hook fn, regex: %s, sym: %s, ret: %d", regex, f.name, ret); - } LOGD(TAG, "mmap enabled ? %d", enable_mmap_hook); if (enable_mmap_hook) { - for (auto f: HOOK_MMAP_FUNCTIONS) { - xhook_grouped_register(HOOK_REQUEST_GROUPID_MEMORY, regex, f.name, f.handler_ptr, f.origin_ptr); - } + HOOK_REGISTER_REGEX_LIBC(mmap) + HOOK_REGISTER_REGEX_LIBC(munmap) + HOOK_REGISTER_REGEX_LIBC(mremap) +#if __ANDROID_API__ >= __ANDROID_API_L__ + HOOK_REGISTER_REGEX_LIBC(mmap64) +#endif } } diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/memory.ver b/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/memory.ver index a8583b9de..cb102d391 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/memory.ver +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/memory/memory.ver @@ -4,6 +4,4 @@ JNI_OnUnload; Java_*; fake_*; - local: - *; }; \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/pthread/PthreadHook.cpp b/matrix/matrix-android/matrix-hooks/src/main/cpp/pthread/PthreadHook.cpp index 8f9b64cad..56a3307ae 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/pthread/PthreadHook.cpp +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/pthread/PthreadHook.cpp @@ -32,6 +32,7 @@ #define LOG_TAG "Matrix.PthreadHook" #define ORIGINAL_LIB "libc.so" +static void *pthread_handle = nullptr; static volatile bool sThreadTraceEnabled = false; static volatile bool sThreadStackShrinkEnabled = false; @@ -67,11 +68,11 @@ DEFINE_HOOK_FUN(int, pthread_create, int ret = 0; if (sThreadTraceEnabled) { auto *routine_wrapper = thread_trace::wrap_pthread_routine(start_routine, args); - CALL_ORIGIN_FUNC_RET(int, tmpRet, pthread_create, pthread, &tmpAttr, routine_wrapper->wrapped_func, + CALL_ORIGIN_FUNC_RET(pthread_handle, int, tmpRet, pthread_create, pthread, &tmpAttr, routine_wrapper->wrapped_func, routine_wrapper); ret = tmpRet; } else { - CALL_ORIGIN_FUNC_RET(int, tmpRet, pthread_create, pthread, &tmpAttr, start_routine, args); + CALL_ORIGIN_FUNC_RET(pthread_handle, int, tmpRet, pthread_create, pthread, &tmpAttr, start_routine, args); ret = tmpRet; } @@ -87,7 +88,7 @@ DEFINE_HOOK_FUN(int, pthread_create, } DEFINE_HOOK_FUN(int, pthread_setname_np, pthread_t pthread, const char* name) { - CALL_ORIGIN_FUNC_RET(int, ret, pthread_setname_np, pthread, name); + CALL_ORIGIN_FUNC_RET(pthread_handle, int, ret, pthread_setname_np, pthread, name); if (LIKELY(ret == 0) && sThreadTraceEnabled) { thread_trace::handle_pthread_setname_np(pthread, name); } @@ -95,7 +96,7 @@ DEFINE_HOOK_FUN(int, pthread_setname_np, pthread_t pthread, const char* name) { } DEFINE_HOOK_FUN(int, pthread_detach, pthread_t pthread) { - CALL_ORIGIN_FUNC_RET(int, ret, pthread_detach, pthread); + CALL_ORIGIN_FUNC_RET(pthread_handle, int, ret, pthread_detach, pthread); LOGD(LOG_TAG, "pthread_detach : %d", ret); if (LIKELY(ret == 0) && sThreadTraceEnabled) { thread_trace::handle_pthread_release(pthread); @@ -104,7 +105,7 @@ DEFINE_HOOK_FUN(int, pthread_detach, pthread_t pthread) { } DEFINE_HOOK_FUN(int, pthread_join, pthread_t pthread, void** return_value_ptr) { - CALL_ORIGIN_FUNC_RET(int, ret, pthread_join, pthread, return_value_ptr); + CALL_ORIGIN_FUNC_RET(pthread_handle, int, ret, pthread_join, pthread, return_value_ptr); LOGD(LOG_TAG, "pthread_join : %d", ret); if (LIKELY(ret == 0) && sThreadTraceEnabled) { thread_trace::handle_pthread_release(pthread); diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/pthread/ThreadTrace.cpp b/matrix/matrix-android/matrix-hooks/src/main/cpp/pthread/ThreadTrace.cpp index e20d50953..78b0e3b97 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/pthread/ThreadTrace.cpp +++ b/matrix/matrix-android/matrix-hooks/src/main/cpp/pthread/ThreadTrace.cpp @@ -385,7 +385,32 @@ static inline void pthread_dump_impl(FILE *log_file) { continue; } - if (meta.unwind_mode == wechat_backtrace::FramePointer) { + if (meta.unwind_mode == wechat_backtrace::Quicken) { + + LOGD(TAG, "native stacktrace:"); + flogger0(log_file, "native stacktrace:\n"); + + size_t elements_size = 0; + const size_t max_elements = PTHREAD_BACKTRACE_FRAME_ELEMENTS_MAX_SIZE; + wechat_backtrace::FrameElement stacktrace_elements[max_elements]; + + get_stacktrace_elements(meta.native_backtrace.frames.get(), + meta.native_backtrace.frame_size, + true, stacktrace_elements, + max_elements, elements_size); + + for (size_t j = 0; j < elements_size; j++) { + std::string data; + wechat_backtrace::quicken_frame_format(stacktrace_elements[j], j, data); + LOGD(TAG, "%s", data.c_str()); + flogger0(log_file, data.c_str()); + + } + + LOGD(TAG, "java stacktrace:\n%s", meta.java_stacktrace.load(std::memory_order_acquire)); + flogger0(log_file, "java stacktrace:\n%s\n", + meta.java_stacktrace.load(std::memory_order_acquire)); + } else { LOGD(TAG, "native stacktrace:"); flogger0(log_file, "native stacktrace:\n"); @@ -410,31 +435,6 @@ static inline void pthread_dump_impl(FILE *log_file) { meta.native_backtrace.frame_size, frame_detail_lambda); - LOGD(TAG, "java stacktrace:\n%s", meta.java_stacktrace.load(std::memory_order_acquire)); - flogger0(log_file, "java stacktrace:\n%s\n", - meta.java_stacktrace.load(std::memory_order_acquire)); - } else if (meta.unwind_mode == wechat_backtrace::Quicken) { - - LOGD(TAG, "native stacktrace:"); - flogger0(log_file, "native stacktrace:\n"); - - size_t elements_size = 0; - const size_t max_elements = PTHREAD_BACKTRACE_FRAME_ELEMENTS_MAX_SIZE; - wechat_backtrace::FrameElement stacktrace_elements[max_elements]; - - get_stacktrace_elements(meta.native_backtrace.frames.get(), - meta.native_backtrace.frame_size, - true, stacktrace_elements, - max_elements, elements_size); - - for (size_t j = 0; j < elements_size; j++) { - std::string data; - wechat_backtrace::quicken_frame_format(stacktrace_elements[j], j, data); - LOGD(TAG, "%s", data.c_str()); - flogger0(log_file, data.c_str()); - - } - LOGD(TAG, "java stacktrace:\n%s", meta.java_stacktrace.load(std::memory_order_acquire)); flogger0(log_file, "java stacktrace:\n%s\n", meta.java_stacktrace.load(std::memory_order_acquire)); diff --git a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/AbsHook.java b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/AbsHook.java index a7f142c87..fea1c8225 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/AbsHook.java +++ b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/AbsHook.java @@ -16,7 +16,7 @@ package com.tencent.matrix.hook; -import androidx.annotation.Nullable; +import androidx.annotation.NonNull; /** * Created by Yves on 2020-03-18 @@ -40,7 +40,7 @@ public Status getStatus() { return mStatus; } - @Nullable + @NonNull protected abstract String getNativeLibraryName(); protected abstract boolean onConfigure(); diff --git a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/HookManager.java b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/HookManager.java index 5db9f99db..70759879a 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/HookManager.java +++ b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/HookManager.java @@ -16,6 +16,7 @@ package com.tencent.matrix.hook; +import android.os.Build; import android.text.TextUtils; import androidx.annotation.Keep; @@ -24,6 +25,10 @@ import com.tencent.matrix.util.MatrixLog; +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; import java.util.HashSet; import java.util.Set; @@ -83,6 +88,47 @@ public void commitHooks() throws HookFailedException { } } + private boolean enableLibCxxSharedCheck = false; + public HookManager enableLibCxxSharedCheck(boolean enable) { + enableLibCxxSharedCheck = enable; + return this; + } + + private boolean checkLibCxxSharedLoaded() { + if (Build.VERSION.SDK_INT != Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT != Build.VERSION_CODES.LOLLIPOP_MR1) { + return true; + } + + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/self/maps")))) { + String line; + while ((line = br.readLine()) != null) { + if (line.endsWith("libc++_shared.so")) { + return true; + } + } + } catch (IOException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + + return false; + } + + private void ensureLibCxxSharedLoadedForLollipop() throws RuntimeException { + if (!enableLibCxxSharedCheck) { + return; + } + enableLibCxxSharedCheck = false; // mark loaded + if (checkLibCxxSharedLoaded()) { + return; + } + if (mNativeLibLoader != null) { + mNativeLibLoader.loadLibrary("c++_shared"); + } else { + System.loadLibrary("c++_shared"); + } + } + + private void commitHooksLocked() throws HookFailedException { synchronized (mPendingHooks) { for (AbsHook hook : mPendingHooks) { @@ -91,6 +137,7 @@ private void commitHooksLocked() throws HookFailedException { continue; } try { + ensureLibCxxSharedLoadedForLollipop(); if (mNativeLibLoader != null) { mNativeLibLoader.loadLibrary(nativeLibName); } else { @@ -160,7 +207,11 @@ public HookManager clearHooks() { @Keep public static String getStack() { - return stackTraceToString(Thread.currentThread().getStackTrace()); + try { + return stackTraceToString(Thread.currentThread().getStackTrace()); + } catch (Throwable e) { + return "ERROR: " + stackTraceToString(e.getStackTrace()); + } } private static String stackTraceToString(final StackTraceElement[] arr) { diff --git a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/art/RuntimeVerifyMute.java b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/art/RuntimeVerifyMute.java new file mode 100644 index 000000000..07efcc95f --- /dev/null +++ b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/art/RuntimeVerifyMute.java @@ -0,0 +1,56 @@ +package com.tencent.matrix.hook.art; + +import androidx.annotation.Nullable; + +import com.tencent.matrix.hook.HookManager.NativeLibraryLoader; +import com.tencent.matrix.util.MatrixLog; + +/** + * Created by tomystang on 2022/11/16. + */ +public final class RuntimeVerifyMute { + private static final String TAG = "Matrix.RuntimeVerifyMute"; + + public static final RuntimeVerifyMute INSTANCE = new RuntimeVerifyMute(); + + private NativeLibraryLoader mNativeLibLoader = null; + private boolean mNativeLibLoaded = false; + + public RuntimeVerifyMute setNativeLibraryLoader(@Nullable NativeLibraryLoader loader) { + mNativeLibLoader = loader; + return this; + } + + private boolean ensureNativeLibLoaded() { + synchronized (this) { + if (mNativeLibLoaded) { + return true; + } + try { + if (mNativeLibLoader != null) { + mNativeLibLoader.loadLibrary("matrix-hookcommon"); + mNativeLibLoader.loadLibrary("matrix-artmisc"); + } else { + System.loadLibrary("matrix-hookcommon"); + System.loadLibrary("matrix-artmisc"); + } + mNativeLibLoaded = true; + } catch (Throwable thr) { + MatrixLog.printErrStackTrace(TAG, thr, "Fail to load native library."); + mNativeLibLoaded = false; + } + return mNativeLibLoaded; + } + } + + public boolean install() { + if (!ensureNativeLibLoaded()) { + return false; + } + return nativeInstall(); + } + + private static native boolean nativeInstall(); + + private RuntimeVerifyMute() { } +} diff --git a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/memory/MemoryHook.java b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/memory/MemoryHook.java index 52183aa80..24b4d10bc 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/memory/MemoryHook.java +++ b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/memory/MemoryHook.java @@ -19,11 +19,10 @@ import android.text.TextUtils; import androidx.annotation.Keep; -import androidx.annotation.Nullable; +import androidx.annotation.NonNull; import com.tencent.matrix.hook.AbsHook; import com.tencent.matrix.hook.HookManager; -import com.tencent.matrix.memguard.MemGuard; import com.tencent.matrix.util.MatrixLog; import java.util.HashSet; @@ -46,6 +45,7 @@ public class MemoryHook extends AbsHook { private int mStacktraceLogThreshold = 10 * 1024 * 1024; private boolean mEnableStacktrace; private boolean mEnableMmap; + private boolean mMemGuardInstalled = false; private boolean mHookInstalled = false; private MemoryHook() { @@ -109,6 +109,10 @@ public MemoryHook stacktraceLogThreshold(int threshold) { return this; } + public void notifyMemGuardInstalled() { + mMemGuardInstalled = true; + } + /** * notice: it is an exclusive interface */ @@ -119,7 +123,7 @@ public void hook() throws HookManager.HookFailedException { .commitHooks(); } - @Nullable + @NonNull @Override protected String getNativeLibraryName() { return "matrix-memoryhook"; @@ -127,7 +131,7 @@ protected String getNativeLibraryName() { @Override public boolean onConfigure() { - if (MemGuard.isInstalled()) { + if (mMemGuardInstalled) { MatrixLog.w(TAG, "MemGuard has been installed, skip MemoryHook install logic."); return false; } diff --git a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/memory/WVPreAllocHook.java b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/memory/WVPreAllocHook.java index f8bfb2d28..ced401446 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/memory/WVPreAllocHook.java +++ b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/memory/WVPreAllocHook.java @@ -18,14 +18,14 @@ import android.os.Build; -import androidx.annotation.Nullable; +import androidx.annotation.NonNull; import com.tencent.matrix.hook.AbsHook; public class WVPreAllocHook extends AbsHook { public static final WVPreAllocHook INSTANCE = new WVPreAllocHook(); - @Nullable + @NonNull @Override protected String getNativeLibraryName() { return "matrix-memoryhook"; diff --git a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/pthread/PthreadHook.java b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/pthread/PthreadHook.java index 4cbf51a44..1cb7917fc 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/pthread/PthreadHook.java +++ b/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/hook/pthread/PthreadHook.java @@ -146,7 +146,7 @@ public void enableLogger(boolean enable) { } } - @Nullable + @NonNull @Override protected String getNativeLibraryName() { return "matrix-pthreadhook"; diff --git a/matrix/matrix-android/matrix-hprof-analyzer/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/CMakeLists.txt new file mode 100644 index 000000000..f0ac82fbc --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +option(TEST_MODE "Enable test mode." OFF) +if (TEST_MODE) + add_definitions(-D__test_mode__) +endif () + + +include_directories(include) + +add_subdirectory(lib) + +if (TEST_MODE) + enable_testing() + add_subdirectory(test) +endif () \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/include/macro.h b/matrix/matrix-android/matrix-hprof-analyzer/include/macro.h new file mode 100644 index 000000000..2ef6dc141 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/include/macro.h @@ -0,0 +1,17 @@ +#ifndef __matrix_hprof_analyzer_macro_h__ +#define __matrix_hprof_analyzer_macro_h__ + +#define unwrap(optional, nullopt_action) \ + ({ \ + const auto &result = optional; \ + if (!result.has_value()) nullopt_action; \ + result.value(); \ + }) + +#ifdef __test_mode__ +#define friend_test(test_case_name, test_name) friend class test_case_name##_##test_name##_Test +#else +#define friend_test(test_case_name, test_name) +#endif + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/lib/CMakeLists.txt new file mode 100644 index 000000000..4cf446583 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +add_library(matrix_hprof_analyzer_error STATIC err/errorha.cpp) +target_include_directories(matrix_hprof_analyzer_error INTERFACE err/include) + +add_library(matrix_hprof_analyzer_reader STATIC reader/reader.cpp) +target_include_directories(matrix_hprof_analyzer_reader INTERFACE reader/include) +target_link_libraries(matrix_hprof_analyzer_reader matrix_hprof_analyzer_error) + +add_library(matrix_hprof_analyzer_heap STATIC heap/heap.cpp heap/primitive.cpp) +target_include_directories(matrix_hprof_analyzer_heap INTERFACE heap/include) +target_link_libraries(matrix_hprof_analyzer_heap matrix_hprof_analyzer_error matrix_hprof_analyzer_reader) + +if (TEST_MODE) + add_library(matrix_hprof_analyzer_parser STATIC parser/parser.cpp parser/engine.cpp) + target_include_directories(matrix_hprof_analyzer_parser INTERFACE parser/include parser/internal) + target_link_libraries(matrix_hprof_analyzer_parser matrix_hprof_analyzer_error matrix_hprof_analyzer_heap) +else () + add_library(matrix_hprof_analyzer_parser STATIC parser/parser.cpp parser/engine.cpp) + target_include_directories(matrix_hprof_analyzer_parser INTERFACE parser/include) + target_link_libraries(matrix_hprof_analyzer_parser matrix_hprof_analyzer_error matrix_hprof_analyzer_heap) +endif () + +add_library(matrix_hprof_analyzer_analyzer STATIC analyzer/analyzer.cpp) +target_include_directories(matrix_hprof_analyzer_analyzer INTERFACE analyzer/include) +target_link_libraries(matrix_hprof_analyzer_analyzer matrix_hprof_analyzer_error matrix_hprof_analyzer_heap) + +if (TEST_MODE) + add_library(matrix_hprof_analyzer SHARED main/analyzer.cpp main/chain.cpp main/heap.cpp) + target_include_directories(matrix_hprof_analyzer INTERFACE main/include main/internal ../include) + target_link_libraries(matrix_hprof_analyzer + PRIVATE matrix_hprof_analyzer_error + PRIVATE matrix_hprof_analyzer_heap + PRIVATE matrix_hprof_analyzer_parser + PRIVATE matrix_hprof_analyzer_analyzer) +else () + add_library(matrix_hprof_analyzer SHARED main/analyzer.cpp main/chain.cpp main/heap.cpp) + target_include_directories(matrix_hprof_analyzer INTERFACE main/include ../include) + target_link_libraries(matrix_hprof_analyzer + PRIVATE matrix_hprof_analyzer_error + PRIVATE matrix_hprof_analyzer_heap + PRIVATE matrix_hprof_analyzer_parser + PRIVATE matrix_hprof_analyzer_analyzer) +endif () \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/analyzer/analyzer.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/analyzer/analyzer.cpp new file mode 100644 index 000000000..cd0d947be --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/analyzer/analyzer.cpp @@ -0,0 +1,88 @@ +#include "include/analyzer.h" + +#include + +namespace matrix::hprof::internal::analyzer { + + struct ref_super_t { + heap::object_id_t referrer_id; + heap::reference_t reference; + }; + + struct ref_node_t { + heap::object_id_t referent_id; + std::optional super; + size_t depth; + }; + + std::map>>> + find_leak_chains(const heap::Heap &heap, const std::vector &tracked) { + + /* Use Breadth-First Search algorithm to find all the references to tracked objects. */ + + std::map>>> ret; + + for (const auto &leak: tracked) { + std::map traversed; + std::deque waiting; + + for (const heap::object_id_t gc_root: heap.GetGcRoots()) { + ref_node_t node = { + .referent_id = gc_root, + .super = std::nullopt, + .depth = 0 + }; + traversed[gc_root] = node; + waiting.push_back(node); + } + + bool found = false; + while (!waiting.empty()) { + const ref_node_t node = waiting.front(); + waiting.pop_front(); + const heap::object_id_t referrer_id = node.referent_id; + if (heap.GetLeakReferenceGraph().count(referrer_id) == 0) continue; + for (const auto &[referent, reference]: heap.GetLeakReferenceGraph().at(referrer_id)) { + try { + if (traversed.at(referent).depth <= node.depth + 1) continue; + } catch (const std::out_of_range &) {} + ref_node_t next_node = { + .referent_id = referent, + .super = ref_super_t{ + .referrer_id = referrer_id, + .reference = reference + }, + .depth = node.depth + 1 + }; + traversed[referent] = next_node; + if (leak == referent) { + found = true; + goto traverse_complete; + } else { + waiting.push_back(next_node); + } + } + } + traverse_complete: + if (found) { + ret[leak] = std::vector>>(); + std::optional current = leak; + std::optional current_reference = std::nullopt; + while (current != std::nullopt) { + ret[leak].push_back(std::make_pair(current.value(), current_reference)); + const auto &super = traversed.at(current.value()).super; + if (super.has_value()) { + current = super.value().referrer_id; + current_reference = super.value().reference; + } else { + current = std::nullopt; + } + } + std::reverse(ret[leak].begin(), ret[leak].end()); + } + } + + return std::move(ret); + } +} + diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/analyzer/include/analyzer.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/analyzer/include/analyzer.h new file mode 100644 index 000000000..67c57ff65 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/analyzer/include/analyzer.h @@ -0,0 +1,23 @@ +#ifndef __matrix_hprof_analyzer_analyzer_h__ +#define __matrix_hprof_analyzer_analyzer_h__ + +#include "heap.h" + +#include +#include +#include + +namespace matrix::hprof::internal::analyzer { + + /** + * The key of returned map is the leak identifier in \a tracked, or the identifier is not in map keys if the object + * is not a leak. + *

+ * The value of returned map is a list of referrer-reference pair. The reference of last list element must be + * std::nullopt and the referrer is the leak. + */ + std::map>>> + find_leak_chains(const heap::Heap &heap, const std::vector& tracked); +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/err/errorha.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/err/errorha.cpp new file mode 100644 index 000000000..a48631cc1 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/err/errorha.cpp @@ -0,0 +1,26 @@ +#include "include/errorha.h" + +#include + +static error_listener_t error_listener = nullptr; + +error_listener_t set_matrix_hprof_analyzer_error_listener(error_listener_t listener) { + const error_listener_t previous = error_listener; + error_listener = listener; + return previous; +} + +void pub_error(const std::string& message) { + if (error_listener != nullptr) { + error_listener(message.c_str()); + } +} + +[[noreturn]] void pub_fatal(const std::string& message) { + pub_error(message); +#if __test_mode__ + throw std::runtime_error(message); +#else + _exit(10); +#endif +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/err/include/errorha.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/err/include/errorha.h new file mode 100644 index 000000000..9d4d1bd27 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/err/include/errorha.h @@ -0,0 +1,14 @@ +#ifndef __matrix_hprof_analyzer_err_h__ +#define __matrix_hprof_analyzer_err_h__ + +#include + +typedef void (*error_listener_t)(const char *message); + +error_listener_t set_matrix_hprof_analyzer_error_listener(error_listener_t listener); + +void pub_error(const std::string& message); + +[[noreturn]] void pub_fatal(const std::string& message); + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/heap.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/heap.cpp new file mode 100644 index 000000000..443cb9a8a --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/heap.cpp @@ -0,0 +1,572 @@ +#include "errorha.h" +#include "include/heap.h" + +#include +#include +#include + +namespace matrix::hprof::internal::heap { + + size_t get_value_type_size(value_type_t type) { + switch (type) { + case value_type_t::kObject: + return 0; + case value_type_t::kBoolean: + return sizeof(uint8_t); + case value_type_t::kChar: + // See https://www.oracle.com/technical-resources/articles/javase/supplementary.html. + return sizeof(uint16_t); + case value_type_t::kFloat: + return sizeof(uint32_t); + case value_type_t::kDouble: + return sizeof(uint64_t); + case value_type_t::kByte: + return sizeof(uint8_t); + case value_type_t::kShort: + return sizeof(uint16_t); + case value_type_t::kInt: + return sizeof(uint32_t); + case value_type_t::kLong: + return sizeof(uint64_t); + } + } + + static const std::vector valid_value_types = { + static_cast(value_type_t::kObject), + static_cast(value_type_t::kBoolean), + static_cast(value_type_t::kChar), + static_cast(value_type_t::kFloat), + static_cast(value_type_t::kDouble), + static_cast(value_type_t::kByte), + static_cast(value_type_t::kShort), + static_cast(value_type_t::kInt), + static_cast(value_type_t::kLong) + }; + + value_type_t value_type_cast(uint8_t type) { + if (std::find(valid_value_types.begin(), valid_value_types.end(), type) == + valid_value_types.end()) { + std::stringstream error_builder; + error_builder << "invalid value type " << std::to_string(type); + pub_fatal(error_builder.str()); + } + return static_cast(type); + } + + HeapFieldsData::HeapFieldsData(object_id_t instance_id, object_id_t class_id, + size_t fields_data_size, reader::Reader *source_reader) : + instance_id_(instance_id), + class_id_(class_id), + fields_data_size_(fields_data_size), + fields_data_( + reinterpret_cast(source_reader->Extract(fields_data_size))) {} + + static std::map> find_class_by_name_memorized; + + static std::optional + find_class_by_name_get_memorized_result(const Heap *heap, const std::string &name) { + try { + return find_class_by_name_memorized.at(heap).at(name); + } catch (const std::out_of_range &) { + return std::nullopt; + } + } + + static void find_class_by_name_set_memorized_result(const Heap *heap, const std::string &name, + object_id_t result) { + find_class_by_name_memorized[heap][name] = result; + } + + static std::map> find_string_id_memorized; + + static std::optional + find_string_id_get_memorized_result(const Heap *heap, const std::string &name) { + try { + return find_string_id_memorized.at(heap).at(name); + } catch (const std::out_of_range &) { + return std::nullopt; + } + } + + static void find_string_id_set_memorized_result(const Heap *heap, const std::string &name, + string_id_t result) { + find_string_id_memorized[heap][name] = result; + } + + Heap::~Heap() { + find_class_by_name_memorized.erase(this); + find_string_id_memorized.erase(this); + } + + + // identifier size + + void Heap::InitializeIdSize(size_t id_size) { + if (id_size == 0) { + pub_fatal("invalid identifier size"); + } + if (id_size_ != 0) { + pub_fatal("identifier size already initialized"); + } + id_size_ = id_size; + } + + size_t Heap::GetIdSize() const { + if (id_size_ == 0) { + pub_fatal("identifier size not initialized"); + } + return id_size_; + } + + + // class name + + void Heap::AddClassNameRecord(object_id_t class_id, string_id_t class_name_id) { + class_names_[class_id] = class_name_id; + } + + std::optional Heap::GetClassNameId(object_id_t class_id) const { + try { + return class_names_.at(class_id); + } catch (const std::out_of_range &) { + return std::nullopt; + } + } + + std::optional Heap::FindClassByName(const std::string &class_name) const { + const std::optional memorized = find_class_by_name_get_memorized_result(this, + class_name); + if (memorized.has_value()) { + if (memorized.value() == 0) { + return std::nullopt; + } else { + return memorized.value(); + } + } + + const object_id_t class_id = FindClassByNameInternal(class_name); + find_class_by_name_set_memorized_result(this, class_name, class_id); + if (class_id == 0) return std::nullopt; + else return class_id; + } + + object_id_t Heap::FindClassByNameInternal(const std::string &class_name) const { + const string_id_t class_name_id = unwrap(FindStringId(class_name), return 0); + const auto find_result = std::find_if(class_names_.begin(), class_names_.end(), + [&](const auto &pair) { + return pair.second == class_name_id; + }); + return (find_result == class_names_.end()) ? 0 : find_result->first; + } + + std::optional Heap::GetClassName(object_id_t class_id) const { + const string_id_t class_name_id = + unwrap(GetClassNameId(class_id), return std::nullopt); + return GetString(class_name_id); + } + + + // inheritance + + void Heap::AddInheritanceRecord(object_id_t class_id, object_id_t super_class_id) { + inheritance_[class_id] = super_class_id; + } + + std::optional Heap::GetSuperClass(object_id_t class_id) const { + try { + const object_id_t super_class_id = inheritance_.at(class_id); + if (super_class_id == 0) return std::nullopt; + else return super_class_id; + } catch (const std::out_of_range &) { + return std::nullopt; + } + } + + bool Heap::ChildClassOf(object_id_t class_id, object_id_t super_class_id) const { + object_id_t current = class_id; + while (true) { + if (current == super_class_id) { + return true; + } + current = unwrap(GetSuperClass(current), return false); + } + } + + + // instance field + + void Heap::AddInstanceFieldRecord(object_id_t class_id, field_t field) { + instance_fields_[class_id].emplace_back(field); + } + + const std::vector &Heap::GetInstanceFields(object_id_t class_id) const { + try { + return instance_fields_.at(class_id); + } catch (const std::out_of_range &) { + return empty_fields_; + } + } + + + // instance type + + void Heap::AddInstanceTypeRecord(object_id_t instance_id, object_type_t type) { + instance_types_[instance_id] = type; + } + + object_type_t Heap::GetInstanceType(object_id_t instance_id) const { + try { + return instance_types_.at(instance_id); + } catch (const std::out_of_range &) { + pub_fatal("find instance type failed"); + } + } + + + // instance class + + void Heap::AddInstanceClassRecord(object_id_t instance_id, object_id_t class_id) { + instance_classes_[instance_id] = class_id; + } + + std::optional Heap::GetClass(object_id_t instance_id) const { + try { + return instance_classes_.at(instance_id); + } catch (const std::out_of_range &) { + return std::nullopt; + } + } + + std::vector Heap::GetInstances(object_id_t class_id) const { + std::vector result; + for (const auto &pair: instance_classes_) { + if (pair.second == class_id) { + result.push_back(pair.first); + } + } + return std::move(result); + } + + bool Heap::InstanceOf(object_id_t instance_id, object_id_t class_id) const { + object_id_t instance_class_id = unwrap(GetClass(instance_id), return false); + return ChildClassOf(instance_class_id, class_id); + } + + // GC root + + void Heap::MarkGcRoot(object_id_t instance_id, gc_root_type_t type) { + gc_roots_.emplace_back(instance_id); + gc_root_types_[instance_id] = type; + } + + gc_root_type_t Heap::GetGcRootType(object_id_t gc_root) const { + try { + return gc_root_types_.at(gc_root); + } catch (const std::out_of_range &) { + pub_fatal("find GC root type failed"); + } + } + + const std::vector &Heap::GetGcRoots() const { + return gc_roots_; + } + + + // thread + + void Heap::AddThreadReferenceRecord(object_id_t instance_id, + thread_serial_number_t thread_serial_number) { + thread_serial_numbers_[instance_id] = thread_serial_number; + } + + thread_serial_number_t Heap::GetThreadReference(object_id_t instance_id) const { + try { + return thread_serial_numbers_.at(instance_id); + } catch (const std::out_of_range &) { + pub_fatal("find thread serial number failed"); + } + } + + void Heap::AddThreadObjectRecord(object_id_t thread_object_id, + thread_serial_number_t thread_serial_number) { + thread_object_ids_[thread_serial_number] = thread_object_id; + } + + object_id_t Heap::GetThreadObject(thread_serial_number_t thread_serial_number) const { + try { + return thread_object_ids_.at(thread_serial_number); + } catch (const std::out_of_range &) { + pub_fatal("find thread object failed"); + } + } + + // string + + void Heap::AddString(string_id_t string_id, const char *data, size_t length) { + strings_[string_id] = string_t{ + .data = data, + .length = length + }; + } + + std::optional Heap::GetString(string_id_t string_id) const { + try { + const string_t &string_record = strings_.at(string_id); + return std::string{string_record.data, string_record.length}; + } catch (const std::out_of_range &) { + return std::nullopt; + } + } + + std::optional Heap::FindStringId(const std::string &value) const { + const std::optional memorized = find_string_id_get_memorized_result(this, + value); + if (memorized.has_value()) { + if (memorized.value() == 0) { + return std::nullopt; + } else { + return memorized.value(); + } + } + + const string_id_t string_id = FindStringIdInternal(value); + find_string_id_set_memorized_result(this, value, string_id); + if (string_id == 0) return std::nullopt; + else return string_id; + } + + string_id_t Heap::FindStringIdInternal(const std::string &value) const { + const auto find_result = std::find_if(strings_.begin(), strings_.end(), + [&](const auto &pair) { + return GetString(pair.first) == value; + }); + return (find_result == strings_.end()) ? 0 : find_result->first; + } + + + // fields data + + void Heap::ReadFieldsData(object_id_t instance_id, object_id_t class_id, + size_t fields_data_size, + reader::Reader *source_reader) { + fields_data_.emplace(instance_id, HeapFieldsData(instance_id, class_id, fields_data_size, + source_reader)); + } + + const HeapFieldsData *Heap::ScopedGetFieldsData(object_id_t instance_id) const { + try { + return &fields_data_.at(instance_id); + } catch (const std::out_of_range &) { + return nullptr; + } + } + + std::vector Heap::ScopedGetFieldsDataList() const { + std::vector result; + for (const auto &[instance_id, fields_data]: fields_data_) { + result.push_back(&fields_data); + } + return result; + } + + + // primitive + + void Heap::ReadPrimitive(object_id_t referrer_id, string_id_t field_name_id, value_type_t type, + reader::Reader *source_reader) { + primitives_[referrer_id].emplace(field_name_id, HeapPrimitiveData(type, source_reader)); + } + + void Heap::ReadPrimitiveArray(object_id_t primitive_array_object_id, value_type_t type, + size_t data_size, + reader::Reader *source_reader) { + primitive_arrays_.emplace(primitive_array_object_id, + HeapPrimitiveArrayData(type, data_size, source_reader)); + } + + const HeapPrimitiveData * + Heap::ScopedGetPrimitiveData(object_id_t instance_id, string_id_t field_name_id) const { + try { + return &primitives_.at(instance_id).at(field_name_id); + } catch (const std::out_of_range &) { + return nullptr; + } + } + + const HeapPrimitiveArrayData * + Heap::ScopedGetPrimitiveArrayData(object_id_t primitive_array_object_id) const { + try { + return &primitive_arrays_.at(primitive_array_object_id); + } catch (const std::out_of_range &) { + return nullptr; + } + } + + std::optional + Heap::GetFieldPrimitiveRaw(object_id_t referrer_id, const std::string &reference_name) const { + const string_id_t reference_name_id = unwrap(FindStringId(reference_name), + return std::nullopt); + const heap::HeapPrimitiveData *data = ScopedGetPrimitiveData(referrer_id, + reference_name_id); + if (data == nullptr) return std::nullopt; + return data->GetValue(); + } + + std::optional> + Heap::GetArrayPrimitiveRaw(object_id_t primitive_array_id) const { + const heap::HeapPrimitiveArrayData *data_list = ScopedGetPrimitiveArrayData( + primitive_array_id); + if (data_list == nullptr) return std::nullopt; + std::vector result; + + reader::Reader reader(data_list->GetData(), data_list->GetSize()); + for (size_t i = 0; i < data_list->GetLength(); ++i) { + HeapPrimitiveData data(data_list->GetType(), &reader); + result.emplace_back(data.GetValue()); + } + return result; + } + + + // reference + + void Heap::AddFieldReference(object_id_t referrer_id, string_id_t field_name_id, + object_id_t referent_id, + bool static_reference) { + AddReference(referrer_id, + reference_t{ + .type = static_reference ? kStaticField : kInstanceField, + .field_name_id = field_name_id}, + referent_id); + } + + void Heap::AddFieldExcludedReference(object_id_t referrer_id, string_id_t field_name_id, + object_id_t referent_id, + bool static_reference) { + AddExcludedReference(referrer_id, + reference_t{ + .type = static_reference ? kStaticField : kInstanceField, + .field_name_id = field_name_id}, + referent_id); + } + + std::optional + Heap::GetFieldReference(object_id_t referrer_id, const std::string &reference_name, + bool with_excluded) const { + const string_id_t reference_name_id = unwrap(FindStringId(reference_name), + return std::nullopt); + try { + for (const auto &pair: references_.at(referrer_id)) { + if (pair.second.field_name_id == reference_name_id) { + return pair.first; + } + } + } catch (const std::out_of_range &) {} + + try { + if (with_excluded) { + for (const auto &pair: excluded_references_.at(referrer_id)) { + if (pair.second.field_name_id == reference_name_id) { + return pair.first; + } + } + } + } catch (const std::out_of_range &) {} + + return std::nullopt; + } + + void Heap::AddArrayReference(object_id_t referrer_id, size_t index, object_id_t referent_id) { + AddReference(referrer_id, reference_t{.type = kArrayElement, .index = index}, referent_id); + } + + std::optional + Heap::GetArrayReference(object_id_t referrer_id, size_t index) const { + try { + for (const auto &pair: references_.at(referrer_id)) { + if (pair.second.index == index) { + return pair.first; + } + } + return std::nullopt; + } catch (const std::out_of_range &) { + return std::nullopt; + } + } + + void Heap::ExcludeReferences(object_id_t referrer_id) { + try { + for (const auto &pair: references_.at(referrer_id)) { + AddExcludedReference(referrer_id, pair.second, pair.first); + } + references_.erase(referrer_id); + } catch (const std::out_of_range &) {} + } + + const std::map> & + Heap::GetLeakReferenceGraph() const { + return references_; + } + + void + Heap::AddReference(object_id_t referrer_id, reference_t reference, object_id_t referent_id) { + references_[referrer_id][referent_id] = reference; + } + + void Heap::AddExcludedReference(object_id_t referrer_id, reference_t reference, + object_id_t referent_id) { + excluded_references_[referrer_id][referent_id] = reference; + } + + + // tools + + static std::wstring_convert, char16_t> converter; + + /** + * Content of JVM string is encoded in UTF-16 that is two bytes per character. The function converts it to an + * std::string instance. + */ + static std::string convert_utf_16_string(const uint8_t *buffer, size_t buffer_size) { + std::vector temp; + reader::Reader reader(buffer, buffer_size); + const size_t length = buffer_size / sizeof(char16_t); + for (size_t i = 0; i < length; ++i) { + temp.emplace_back(reader.Read(sizeof(char16_t))); + } + return converter.to_bytes(std::u16string(temp.data(), temp.size())); + } + + std::optional + Heap::GetValueFromStringInstance(object_id_t instance_id) const { + const object_id_t string_class_id = unwrap(FindClassByName("java.lang.String"), + return std::nullopt); + if (!InstanceOf(instance_id, string_class_id)) return std::nullopt; + + const object_id_t string_content_array = unwrap(GetFieldReference(instance_id, "value"), + return std::nullopt); + const heap::HeapPrimitiveArrayData *string_content_array_data = + ScopedGetPrimitiveArrayData(string_content_array); + if (string_content_array_data == nullptr) return std::nullopt; + if (string_content_array_data->GetType() == heap::value_type_t::kChar) { + const size_t offset = + GetFieldPrimitive(instance_id, "offset").value_or(0) * + heap::get_value_type_size(heap::value_type_t::kChar); + const size_t size = + std::min( + GetFieldPrimitive(instance_id, "count").value_or(0) * + heap::get_value_type_size(heap::value_type_t::kChar), + string_content_array_data->GetSize() - offset + ); + return convert_utf_16_string(string_content_array_data->GetData() + offset, size); + } else if (string_content_array_data->GetType() == heap::value_type_t::kByte) { + return std::string( + reinterpret_cast(string_content_array_data->GetData()), + string_content_array_data->GetSize()); + } else { + pub_fatal("unexpected array type for field java.lang.String.value"); + } + } + + const std::vector Heap::empty_fields_ = {}; +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/include/heap.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/include/heap.h new file mode 100644 index 000000000..080fc5638 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/include/heap.h @@ -0,0 +1,416 @@ +#ifndef __matrix_hprof_analyzer_heap_h__ +#define __matrix_hprof_analyzer_heap_h__ + +#include +#include +#include +#include +#include +#include +#include + +#include "macro.h" +#include "reader.h" + +namespace matrix::hprof::internal::heap { + + typedef uint64_t object_id_t; + typedef uint64_t string_id_t; + + typedef uint32_t thread_serial_number_t; + + enum class value_type_t : uint8_t { + kObject = 2, + kBoolean = 4, + kChar = 5, + kFloat = 6, + kDouble = 7, + kByte = 8, + kShort = 9, + kInt = 10, + kLong = 11, + }; + + size_t get_value_type_size(value_type_t type); + + value_type_t value_type_cast(uint8_t type); + + enum class gc_root_type_t { + kRootJniGlobal, + kRootJniLocal, + kRootJavaFrame, + kRootNativeStack, + kRootStickyClass, + kRootThreadBlock, + kRootMonitorUsed, + kRootThreadObject, + kRootInternedString, + kRootFinalizing, + kRootDebugger, + kRootReferenceCleanup, + kRootVMInternal, + kRootJniMonitor, + kRootUnknown, + kRootUnreachable + }; + + enum class object_type_t { + kClass, + kObjectArray, + kPrimitiveArray, + kInstance + }; + + enum reference_type_t { + kStaticField, + kInstanceField, + kArrayElement + }; + + struct string_t { + const char *data; + size_t length; + }; + + struct reference_t { + reference_type_t type; + size_t index; + string_id_t field_name_id; + }; + + struct field_t { + string_id_t name_id; + value_type_t type; + + bool operator==(const field_t& field) const { + return field.name_id == name_id && field.type == type; + } + }; + + class HeapFieldsData { + public: + HeapFieldsData(object_id_t instance_id, + object_id_t class_id, + size_t fields_data_size, + reader::Reader *source_reader); + + [[nodiscard]] object_id_t GetInstanceId() const { + return instance_id_; + } + + [[nodiscard]] object_id_t GetClassId() const { + return class_id_; + } + + [[nodiscard]] size_t GetSize() const { + return fields_data_size_; + } + + [[nodiscard]] reader::Reader GetScopeReader() const { + return {fields_data_, fields_data_size_}; + } + + private: + const object_id_t instance_id_; + const object_id_t class_id_; + const size_t fields_data_size_; + const uint8_t *fields_data_; + }; + + class HeapPrimitiveData { + public: + HeapPrimitiveData(value_type_t type, reader::Reader *source_reader); + + [[nodiscard]] value_type_t GetType() const { + return type_; + } + + template + [[nodiscard]] T GetValue() const { + return *reinterpret_cast(&data_); + } + + private: + const value_type_t type_; + const uint64_t data_; + }; + + class HeapPrimitiveArrayData { + public: + HeapPrimitiveArrayData(value_type_t type, size_t data_size, reader::Reader *source_reader); + + [[nodiscard]] value_type_t GetType() const { + return type_; + } + + [[nodiscard]] size_t GetSize() const { + return data_size_; + } + + [[nodiscard]] size_t GetLength() const { + return data_size_ / get_value_type_size(type_); + } + + [[nodiscard]] const uint8_t *GetData() const { + return data_; + } + + private: + const value_type_t type_; + const size_t data_size_; + const uint8_t *data_; + }; + + /** + * Noticed that all APIs which have the prefix "Find" are not side-effect free. For performance requirements, these + * functions will memorize the return value and return it again if the same arguments are passed. Though they are + * marked as const, the state of framework still be changed and memory increases until the analyzing is completed. + */ + class Heap { + public: + ~Heap(); + + // identifier size + + public: + void InitializeIdSize(size_t id_size); + + [[nodiscard]] size_t GetIdSize() const; + + private: + size_t id_size_ = 0; + + + // class name + + public: + void AddClassNameRecord(object_id_t class_id, string_id_t class_name_id); + + [[nodiscard]] std::optional GetClassNameId(object_id_t class_id) const; + + [[nodiscard]] std::optional + FindClassByName(const std::string &class_name) const; + + [[nodiscard]] std::optional + GetClassName(object_id_t class_id) const; + + private: + [[nodiscard]] object_id_t FindClassByNameInternal(const std::string &name) const; + + std::map class_names_; + + + // inheritance + + public: + void AddInheritanceRecord(object_id_t class_id, object_id_t super_class_id); + + [[nodiscard]] std::optional GetSuperClass(object_id_t class_id) const; + + [[nodiscard]] bool ChildClassOf(object_id_t class_id, object_id_t super_class_id) const; + + private: + std::map inheritance_; + + + // instance field + + public: + void AddInstanceFieldRecord(object_id_t class_id, field_t field); + + [[nodiscard]] const std::vector &GetInstanceFields(object_id_t class_id) const; + + private: + std::map> instance_fields_; + + + // instance type + + public: + void AddInstanceTypeRecord(object_id_t instance_id, object_type_t type); + + [[nodiscard]] object_type_t GetInstanceType(object_id_t instance_id) const; + + private: + std::map instance_types_; + + + // instance class + + public: + void AddInstanceClassRecord(object_id_t instance_id, object_id_t class_id); + + [[nodiscard]] std::optional GetClass(object_id_t instance_id) const; + + [[nodiscard]] std::vector GetInstances(object_id_t class_id) const; + + [[nodiscard]] bool InstanceOf(object_id_t instance_id, object_id_t class_id) const; + + private: + std::map instance_classes_; + + + // GC root + + public: + void MarkGcRoot(object_id_t instance_id, gc_root_type_t type); + + [[nodiscard]] gc_root_type_t GetGcRootType(object_id_t gc_root) const; + + [[nodiscard]] const std::vector &GetGcRoots() const; + + private: + std::vector gc_roots_; + std::map gc_root_types_; + + + // thread + + public: + void AddThreadReferenceRecord(object_id_t instance_id, thread_serial_number_t thread_serial_number); + + [[nodiscard]] thread_serial_number_t GetThreadReference(object_id_t instance_id) const; + + void AddThreadObjectRecord(object_id_t thread_object_id, thread_serial_number_t thread_serial_number); + + [[nodiscard]] object_id_t GetThreadObject(thread_serial_number_t thread_serial_number) const; + + private: + std::map thread_serial_numbers_; + std::map thread_object_ids_; + + + // string + + public: + void AddString(string_id_t string_id, const char *data) { + AddString(string_id, data, strlen(data)); + } + + void AddString(string_id_t string_id, const char *data, size_t length); + + [[nodiscard]] std::optional GetString(string_id_t string_id) const; + + [[nodiscard]] std::optional FindStringId(const std::string &value) const; + + private: + [[nodiscard]] string_id_t FindStringIdInternal(const std::string &value) const; + + std::map strings_; + + + // fields data + + public: + void ReadFieldsData(object_id_t instance_id, object_id_t class_id, + size_t fields_data_size, reader::Reader *source_reader); + + [[nodiscard]] const HeapFieldsData *ScopedGetFieldsData(object_id_t instance_id) const; + + [[nodiscard]] std::vector ScopedGetFieldsDataList() const; + + private: + std::map fields_data_; + + + // primitive + + public: + void ReadPrimitive(object_id_t referrer_id, string_id_t field_name_id, + value_type_t type, reader::Reader *source_reader); + + void ReadPrimitiveArray(object_id_t primitive_array_object_id, + value_type_t type, size_t data_size, reader::Reader *source_reader); + + [[nodiscard]] const HeapPrimitiveData * + ScopedGetPrimitiveData(object_id_t instance_id, string_id_t field_name_id) const; + + [[nodiscard]] const HeapPrimitiveArrayData * + ScopedGetPrimitiveArrayData(object_id_t primitive_array_object_id) const; + + template + [[nodiscard]] std::optional + GetFieldPrimitive(object_id_t referrer_id, const std::string &reference_name) const { + const uint64_t data = unwrap(GetFieldPrimitiveRaw(referrer_id, reference_name), return std::nullopt); + return *reinterpret_cast(&data); + } + + [[nodiscard]] std::optional + GetFieldPrimitiveRaw(object_id_t referrer_id, const std::string &reference_name) const; + + template + [[nodiscard]] std::optional> + GetArrayPrimitive(object_id_t primitive_array_id) const { + const std::vector data_list = unwrap(GetArrayPrimitiveRaw(primitive_array_id), + return std::nullopt); + std::vector result; + for (const auto data: data_list) { + result.template emplace_back(*reinterpret_cast(&data)); + } + return result; + } + + [[nodiscard]] std::optional> + GetArrayPrimitiveRaw(object_id_t primitive_array_id) const; + + private: + std::map> primitives_; + std::map primitive_arrays_; + + + // reference + + public: + void AddFieldReference(object_id_t referrer_id, string_id_t field_name_id, object_id_t referent_id, + bool static_reference = false); + + /** + * See ::AddExcludedReference. + */ + void AddFieldExcludedReference(object_id_t referrer_id, string_id_t field_name_id, object_id_t referent_id, + bool static_reference = false); + + [[nodiscard]] std::optional + GetFieldReference(object_id_t referrer_id, const std::string &reference_name, bool with_excluded = false) const; + + void AddArrayReference(object_id_t referrer_id, size_t index, object_id_t referent_id); + + [[nodiscard]] std::optional + GetArrayReference(object_id_t referrer_id, size_t index) const; + + void ExcludeReferences(object_id_t referrer_id); + + /** + * The directed graph of references not being excluded. The analyzer can ignore exclude reference matchers and + * calculate shortest path with graph directly for increasing performance. + */ + [[nodiscard]] const std::map> &GetLeakReferenceGraph() const; + + private: + void AddReference(object_id_t referrer_id, reference_t reference, object_id_t referent_id); + + /** + * Add a reference which is matched by exclude matchers while parsing heap. The reference will not be added to + * leak reference graph, but it can still be searched by ::GetFieldReference if \a with_excluded is true so that + * API caller can use the whole reference graph to find what they want. + */ + void AddExcludedReference(object_id_t referrer_id, reference_t reference, object_id_t referent_id); + + std::map> references_; + std::map> excluded_references_; + + + // tools + + public: + + [[nodiscard]] std::optional + GetValueFromStringInstance(object_id_t instance_id) const; + + private: + + static const std::vector empty_fields_; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/primitive.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/primitive.cpp new file mode 100644 index 000000000..27d71853f --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/heap/primitive.cpp @@ -0,0 +1,41 @@ +#include "errorha.h" +#include "include/heap.h" + +namespace matrix::hprof::internal::heap { + HeapPrimitiveData::HeapPrimitiveData(value_type_t type, reader::Reader *source_reader) : + type_(({ + if (type == value_type_t::kObject) { + pub_fatal("value type is not primitive"); + } + type; + })), + data_(({ + uint64_t data = 0; + switch (get_value_type_size(type)) { + case sizeof(uint8_t): + *reinterpret_cast(&data) = source_reader->ReadU1(); + break; + case sizeof(uint16_t): + *reinterpret_cast(&data) = source_reader->ReadU2(); + break; + case sizeof(uint32_t): + *reinterpret_cast(&data) = source_reader->ReadU4(); + break; + case sizeof(uint64_t): + *reinterpret_cast(&data) = source_reader->ReadU8(); + break; + } + data; + })) {} + + HeapPrimitiveArrayData::HeapPrimitiveArrayData(value_type_t type, size_t data_size, + reader::Reader *source_reader) : + type_(({ + if (type == value_type_t::kObject) { + pub_fatal("array element type is not primitive"); + } + type; + })), + data_size_(data_size), + data_(reinterpret_cast(source_reader->Extract(data_size))) {} +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/main/analyzer.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/analyzer.cpp new file mode 100644 index 000000000..acf9f8f6f --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/analyzer.cpp @@ -0,0 +1,183 @@ +#include "internal/main_analyzer.h" +#include "internal/main_chain.h" +#include "internal/main_heap.h" + +#include +#include +#include +#include +#include + +#include "analyzer.h" +#include "errorha.h" + +namespace matrix::hprof { + + // public interface + + error_listener_t HprofAnalyzer::SetErrorListener(error_listener_t listener) { + return set_matrix_hprof_analyzer_error_listener(listener); + } + + HprofAnalyzer::HprofAnalyzer(int hprof_fd) : impl_(HprofAnalyzerImpl::Create(hprof_fd)) {} + + HprofAnalyzer::~HprofAnalyzer() = default; + + void HprofAnalyzer::ExcludeInstanceFieldReference(const std::string &class_name, + const std::string &field_name) { + if (impl_ != nullptr) { + impl_->ExcludeInstanceFieldReference(class_name, field_name); + } + } + + void HprofAnalyzer::ExcludeStaticFieldReference(const std::string &class_name, + const std::string &field_name) { + if (impl_ != nullptr) { + impl_->ExcludeStaticFieldReference(class_name, field_name); + } + } + + void HprofAnalyzer::ExcludeThreadReference(const std::string &thread_name) { + if (impl_ != nullptr) { + impl_->ExcludeThreadReference(thread_name); + } + } + + void HprofAnalyzer::ExcludeNativeGlobalReference(const std::string &class_name) { + if (impl_ != nullptr) { + impl_->ExcludeNativeGlobalReference(class_name); + } + } + + std::optional> + HprofAnalyzer::Analyze( + const std::function(const HprofHeap &)> &leak_finder) { + if (impl_ != nullptr) { + return impl_->Analyze(leak_finder); + } else { + return std::nullopt; + } + } + + // implementation + + std::unique_ptr HprofAnalyzerImpl::Create(int hprof_fd) { + struct stat file_stat{}; + if (fstat(hprof_fd, &file_stat)) { + std::stringstream error; + error << "invoke fstat() failed on HPROF with errno " << errno; + pub_error(error.str()); + return nullptr; + } + if (!S_ISREG(file_stat.st_mode)) { + pub_error("file descriptor is not pointed to a regular file"); + return nullptr; + } + const size_t data_size = file_stat.st_size; + if (data_size == 0) { + pub_error("empty HPROF"); + return nullptr; + } + void *data = mmap(nullptr, data_size, PROT_READ, MAP_PRIVATE, hprof_fd, 0); + if (data == MAP_FAILED) { + std::stringstream error_builder; + error_builder << "invoke mmap() failed on HPROF with errno " << errno; + pub_error(error_builder.str()); + return nullptr; + } + return std::make_unique(data, data_size); + } + + HprofAnalyzerImpl::HprofAnalyzerImpl(void *data, size_t data_size) : + data_(data), + data_size_(data_size), + parser_(new internal::parser::HeapParser()), + exclude_matcher_group_() {} + + HprofAnalyzerImpl::~HprofAnalyzerImpl() { + munmap(data_, data_size_); + } + + void HprofAnalyzerImpl::ExcludeInstanceFieldReference(const std::string &class_name, + const std::string &field_name) { + exclude_matcher_group_.instance_fields_.emplace_back( + internal::parser::FieldExcludeMatcher(class_name, field_name)); + } + + void HprofAnalyzerImpl::ExcludeStaticFieldReference(const std::string &class_name, + const std::string &field_name) { + exclude_matcher_group_.static_fields_.emplace_back( + internal::parser::FieldExcludeMatcher(class_name, field_name)); + } + + void HprofAnalyzerImpl::ExcludeThreadReference(const std::string &thread_name) { + exclude_matcher_group_.threads_.emplace_back( + internal::parser::ThreadExcludeMatcher(thread_name)); + } + + void HprofAnalyzerImpl::ExcludeNativeGlobalReference(const std::string &class_name) { + exclude_matcher_group_.native_globals_.emplace_back( + internal::parser::NativeGlobalExcludeMatcher(class_name)); + } + + std::vector + HprofAnalyzerImpl::Analyze( + const std::function(const HprofHeap &)> &leak_finder) { + internal::heap::Heap heap; + internal::reader::Reader reader(reinterpret_cast(data_), data_size_); + parser_->Parse(reader, heap, exclude_matcher_group_); + return Analyze(heap, leak_finder(HprofHeap(new HprofHeapImpl(heap)))); + } + + std::vector HprofAnalyzerImpl::Analyze(const internal::heap::Heap &heap, + const std::vector &leaks) { + const auto chains = ({ + const HprofHeap hprof_heap(new HprofHeapImpl(heap)); + internal::analyzer::find_leak_chains(heap, leaks); + }); + std::vector result; + for (const auto&[_, chain]: chains) { + const std::optional leak_chain = BuildLeakChain(heap, chain); + if (leak_chain.has_value()) result.emplace_back(leak_chain.value()); + } + return std::move(result); + } + + std::optional HprofAnalyzerImpl::BuildLeakChain(const internal::heap::Heap &heap, + const std::vector>> &chain) { + if (chain.empty()) return std::nullopt; + std::optional gc_root; + std::vector nodes; + internal::heap::reference_t next_reference{}; + for (size_t i = 0; i < chain.size(); ++i) { + const auto &chain_node = chain[i]; + const std::string referent = ({ + const object_id_t class_id = (chain_node.second.has_value() && + chain_node.second.value().type == + internal::heap::kStaticField) + ? chain_node.first + : unwrap(heap.GetClass(chain_node.first), + return std::nullopt); + unwrap(heap.GetClassName(class_id), return std::nullopt); + }); + if (i == 0) { + const LeakChain::GcRoot::Type gc_root_type = + convert_gc_root_type(heap.GetGcRootType(chain_node.first)); + gc_root = create_leak_chain_gc_root(referent, gc_root_type); + } else { + const std::string reference = next_reference.type == internal::heap::kArrayElement + ? std::to_string(next_reference.index) + : heap.GetString( + next_reference.field_name_id).value_or(""); + const LeakChain::Node::ReferenceType reference_type = + convert_reference_type(next_reference.type); + const LeakChain::Node::ObjectType referent_type = + convert_object_type(heap.GetInstanceType(chain_node.first)); + nodes.emplace_back( + create_leak_chain_node(reference, reference_type, referent, referent_type)); + } + next_reference = unwrap(chain_node.second, break); + } + return create_leak_chain(gc_root.value(), nodes); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/main/chain.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/chain.cpp new file mode 100644 index 000000000..4ad883eb1 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/chain.cpp @@ -0,0 +1,130 @@ +#include "internal/main_chain.h" + +namespace matrix::hprof { + + const std::string &LeakChain::GcRoot::GetName() const { + return name_; + } + + LeakChain::GcRoot::Type LeakChain::GcRoot::GetType() const { + return type_; + } + + LeakChain::GcRoot::GcRoot(std::string name, LeakChain::GcRoot::Type type) : + name_(std::move(name)), + type_(type) {} + + const std::string &LeakChain::Node::GetReference() const { + return reference_; + } + + LeakChain::Node::ReferenceType LeakChain::Node::GetReferenceType() const { + return reference_type_; + } + + const std::string &LeakChain::Node::GetObject() const { + return object_; + } + + LeakChain::Node::ObjectType LeakChain::Node::GetObjectType() const { + return object_type_; + } + + LeakChain::Node::Node(std::string reference, Node::ReferenceType reference_type, + std::string object, Node::ObjectType object_type) : + reference_(std::move(reference)), + reference_type_(reference_type), + object_(std::move(object)), + object_type_(object_type) {} + + const LeakChain::GcRoot &LeakChain::GetGcRoot() const { + return gc_root_; + } + + const std::vector &LeakChain::GetNodes() const { + return nodes_; + } + + size_t LeakChain::GetDepth() const { + return depth_; + } + + LeakChain::LeakChain(GcRoot gc_root, std::vector nodes) : + gc_root_(std::move(gc_root)), + nodes_(std::move(nodes)), + depth_(nodes_.size()) {} + + LeakChain::GcRoot create_leak_chain_gc_root(const std::string &name, LeakChain::GcRoot::Type type) { + return {name, type}; + } + + LeakChain::Node create_leak_chain_node(const std::string &reference, LeakChain::Node::ReferenceType reference_type, + const std::string &object, LeakChain::Node::ObjectType object_type) { + return {reference, reference_type, object, object_type}; + } + + LeakChain create_leak_chain(const LeakChain::GcRoot &gc_root, const std::vector &nodes) { + return {gc_root, nodes}; + } + + LeakChain::GcRoot::Type convert_gc_root_type(internal::heap::gc_root_type_t type) { + switch (type) { + case internal::heap::gc_root_type_t::kRootJniGlobal: + return LeakChain::GcRoot::Type::kRootJniGlobal; + case internal::heap::gc_root_type_t::kRootJniLocal: + return LeakChain::GcRoot::Type::kRootJniLocal; + case internal::heap::gc_root_type_t::kRootJavaFrame: + return LeakChain::GcRoot::Type::kRootJavaFrame; + case internal::heap::gc_root_type_t::kRootNativeStack: + return LeakChain::GcRoot::Type::kRootNativeStack; + case internal::heap::gc_root_type_t::kRootStickyClass: + return LeakChain::GcRoot::Type::kRootStickyClass; + case internal::heap::gc_root_type_t::kRootThreadBlock: + return LeakChain::GcRoot::Type::kRootThreadBlock; + case internal::heap::gc_root_type_t::kRootMonitorUsed: + return LeakChain::GcRoot::Type::kRootMonitorUsed; + case internal::heap::gc_root_type_t::kRootThreadObject: + return LeakChain::GcRoot::Type::kRootThreadObject; + case internal::heap::gc_root_type_t::kRootInternedString: + return LeakChain::GcRoot::Type::kRootInternedString; + case internal::heap::gc_root_type_t::kRootFinalizing: + return LeakChain::GcRoot::Type::kRootFinalizing; + case internal::heap::gc_root_type_t::kRootDebugger: + return LeakChain::GcRoot::Type::kRootDebugger; + case internal::heap::gc_root_type_t::kRootReferenceCleanup: + return LeakChain::GcRoot::Type::kRootReferenceCleanup; + case internal::heap::gc_root_type_t::kRootVMInternal: + return LeakChain::GcRoot::Type::kRootVMInternal; + case internal::heap::gc_root_type_t::kRootJniMonitor: + return LeakChain::GcRoot::Type::kRootJniMonitor; + case internal::heap::gc_root_type_t::kRootUnknown: + return LeakChain::GcRoot::Type::kRootUnknown; + case internal::heap::gc_root_type_t::kRootUnreachable: + return LeakChain::GcRoot::Type::kRootUnreachable; + } + } + + LeakChain::Node::ReferenceType convert_reference_type(internal::heap::reference_type_t type) { + switch (type) { + case internal::heap::reference_type_t::kStaticField: + return LeakChain::Node::ReferenceType::kStaticField; + case internal::heap::reference_type_t::kInstanceField: + return LeakChain::Node::ReferenceType::kInstanceField; + case internal::heap::reference_type_t::kArrayElement: + return LeakChain::Node::ReferenceType::kArrayElement; + } + } + + LeakChain::Node::ObjectType convert_object_type(internal::heap::object_type_t type) { + switch (type) { + case internal::heap::object_type_t::kClass: + return LeakChain::Node::ObjectType::kClass; + case internal::heap::object_type_t::kInstance: + return LeakChain::Node::ObjectType::kInstance; + case internal::heap::object_type_t::kObjectArray: + return LeakChain::Node::ObjectType::kObjectArray; + case internal::heap::object_type_t::kPrimitiveArray: + return LeakChain::Node::ObjectType::kPrimitiveArray; + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/main/heap.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/heap.cpp new file mode 100644 index 000000000..8af052c89 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/heap.cpp @@ -0,0 +1,115 @@ +#include "internal/main_heap.h" + +namespace matrix::hprof { + + // public interface + + HprofHeap::HprofHeap(HprofHeapImpl *impl) : impl_(impl) {} + + std::optional HprofHeap::FindClassByName(const std::string &class_name) const { + return impl_->FindClassByName(class_name); + } + + std::optional HprofHeap::GetClassName(object_id_t class_id) const { + return impl_->GetClassName(class_id); + } + + std::optional HprofHeap::GetSuperClass(object_id_t class_id) const { + return impl_->GetSuperClass(class_id); + } + + bool HprofHeap::ChildClassOf(object_id_t class_id, object_id_t super_class_id) const { + return impl_->ChildClassOf(class_id, super_class_id); + } + + std::optional HprofHeap::GetClass(object_id_t instance_id) const { + return impl_->GetClass(instance_id); + } + + std::vector HprofHeap::GetInstances(object_id_t class_id) const { + return impl_->GetInstances(class_id); + } + + bool HprofHeap::InstanceOf(object_id_t instance_id, object_id_t class_id) const { + return impl_->InstanceOf(instance_id, class_id); + } + + std::optional + HprofHeap::GetFieldReference(object_id_t referrer_id, const std::string &field_name) const { + return impl_->GetFieldReference(referrer_id, field_name); + } + + std::optional + HprofHeap::GetArrayReference(object_id_t referrer_id, size_t index) const { + return impl_->GetArrayReference(referrer_id, index); + } + + std::optional + HprofHeap::GetFieldPrimitiveRaw(object_id_t referrer_id, const std::string &reference_name) const { + return impl_->GetFieldPrimitiveRaw(referrer_id, reference_name); + } + + std::optional> + HprofHeap::GetArrayPrimitiveRaw(object_id_t primitive_array_id) const { + return impl_->GetArrayPrimitiveRaw(primitive_array_id); + } + + std::optional HprofHeap::GetValueFromStringInstance(object_id_t instance_id) const { + return impl_->GetValueFromStringInstance(instance_id); + } + + // implementation + + HprofHeapImpl::HprofHeapImpl(const internal::heap::Heap &heap) : heap_(heap) {} + + std::optional HprofHeapImpl::FindClassByName(const std::string &class_name) const { + return heap_.FindClassByName(class_name); + } + + std::optional HprofHeapImpl::GetClassName(object_id_t class_id) const { + return heap_.GetClassName(class_id); + } + + std::optional HprofHeapImpl::GetSuperClass(object_id_t class_id) const { + return heap_.GetSuperClass(class_id); + } + + bool HprofHeapImpl::ChildClassOf(object_id_t class_id, object_id_t super_class_id) const { + return heap_.ChildClassOf(class_id, super_class_id); + } + + std::optional HprofHeapImpl::GetClass(object_id_t instance_id) const { + return heap_.GetClass(instance_id); + } + + std::vector HprofHeapImpl::GetInstances(object_id_t class_id) const { + return heap_.GetInstances(class_id); + } + + bool HprofHeapImpl::InstanceOf(object_id_t instance_id, object_id_t class_id) const { + return heap_.InstanceOf(instance_id, class_id); + } + + std::optional HprofHeapImpl::GetFieldReference(object_id_t referrer_id, + const std::string &field_name) const { + return heap_.GetFieldReference(referrer_id, field_name, true); + } + + std::optional HprofHeapImpl::GetArrayReference(object_id_t referrer_id, size_t index) const { + return heap_.GetArrayReference(referrer_id, index); + } + + std::optional + HprofHeapImpl::GetFieldPrimitiveRaw(object_id_t referrer_id, const std::string &reference_name) const { + return heap_.GetFieldPrimitiveRaw(referrer_id, reference_name); + } + + std::optional> + HprofHeapImpl::GetArrayPrimitiveRaw(object_id_t primitive_array_id) const { + return heap_.GetArrayPrimitiveRaw(primitive_array_id); + } + + std::optional HprofHeapImpl::GetValueFromStringInstance(object_id_t instance_id) const { + return heap_.GetValueFromStringInstance(instance_id); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/main/include/matrix_hprof_analyzer.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/include/matrix_hprof_analyzer.h new file mode 100644 index 000000000..28fd86bca --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/include/matrix_hprof_analyzer.h @@ -0,0 +1,363 @@ +#ifndef __matrix_hprof_analyzer_h__ +#define __matrix_hprof_analyzer_h__ + +#include +#include +#include +#include + +#include "macro.h" + +namespace matrix::hprof { + + typedef uint64_t object_id_t; + + typedef void (*error_listener_t)(const char *); + + class HprofHeapImpl; + + /** + * The class which instances are used obtain relations between objects in the heap. + */ + class HprofHeap { + public: + /** + * Gets class identifier in HPROF file with corresponding class name \a class_name, or returns std::nullopt if + * the record is not found. + */ + [[nodiscard]] std::optional + FindClassByName(const std::string &class_name) const; + + /** + * Gets class name with class identifier in HPROF file \a class_id, or returns std::nullopt if the record is not + * found. + */ + [[nodiscard]] std::optional + GetClassName(object_id_t class_id) const; + + /** + * Gets super class identifier in HPROF file of class which identifier is \a class_id, or returns std::nullopt + * if the record is not found. + */ + [[nodiscard]] std::optional + GetSuperClass(object_id_t class_id) const; + + /** + * Checks whether the class with identifier \a class_id is a child class of class with identifier + * \a super_class_id. + */ + [[nodiscard]] bool ChildClassOf(object_id_t class_id, object_id_t super_class_id) const; + + /** + * Gets class identifier in HPROF file which the object with \a instance_id is its instance, or returns + * std::nullopt if the record is not found. + */ + [[nodiscard]] std::optional + GetClass(object_id_t instance_id) const; + + /** + * Gets all instance identifiers in HPROF file of class with id \a class_id. + */ + [[nodiscard]] std::vector + GetInstances(object_id_t class_id) const; + + /** + * Checks whether the object with identifier \a instance_id is the instance of class with identifier + * \a class_id. + */ + [[nodiscard]] bool InstanceOf(object_id_t instance_id, object_id_t class_id) const; + + + public: + /** + * Gets object identifier in HPROF file which is referred by the field named \a field_name of the object with + * identifier \a referrer_id, or returns std::nullopt if the record is not found. + */ + [[nodiscard]] std::optional + GetFieldReference(object_id_t referrer_id, const std::string &field_name) const; + + /** + * Gets object identifier in HPROF file which is referred as an array element indexed \a index of the array with + * identifier \a referrer_id, or returns std::nullopt if the record is not found. + */ + [[nodiscard]] std::optional + GetArrayReference(object_id_t referrer_id, size_t index) const; + + /** + * Gets primitive value of the field named \a field_name of the object with identifier \a referrer_id and + * converts as type \a T, or returns std::nullopt if the record is not found. + */ + template + [[nodiscard]] std::optional + GetFieldPrimitive(object_id_t referrer_id, const std::string &reference_name) const { + const uint64_t data = unwrap(GetFieldPrimitiveRaw(referrer_id, reference_name), + return std::nullopt); + return *reinterpret_cast(&data); + } + + private: + [[nodiscard]] std::optional + GetFieldPrimitiveRaw(object_id_t referrer_id, const std::string &reference_name) const; + + + public: + /** + * Gets primitive value which as an array element indexed \a index of the array with identifier \a referrer_id, + * or returns std::nullopt if the record is not found. + */ + template + [[nodiscard]] std::optional> + GetArrayPrimitive(object_id_t primitive_array_id) const { + const std::vector data_list = unwrap(GetArrayPrimitiveRaw(primitive_array_id), + return std::nullopt); + std::vector result; + for (const auto data: data_list) { + result.template emplace_back(*reinterpret_cast(&data)); + } + return result; + } + + private: + [[nodiscard]] std::optional> + GetArrayPrimitiveRaw(object_id_t primitive_array_id) const; + + + public: + /** + * Gets string content from java.lang.String object with identifier \a instance_id, or returns std::nullopt if + * the record is not found or the object is not a java.lang.String instance. + */ + [[nodiscard]] std::optional + GetValueFromStringInstance(object_id_t instance_id) const; + + private: + friend class HprofAnalyzerImpl; + + explicit HprofHeap(HprofHeapImpl *impl); + + friend_test(main_heap, delegate); + + std::unique_ptr impl_; + }; + + /** + * Shortest path from GC root to the leaking object. + */ + class LeakChain { + public: + /** + * The nearest GC root to the chain leaking object. + */ + class GcRoot { + public: + /** + * GC root type. + */ + enum class Type { + kRootJniGlobal, + kRootJniLocal, + kRootJavaFrame, + kRootNativeStack, + kRootStickyClass, + kRootThreadBlock, + kRootMonitorUsed, + kRootThreadObject, + kRootInternedString, + kRootFinalizing, + kRootDebugger, + kRootReferenceCleanup, + kRootVMInternal, + kRootJniMonitor, + kRootUnknown, + kRootUnreachable, + }; + + /** + * Gets GC root class name. + *

+ * The function returns class name of the GC root object, or self class name of root if GC root is an class + * and refer next node as static field. + */ + [[nodiscard]] const std::string &GetName() const; + + /** + * Gets GC root type. + */ + [[nodiscard]] Type GetType() const; + + private: + friend GcRoot create_leak_chain_gc_root(const std::string &name, Type type); + + GcRoot(std::string name, Type type); + + std::string name_; + Type type_; + }; + + /** + * Node of the leaking chain. + */ + class Node { + public: + /** + * Reference type. + */ + enum class ReferenceType { + kStaticField, + kInstanceField, + kArrayElement + }; + + /** + * Referent object type. + */ + enum class ObjectType { + kClass, + kInstance, + kObjectArray, + kPrimitiveArray + }; + + /** + * Gets name of reference which the previous node used to referring current node object. + *

+ * The function returns field name if the previous node is referring current node as instance or static + * field, or array index if current node is an element of previous node. + */ + [[nodiscard]] const std::string &GetReference() const; + + /** + * Get referent type. + */ + [[nodiscard]] ReferenceType GetReferenceType() const; + + /** + * Gets node object class name. + *

+ * The function returns class name of the node object, or self class name of root if node object is an class + * and refer next node as static field. + */ + [[nodiscard]] const std::string &GetObject() const; + + /** + * Gets node object type. + */ + [[nodiscard]] ObjectType GetObjectType() const; + + private: + friend Node + create_leak_chain_node(const std::string &reference, Node::ReferenceType reference_type, + const std::string &object, Node::ObjectType object_type); + + Node(std::string reference, Node::ReferenceType reference_type, + std::string object, Node::ObjectType object_type); + + std::string reference_; + ReferenceType reference_type_; + std::string object_; + ObjectType object_type_; + }; + + /** + * Gets GC root info of leak chain. + */ + [[nodiscard]] const GcRoot &GetGcRoot() const; + + /** + * Gets chain nodes of leak chain. The last node of the returned vector is the leaking object, or function + * returns empty vector if GC root is the tracked "leaking" object. + */ + [[nodiscard]] const std::vector &GetNodes() const; + + /** + * Gets number of hops from GC root to the leaking object. + */ + [[nodiscard]] size_t GetDepth() const; + + private: + friend LeakChain create_leak_chain(const GcRoot &gc_root, const std::vector &nodes); + + LeakChain(GcRoot gc_root, std::vector nodes); + + GcRoot gc_root_; + std::vector nodes_; + size_t depth_; + }; + + class HprofAnalyzerImpl; + + /** + * HPROF file analyzer. + *

+ * The class used to find shortest leaking path of leaking objects. + */ + class HprofAnalyzer { + public: + + /** + * Listens error message if the analyzing errors or failures happen. The function returns the origin listener + * pointer. + */ + static error_listener_t SetErrorListener(error_listener_t listener); + + explicit HprofAnalyzer(int hprof_fd); + + ~HprofAnalyzer(); + + /** + * Exclude references referred as instance field \a field_name of class \a class_name instance. + *

+ * Use "*" to match all. + */ + void + ExcludeInstanceFieldReference(const std::string &class_name, const std::string &field_name); + + /** + * Exclude references referred as static field \a field_name of class \a class_name. + *

+ * Use "*" to match all. + */ + void + ExcludeStaticFieldReference(const std::string &class_name, const std::string &field_name); + + /** + * Exclude references referred from stack frame of thread \a thread_name. + *

+ * Use "*" to match all. + */ + void ExcludeThreadReference(const std::string &thread_name); + + /** + * Exclude native global references. + *

+ * Use "*" to match all. + */ + void ExcludeNativeGlobalReference(const std::string &class_name); + + /** + * Start analyzing the HPROF file. + *

+ * The function \a leak_finder should returns leaks found from HprofHeap which are wanted to be tracked, The + * function will returns the same amount of LeakChain instances as leaks, each of which corresponds to a leak, + * or amount of LeakChain instances is less than leaks count when there is a "leak" did not being referred by + * GC roots. + *

+ * The function returns std::nullopt if analyzes failed. + */ + std::optional> + Analyze(const std::function(const HprofHeap &)> &leak_finder); + + private: + friend_test(main_analyzer, construct); + + friend_test(main_analyzer, construct_error_handle); + + friend_test(main_analyzer, exclude_matchers); + + friend_test(main_analyzer, analyze); + + std::unique_ptr impl_; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_analyzer.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_analyzer.h new file mode 100644 index 000000000..5ff885b48 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_analyzer.h @@ -0,0 +1,50 @@ +#ifndef __matrix_hprof_analyzer_internal_analyzer_h__ +#define __matrix_hprof_analyzer_internal_analyzer_h__ + +#include "../include/matrix_hprof_analyzer.h" + +#include +#include +#include + +#include "macro.h" + +#include "heap.h" +#include "parser.h" + +namespace matrix::hprof { + + class HprofAnalyzerImpl { + public: + static std::unique_ptr Create(int hprof_fd); + + HprofAnalyzerImpl(void *data, size_t data_size); + + ~HprofAnalyzerImpl(); + + void ExcludeInstanceFieldReference(const std::string &class_name, const std::string &field_name); + + void ExcludeStaticFieldReference(const std::string &class_name, const std::string &field_name); + + void ExcludeThreadReference(const std::string &thread_name); + + void ExcludeNativeGlobalReference(const std::string &class_name); + + std::vector + Analyze(const std::function(const HprofHeap &)> &leak_finder); + + static std::vector + Analyze(const internal::heap::Heap &heap, const std::vector &leaks); + + static std::optional + BuildLeakChain(const internal::heap::Heap &heap, const std::vector>> &chain); + + void *data_{}; + size_t data_size_{}; + const std::unique_ptr parser_; + internal::parser::ExcludeMatcherGroup exclude_matcher_group_; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_chain.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_chain.h new file mode 100644 index 000000000..4e8d9baf4 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_chain.h @@ -0,0 +1,17 @@ +#ifndef __matrix_hprof_analyzer_internal_chain_h__ +#define __matrix_hprof_analyzer_internal_chain_h__ + +#include "../include/matrix_hprof_analyzer.h" + +#include "heap.h" + +namespace matrix::hprof { + + LeakChain::GcRoot::Type convert_gc_root_type(internal::heap::gc_root_type_t type); + + LeakChain::Node::ReferenceType convert_reference_type(internal::heap::reference_type_t type); + + LeakChain::Node::ObjectType convert_object_type(internal::heap::object_type_t type); +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_heap.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_heap.h new file mode 100644 index 000000000..7ebcbef82 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/main/internal/main_heap.h @@ -0,0 +1,61 @@ +#ifndef __matrix_hprof_analyzer_internal_heap_h__ +#define __matrix_hprof_analyzer_internal_heap_h__ + +#include "../include/matrix_hprof_analyzer.h" + +#include +#include +#include + +#include "macro.h" + +#include "heap.h" + +namespace matrix::hprof { + + class HprofHeapImpl { + public: + explicit HprofHeapImpl(const internal::heap::Heap &heap); + + [[nodiscard]] std::optional + FindClassByName(const std::string &class_name) const; + + [[nodiscard]] std::optional + GetClassName(object_id_t class_id) const; + + [[nodiscard]] std::optional + GetSuperClass(object_id_t class_id) const; + + [[nodiscard]] bool ChildClassOf(object_id_t class_id, object_id_t super_class_id) const; + + [[nodiscard]] std::optional + GetClass(object_id_t instance_id) const; + + [[nodiscard]] std::vector + GetInstances(object_id_t class_id) const; + + [[nodiscard]] bool InstanceOf(object_id_t instance_id, object_id_t class_id) const; + + [[nodiscard]] std::optional + GetFieldReference(object_id_t referrer_id, const std::string &field_name) const; + + [[nodiscard]] std::optional + GetArrayReference(object_id_t referrer_id, size_t index) const; + + [[nodiscard]] std::optional + GetFieldPrimitiveRaw(object_id_t referrer_id, const std::string &reference_name) const; + + [[nodiscard]] std::optional> + GetArrayPrimitiveRaw(object_id_t primitive_array_id) const; + + [[nodiscard]] std::optional + GetValueFromStringInstance(object_id_t instance_id) const; + + private: + friend_test(main_heap, delegate); + + const internal::heap::Heap &heap_; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/engine.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/engine.cpp new file mode 100644 index 000000000..dd8d6ce3c --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/engine.cpp @@ -0,0 +1,642 @@ +#include "internal/engine.h" +#include "errorha.h" + +#include "macro.h" + +#include + +namespace matrix::hprof::internal::parser { + + namespace tag { + static constexpr uint8_t kStrings = 0x01; + static constexpr uint8_t kLoadClasses = 0x02; + static constexpr uint8_t kHeapDump = 0x0c; + static constexpr uint8_t kHeapDumpSegment = 0x1c; + static constexpr uint8_t kHeapDumpEnd = 0x2c; + + /** + * Traditional heap tag. + */ + + static constexpr uint8_t kHeapRootUnknown = 0xff; + static constexpr uint8_t kHeapRootJniGlobal = 0x01; + static constexpr uint8_t kHeapRootJniLocal = 0x02; + static constexpr uint8_t kHeapRootJavaFrame = 0x03; + static constexpr uint8_t kHeapRootNativeStack = 0x04; + static constexpr uint8_t kHeapRootStickyClass = 0x05; + static constexpr uint8_t kHeapRootThreadBlock = 0x06; + static constexpr uint8_t kHeapRootMonitorUsed = 0x07; + static constexpr uint8_t kHeapRootThreadObject = 0x08; + + static constexpr uint8_t kHeapClassDump = 0x20; + static constexpr uint8_t kHeapInstanceDump = 0x21; + static constexpr uint8_t kHeapObjectArrayDump = 0x22; + static constexpr uint8_t kHeapPrimitiveArrayDump = 0x23; + + /** + * Android additional heap tag. + */ + + static constexpr uint8_t kHeapRootInternedString = 0x89; + static constexpr uint8_t kHeapRootFinalizing = 0x8a; + static constexpr uint8_t kHeapRootDebugger = 0x8b; + static constexpr uint8_t kHeapRootReferenceCleanup = 0x8c; + static constexpr uint8_t kHeapRootVMInternal = 0x8d; + static constexpr uint8_t kHeapRootJniMonitor = 0x8e; + static constexpr uint8_t kHeapRootUnreachable = 0x90; + + static constexpr uint8_t kHeapPrimitiveArrayNoDataDump = 0xc3; + static constexpr uint8_t kHeapDumpInfo = 0xfe; + } + + void + HeapParserEngineImpl::Parse(reader::Reader &reader, heap::Heap &heap, + const ExcludeMatcherGroup &exclude_matcher_group, + const HeapParserEngine &next) const { + next.ParseHeader(reader, heap); + + while (true) { + const uint8_t tag = reader.ReadU1(); + reader.SkipU4(); // Skip timestamp. + const uint32_t length = reader.ReadU4(); + switch (tag) { + case tag::kStrings: + next.ParseStringRecord(reader, heap, length); + break; + case tag::kLoadClasses: + next.ParseLoadClassRecord(reader, heap, length); + break; + case tag::kHeapDump: + case tag::kHeapDumpSegment: + next.ParseHeapContent(reader, heap, length, exclude_matcher_group, next); + break; + case tag::kHeapDumpEnd: + goto break_read_loop; + default: + reader.Skip(length); + break; + } + } + break_read_loop: + next.LazyParse(heap, exclude_matcher_group); + } + + void HeapParserEngineImpl::ParseHeader(reader::Reader &reader, heap::Heap &heap) const { + // Version string. + const std::string version = reader.ReadNullTerminatedString(); + if (version != "JAVA PROFILE 1.0" && + version != "JAVA PROFILE 1.0.1" && + version != "JAVA PROFILE 1.0.2" && + version != "JAVA PROFILE 1.0.3") { + pub_fatal("invalid HPROF header"); + } + // Identifier size. + heap.InitializeIdSize(reader.ReadU4()); + // Skip timestamp. + reader.SkipU8(); + } + + void HeapParserEngineImpl::ParseStringRecord(reader::Reader &reader, heap::Heap &heap, + size_t record_length) const { + const heap::string_id_t string_id = reader.Read(heap.GetIdSize()); + const size_t length = record_length - heap.GetIdSize(); + heap.AddString(string_id, reinterpret_cast(reader.Extract(length)), length); + } + + void + HeapParserEngineImpl::ParseLoadClassRecord(reader::Reader &reader, heap::Heap &heap, + size_t record_length) const { + // Skip class serial number. + reader.SkipU4(); + // Class object ID. + const heap::object_id_t class_id = reader.Read(heap.GetIdSize()); + // Skip stack trace serial number. + reader.SkipU4(); + // Class name string ID. + const heap::string_id_t class_name_id = reader.Read(heap.GetIdSize()); + heap.AddClassNameRecord(class_id, class_name_id); + } + + static std::optional + get_thread_name(const heap::Heap &heap, heap::object_id_t thread_object_id) { + const heap::object_id_t thread_class_id = + unwrap(heap.FindClassByName("java.lang.Thread"), return std::nullopt); + if (!heap.InstanceOf(thread_object_id, thread_class_id)) return std::nullopt; + const heap::object_id_t name_string_id = + unwrap(heap.GetFieldReference(thread_object_id, "name"), return std::nullopt); + return heap.GetValueFromStringInstance(name_string_id); + } + + void HeapParserEngineImpl::ParseHeapContent(reader::Reader &reader, heap::Heap &heap, + size_t record_length, + const ExcludeMatcherGroup &exclude_matcher_group, + const HeapParserEngine &next) const { + size_t bytes_read = 0; + while (bytes_read < record_length) { + const uint8_t tag = reader.ReadU1(); + bytes_read += sizeof(uint8_t); + switch (tag) { + case tag::kHeapRootUnknown: + bytes_read += next.ParseHeapContentRootUnknownSubRecord(reader, heap); + break; + case tag::kHeapRootJniGlobal: + bytes_read += next.ParseHeapContentRootJniGlobalSubRecord(reader, heap); + break; + case tag::kHeapRootJniLocal: + bytes_read += next.ParseHeapContentRootJniLocalSubRecord(reader, heap); + break; + case tag::kHeapRootJavaFrame: + bytes_read += next.ParseHeapContentRootJavaFrameSubRecord(reader, heap); + break; + case tag::kHeapRootNativeStack: + bytes_read += next.ParseHeapContentRootNativeStackSubRecord(reader, heap); + break; + case tag::kHeapRootStickyClass: + bytes_read += next.ParseHeapContentRootStickyClassSubRecord(reader, heap); + break; + case tag::kHeapRootThreadBlock: + bytes_read += next.ParseHeapContentRootThreadBlockSubRecord(reader, heap); + break; + case tag::kHeapRootMonitorUsed: + bytes_read += next.ParseHeapContentRootMonitorUsedSubRecord(reader, heap); + break; + case tag::kHeapRootThreadObject: + bytes_read += next.ParseHeapContentRootThreadObjectSubRecord(reader, heap); + break; + case tag::kHeapRootInternedString: + bytes_read += next.ParseHeapContentRootInternedStringSubRecord(reader, heap); + break; + case tag::kHeapRootFinalizing: + bytes_read += next.ParseHeapContentRootFinalizingSubRecord(reader, heap); + break; + case tag::kHeapRootDebugger: + bytes_read += next.ParseHeapContentRootDebuggerSubRecord(reader, heap); + break; + case tag::kHeapRootReferenceCleanup: + bytes_read += next.ParseHeapContentRootReferenceCleanupSubRecord(reader, heap); + break; + case tag::kHeapRootVMInternal: + bytes_read += next.ParseHeapContentRootVMInternalSubRecord(reader, heap); + break; + case tag::kHeapRootJniMonitor: + bytes_read += next.ParseHeapContentRootJniMonitorSubRecord(reader, heap); + break; + case tag::kHeapRootUnreachable: + bytes_read += next.ParseHeapContentRootUnreachableSubRecord(reader, heap); + break; + case tag::kHeapClassDump: + bytes_read += next.ParseHeapContentClassSubRecord(reader, heap, + exclude_matcher_group); + break; + case tag::kHeapInstanceDump: + bytes_read += next.ParseHeapContentInstanceSubRecord(reader, heap); + break; + case tag::kHeapObjectArrayDump: + bytes_read += next.ParseHeapContentObjectArraySubRecord(reader, heap); + break; + case tag::kHeapPrimitiveArrayDump: + bytes_read += next.ParseHeapContentPrimitiveArraySubRecord(reader, heap); + break; + case tag::kHeapPrimitiveArrayNoDataDump: + bytes_read += next.ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader, + heap); + break; + case tag::kHeapDumpInfo: + bytes_read += next.SkipHeapContentInfoSubRecord(reader, heap); + break; + default: + std::stringstream error_builder; + error_builder << "unsupported heap dump tag " << std::to_string(tag); + pub_fatal(error_builder.str()); + } + } + } + + struct field_exclude_matcher_flatten_t { + const heap::object_id_t class_id; + const heap::string_id_t field_name_id; + }; + + static void + flatten_field_exclude_matchers(std::vector &flatten_matchers, + const heap::Heap &heap, + const std::vector &matchers) { + for (const auto &matcher: matchers) { + flatten_matchers.emplace_back(field_exclude_matcher_flatten_t{ + .class_id = matcher.FullMatchClassName() + ? 0 + : unwrap(heap.FindClassByName(matcher.GetClassName()), continue), + .field_name_id = matcher.FullMatchFieldName() + ? 0 + : unwrap(heap.FindStringId(matcher.GetFieldName()), continue) + }); + } + } + + struct native_global_exclude_matcher_flatten_t { + const heap::string_id_t class_id; + }; + + static void + flatten_native_global_exclude_matchers( + std::vector &flatten_matchers, + const heap::Heap &heap, + const std::vector &matchers) { + for (const auto &matcher: matchers) { + flatten_matchers.emplace_back(native_global_exclude_matcher_flatten_t{ + .class_id = matcher.FullMatchClassName() + ? 0 + : unwrap(heap.FindClassByName(matcher.GetClassName()), continue) + }); + } + } + + void HeapParserEngineImpl::LazyParse(heap::Heap &heap, + const ExcludeMatcherGroup &exclude_matcher_group) const { + std::vector instance_field_matchers; + flatten_field_exclude_matchers(instance_field_matchers, heap, + exclude_matcher_group.instance_fields_); + + // Analyze instances. + for (const auto task: heap.ScopedGetFieldsDataList()) { + reader::Reader fields_data_reader = task->GetScopeReader(); + + const heap::object_id_t referrer_id = task->GetInstanceId(); + const heap::object_id_t referrer_class_id = task->GetClassId(); + + heap::object_id_t current_class_id = referrer_class_id; + std::vector class_match_matchers; + for (const auto &matcher: instance_field_matchers) { + if (matcher.class_id == 0 || + heap.ChildClassOf(referrer_class_id, matcher.class_id)) { + class_match_matchers.emplace_back(matcher); + } + } + + while (true) { + for (const auto &field: heap.GetInstanceFields(current_class_id)) { + if (field.type == heap::value_type_t::kObject) { + heap::object_id_t referent_id = fields_data_reader.Read(heap.GetIdSize()); + { // Add reference. + bool exclude = false; + // Find exclude matcher of instance field. + for (const auto &matcher: class_match_matchers) { + if (matcher.field_name_id == 0 || + matcher.field_name_id == field.name_id) { + exclude = true; + break; + } + } + if (exclude) + heap.AddFieldExcludedReference(referrer_id, field.name_id, + referent_id); + else heap.AddFieldReference(referrer_id, field.name_id, referent_id); + } + } else { + heap.ReadPrimitive(referrer_id, field.name_id, field.type, + &fields_data_reader); + } + } + current_class_id = unwrap(heap.GetSuperClass(current_class_id), break); + } + } + + // Remove exclude GC roots references. + std::vector native_global_matchers; + flatten_native_global_exclude_matchers(native_global_matchers, heap, + exclude_matcher_group.native_globals_); + for (const auto gc_root: heap.GetGcRoots()) { + if (heap.GetGcRootType(gc_root) == heap::gc_root_type_t::kRootJavaFrame) { + const heap::object_id_t thread_object_id = heap.GetThreadObject( + heap.GetThreadReference(gc_root)); + const std::string thread_name = unwrap(get_thread_name(heap, thread_object_id), + continue); + for (const auto &matcher: exclude_matcher_group.threads_) { + if (matcher.FullMatchThreadName() || matcher.GetThreadName() == thread_name) { + heap.ExcludeReferences(gc_root); + break; + } + } + } + + // Find exclude matcher of native global reference. + if (heap.GetGcRootType(gc_root) == heap::gc_root_type_t::kRootJniGlobal) { + for (const auto &matcher: native_global_matchers) { + if (matcher.class_id == 0 || heap.InstanceOf(gc_root, matcher.class_id)) { + heap.ExcludeReferences(gc_root); + } + } + } + } + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootUnknownSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootUnknown); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootJniGlobalSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootJniGlobal); + reader.Skip(heap.GetIdSize()); // Skip JNI global reference ID. + return heap.GetIdSize() + heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootJniLocalSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootJniLocal); + reader.SkipU4(); // Skip thread serial number. + reader.SkipU4(); // Skip frame number. + return heap.GetIdSize() + sizeof(uint32_t) + sizeof(uint32_t); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootJavaFrameSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootJavaFrame); + const heap::thread_serial_number_t thread_serial_number = reader.ReadU4(); + if (object_id != 0) { + heap.AddThreadReferenceRecord(object_id, thread_serial_number); + } + reader.SkipU4(); // Skip frame number. + return heap.GetIdSize() + sizeof(uint32_t) + sizeof(uint32_t); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootNativeStackSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootNativeStack); + reader.SkipU4(); // Skip thread serial number. + return heap.GetIdSize() + sizeof(uint32_t); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootStickyClassSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootStickyClass); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootThreadBlockSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootThreadBlock); + reader.SkipU4(); // Skip thread serial number. + return heap.GetIdSize() + sizeof(uint32_t); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootMonitorUsedSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootMonitorUsed); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootThreadObjectSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootThreadObject); + const heap::thread_serial_number_t thread_serial_number = reader.ReadU4(); + if (object_id != 0) { + heap.AddThreadObjectRecord(object_id, thread_serial_number); + } + reader.SkipU4(); // Skip stack trace serial number. + return heap.GetIdSize() + sizeof(uint32_t) + sizeof(uint32_t); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootInternedStringSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootInternedString); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootFinalizingSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootFinalizing); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootDebuggerSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootDebugger); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootReferenceCleanupSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootReferenceCleanup); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootVMInternalSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootVMInternal); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootJniMonitorSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootJniMonitor); + reader.SkipU4(); // Skip stack trace serial number. + reader.SkipU4(); // Skip stack depth. + return heap.GetIdSize() + sizeof(uint32_t) + sizeof(uint32_t); + } + + size_t + HeapParserEngineImpl::ParseHeapContentRootUnreachableSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t object_id = reader.Read(heap.GetIdSize()); + if (object_id != 0) heap.MarkGcRoot(object_id, heap::gc_root_type_t::kRootUnreachable); + return heap.GetIdSize(); + } + + size_t + HeapParserEngineImpl::ParseHeapContentClassSubRecord(reader::Reader &reader, heap::Heap &heap, + const ExcludeMatcherGroup &exclude_matcher_group) const { + const heap::object_id_t class_id = reader.Read(heap.GetIdSize()); + heap.AddInstanceTypeRecord(class_id, heap::object_type_t::kClass); + + reader.SkipU4(); // Skip stack trace serial number. + + const heap::object_id_t super_class_id = reader.Read(heap.GetIdSize()); + heap.AddInheritanceRecord(class_id, super_class_id); + + reader.Skip(heap.GetIdSize()); // Skip class loader object ID. + reader.Skip(heap.GetIdSize()); // Skip signers object ID. + reader.Skip(heap.GetIdSize()); // Skip protection domain object ID. + reader.Skip(heap.GetIdSize()); // Skip reserved space. + reader.Skip(heap.GetIdSize()); // Skip reserved space. + reader.SkipU4(); // Skip instance size. + + size_t content_size = 0; + + // Skip constant pool. + const size_t constant_pool_length = reader.ReadU2(); + content_size += sizeof(uint16_t); + for (int i = 0; i < constant_pool_length; ++i) { + reader.SkipU2(); // Skip constant pool element index. + content_size += sizeof(uint16_t); + const size_t type_size = ({ + const heap::value_type_t type = heap::value_type_cast(reader.ReadU1()); + content_size += sizeof(uint8_t); + (type == heap::value_type_t::kObject) ? heap.GetIdSize() + : heap::get_value_type_size(type); + }); + reader.Skip(type_size); + content_size += type_size; + } + + // Read static fields. + const size_t static_fields_length = reader.ReadU2(); + content_size += sizeof(uint16_t); + + std::vector static_exclude_matchers; + flatten_field_exclude_matchers(static_exclude_matchers, heap, + exclude_matcher_group.static_fields_); + std::vector class_match_matchers; + for (const auto &matcher: static_exclude_matchers) { + if (matcher.class_id == 0 || heap.ChildClassOf(class_id, matcher.class_id)) { + class_match_matchers.emplace_back(matcher); + } + } + + for (int i = 0; i < static_fields_length; ++i) { + const heap::string_id_t field_name_id = reader.Read(heap.GetIdSize()); + content_size += heap.GetIdSize(); + const auto type = heap::value_type_cast(reader.ReadU1()); + content_size += sizeof(uint8_t); + if (type == heap::value_type_t::kObject) { + const heap::object_id_t referent_id = reader.Read(heap.GetIdSize()); + content_size += heap.GetIdSize(); + { // Add reference. + bool exclude = false; + // Find exclude matcher of static field. + for (const auto &matcher: class_match_matchers) { + if (matcher.field_name_id == 0 || matcher.field_name_id == field_name_id) { + exclude = true; + break; + } + } + if (exclude) + heap.AddFieldExcludedReference(class_id, field_name_id, referent_id, true); + else heap.AddFieldReference(class_id, field_name_id, referent_id, true); + } + } else { + const size_t type_size = heap::get_value_type_size(type); + reader.Skip(type_size); + content_size += type_size; + } + } + + // Read instance fields. + const size_t non_static_fields_length = reader.ReadU2(); + content_size += sizeof(uint16_t); + for (int i = 0; i < non_static_fields_length; ++i) { + const heap::string_id_t field_name_id = reader.Read(heap.GetIdSize()); + content_size += heap.GetIdSize(); + const auto type = heap::value_type_cast(reader.ReadU1()); + content_size += sizeof(uint8_t); + heap.AddInstanceFieldRecord(class_id, + heap::field_t{.name_id = field_name_id, .type = type}); + } + + return heap.GetIdSize() + sizeof(uint32_t) + (heap.GetIdSize() * 6) + sizeof(uint32_t) + + content_size; + } + + size_t + HeapParserEngineImpl::ParseHeapContentInstanceSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t instance_id = reader.Read(heap.GetIdSize()); + heap.AddInstanceTypeRecord(instance_id, heap::object_type_t::kInstance); + + reader.SkipU4(); // Skip stack trace serial number. + + const heap::object_id_t class_id = reader.Read(heap.GetIdSize()); + heap.AddInstanceClassRecord(instance_id, class_id); + + const size_t fields_data_size = reader.ReadU4(); + heap.ReadFieldsData(instance_id, class_id, fields_data_size, &reader); + + return heap.GetIdSize() + sizeof(uint32_t) + heap.GetIdSize() + sizeof(uint32_t) + + fields_data_size; + } + + size_t + HeapParserEngineImpl::ParseHeapContentObjectArraySubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t array_id = reader.Read(heap.GetIdSize()); + heap.AddInstanceTypeRecord(array_id, heap::object_type_t::kObjectArray); + + reader.SkipU4(); // Skip stack trace serial number. + const size_t array_length = reader.ReadU4(); + const heap::object_id_t class_id = reader.Read(heap.GetIdSize()); + heap.AddInstanceClassRecord(array_id, class_id); + for (size_t i = 0; i < array_length; ++i) { + const heap::object_id_t referent_id = reader.Read(heap.GetIdSize()); + heap.AddArrayReference(array_id, i, referent_id); + } + return heap.GetIdSize() + sizeof(uint32_t) + sizeof(uint32_t) + heap.GetIdSize() + + (array_length * heap.GetIdSize()); + } + + size_t + HeapParserEngineImpl::ParseHeapContentPrimitiveArraySubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t array_id = reader.Read(heap.GetIdSize()); + heap.AddInstanceTypeRecord(array_id, heap::object_type_t::kPrimitiveArray); + + reader.SkipU4(); // Skip stack trace serial number. + const size_t array_length = reader.ReadU4(); + const heap::value_type_t type = heap::value_type_cast(reader.ReadU1()); + const size_t array_size = array_length * heap::get_value_type_size(type); + heap.ReadPrimitiveArray(array_id, type, array_size, &reader); + return heap.GetIdSize() + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint8_t) + + array_size; + } + + size_t + HeapParserEngineImpl::ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader::Reader &reader, + heap::Heap &heap) const { + const heap::object_id_t array_id = reader.Read(heap.GetIdSize()); + heap.AddInstanceTypeRecord(array_id, heap::object_type_t::kPrimitiveArray); + + reader.SkipU4(); // Skip stack trace serial number. + reader.SkipU4(); // Skip array length; + reader.SkipU1(); // Skip array type. + return heap.GetIdSize() + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint8_t); + } + + size_t + HeapParserEngineImpl::SkipHeapContentInfoSubRecord(reader::Reader &reader, + const heap::Heap &heap) const { + reader.SkipU4(); + reader.Skip(heap.GetIdSize()); + return sizeof(uint32_t) + heap.GetIdSize(); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/include/parser.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/include/parser.h new file mode 100644 index 000000000..5c6585b59 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/include/parser.h @@ -0,0 +1,111 @@ +#ifndef __matrix_hprof_analyzer_parser_h__ +#define __matrix_hprof_analyzer_parser_h__ + +#include +#include +#include + +#include "heap.h" + +#include "macro.h" + +namespace matrix::hprof::internal::parser { + + class FieldExcludeMatcher { + public: + FieldExcludeMatcher(std::string class_name, std::string field_name) : + class_name_full_match_(class_name == "*"), field_name_full_match_(field_name == "*"), + class_name_(class_name == "*" ? "" : std::move(class_name)), + field_name_(field_name == "*" ? "" : std::move(field_name)) {} + + [[nodiscard]] bool FullMatchClassName() const { + return class_name_full_match_; + } + + [[nodiscard]] const std::string &GetClassName() const { + return class_name_; + } + + [[nodiscard]] bool FullMatchFieldName() const { + return field_name_full_match_; + } + + [[nodiscard]] const std::string &GetFieldName() const { + return field_name_; + } + + private: + const bool class_name_full_match_; + const std::string class_name_; + const bool field_name_full_match_; + const std::string field_name_; + }; + + class ThreadExcludeMatcher { + public: + explicit ThreadExcludeMatcher(std::string thread_name) : + thread_name_full_match_(thread_name == "*"), + thread_name_(thread_name == "*" ? "" : std::move(thread_name)) {} + + [[nodiscard]] bool FullMatchThreadName() const { + return thread_name_full_match_; + } + + [[nodiscard]] const std::string &GetThreadName() const { + return thread_name_; + } + + private: + const bool thread_name_full_match_; + const std::string thread_name_; + }; + + class NativeGlobalExcludeMatcher { + public: + explicit NativeGlobalExcludeMatcher(std::string class_name) : + class_name_full_match_(class_name == "*"), + class_name_(class_name == "*" ? "" : std::move(class_name)) {} + + [[nodiscard]] bool FullMatchClassName() const { + return class_name_full_match_; + } + + [[nodiscard]] const std::string &GetClassName() const { + return class_name_; + } + + private: + const bool class_name_full_match_; + const std::string class_name_; + }; + + class ExcludeMatcherGroup { + public: + std::vector instance_fields_; + std::vector static_fields_; + std::vector threads_; + std::vector native_globals_; + }; + + class HeapParserEngine; + + class HeapParser { + public: + HeapParser(); + + private: + friend_test(parser, parse); + + explicit HeapParser(HeapParserEngine *engine); + + public: + ~HeapParser(); + + void Parse(reader::Reader &reader, heap::Heap &heap, const ExcludeMatcherGroup &exclude_matcher_group) const; + + private: + const std::unique_ptr engine_; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/internal/engine.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/internal/engine.h new file mode 100644 index 000000000..685827143 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/internal/engine.h @@ -0,0 +1,173 @@ +#ifndef __matrix_hprof_analyzer_parser_engine_h__ +#define __matrix_hprof_analyzer_parser_engine_h__ + +#include "../include/parser.h" + +#include "reader.h" + +namespace matrix::hprof::internal::parser { + + /** + * The only production implementation of the engine interface must implements functions pure for testability. + *

+ * There are several functions have parameter "next". The "next" engine is used to parse sub-level HPROF record. In + * the only implementation, it is always be set to the engine instance itself, but it will be replaced by a mock + * engine while testing the implementation. + */ + class HeapParserEngine { + public: + virtual ~HeapParserEngine() = default; + + virtual void Parse(reader::Reader &reader, heap::Heap &heap, const ExcludeMatcherGroup &exclude_matchers, + const HeapParserEngine &next) const = 0; + + virtual void ParseHeader(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual void ParseStringRecord(reader::Reader &reader, heap::Heap &heap, size_t record_length) const = 0; + + virtual void ParseLoadClassRecord(reader::Reader &reader, heap::Heap &heap, size_t record_length) const = 0; + + virtual void ParseHeapContent(reader::Reader &reader, heap::Heap &heap, size_t record_length, + const ExcludeMatcherGroup &exclude_matchers, + const HeapParserEngine &next) const = 0; + + virtual void LazyParse(heap::Heap &heap, const ExcludeMatcherGroup &exclude_matcher_group) const = 0; + + virtual size_t + ParseHeapContentRootUnknownSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootJniGlobalSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootJniLocalSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootJavaFrameSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootNativeStackSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootStickyClassSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootThreadBlockSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootMonitorUsedSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootThreadObjectSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootInternedStringSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootFinalizingSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootDebuggerSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootReferenceCleanupSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootVMInternalSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootJniMonitorSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentRootUnreachableSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentClassSubRecord(reader::Reader &reader, heap::Heap &heap, + const ExcludeMatcherGroup &exclude_matcher_group) const = 0; + + virtual size_t + ParseHeapContentInstanceSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentObjectArraySubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentPrimitiveArraySubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader::Reader &reader, heap::Heap &heap) const = 0; + + virtual size_t + SkipHeapContentInfoSubRecord(reader::Reader &reader, const heap::Heap &heap) const = 0; + }; + + /** + * Parser engine implementation, see HeapParserEngine. + */ + class HeapParserEngineImpl final : public HeapParserEngine { + public: + void Parse(reader::Reader &reader, heap::Heap &heap, const ExcludeMatcherGroup &exclude_matcher_group, + const HeapParserEngine &next) const override; + + void ParseHeader(reader::Reader &reader, heap::Heap &heap) const override; + + void ParseStringRecord(reader::Reader &reader, heap::Heap &heap, size_t record_length) const override; + + void ParseLoadClassRecord(reader::Reader &reader, heap::Heap &heap, size_t record_length) const override; + + void ParseHeapContent(reader::Reader &reader, heap::Heap &heap, size_t record_length, + const ExcludeMatcherGroup &exclude_matcher_group, + const HeapParserEngine &next) const override; + + void LazyParse(heap::Heap &heap, const ExcludeMatcherGroup &exclude_matcher_group) const override; + + size_t ParseHeapContentRootUnknownSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootJniGlobalSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootJniLocalSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootJavaFrameSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootNativeStackSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootStickyClassSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootThreadBlockSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootMonitorUsedSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootThreadObjectSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootInternedStringSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootFinalizingSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootDebuggerSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootReferenceCleanupSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootVMInternalSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootJniMonitorSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootUnreachableSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentClassSubRecord(reader::Reader &reader, heap::Heap &heap, + const ExcludeMatcherGroup &exclude_matcher_group) const override; + + size_t ParseHeapContentInstanceSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentObjectArraySubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentPrimitiveArraySubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t + ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t + SkipHeapContentInfoSubRecord(reader::Reader &reader, const heap::Heap &heap) const override; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/parser.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/parser.cpp new file mode 100644 index 000000000..94a6078e4 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/parser/parser.cpp @@ -0,0 +1,19 @@ +#include "include/parser.h" +#include "internal/engine.h" + +namespace matrix::hprof::internal::parser { + + HeapParser::HeapParser() : + engine_(new HeapParserEngineImpl()) {} + + HeapParser::HeapParser(HeapParserEngine *engine) : + engine_(engine) {} + + HeapParser::~HeapParser() = default; + + void + HeapParser::Parse(reader::Reader &reader, heap::Heap &heap, + const ExcludeMatcherGroup &exclude_matcher_group) const { + engine_->Parse(reader, heap, exclude_matcher_group, *engine_); + } +} diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/reader/include/reader.h b/matrix/matrix-android/matrix-hprof-analyzer/lib/reader/include/reader.h new file mode 100644 index 000000000..e90e8fe30 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/reader/include/reader.h @@ -0,0 +1,85 @@ +#ifndef __matrix_hprof_analyzer_reader_h__ +#define __matrix_hprof_analyzer_reader_h__ + +#include +#include + +#include "macro.h" + +namespace matrix::hprof::internal::reader { + + class Reader { + public: + Reader(const uint8_t *source, size_t buffer_size); + + uint8_t ReadU1() { + return Read(sizeof(uint8_t)); + } + + void SkipU1() { + Skip(sizeof(uint8_t)); + } + + uint16_t ReadU2() { + return Read(sizeof(uint16_t)); + } + + void SkipU2() { + Skip(sizeof(uint16_t)); + } + + uint32_t ReadU4() { + return Read(sizeof(uint32_t)); + } + + void SkipU4() { + Skip(sizeof(uint32_t)); + } + + uint64_t ReadU8() { + return Read(sizeof(uint64_t)); + } + + void SkipU8() { + Skip(sizeof(uint64_t)); + } + + template + T ReadTyped(size_t size) { + if (size == sizeof(uint8_t)) { + const uint8_t value = ReadU1(); + return *reinterpret_cast(&value); + } else if (size == sizeof(uint16_t)) { + const uint16_t value = ReadU2(); + return *reinterpret_cast(&value); + } else if (size == sizeof(uint32_t)) { + const uint32_t value = ReadU4(); + return *reinterpret_cast(&value); + } else if (size == sizeof(uint64_t)) { + const uint64_t value = ReadU8(); + return *reinterpret_cast(&value); + } else { + throw std::runtime_error("Invalid type size."); + } + } + + std::string ReadString(size_t length); + + std::string ReadNullTerminatedString(); + + uint64_t Read(size_t size); + + void Skip(size_t size); + + void ResetCursor(); + + const void *Extract(size_t size); + + private: + const size_t buffer_size_; + const uint8_t *buffer_; + size_t cursor_; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/lib/reader/reader.cpp b/matrix/matrix-android/matrix-hprof-analyzer/lib/reader/reader.cpp new file mode 100644 index 000000000..e860b4593 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/lib/reader/reader.cpp @@ -0,0 +1,54 @@ +#include "errorha.h" +#include "include/reader.h" + +#include + +namespace matrix::hprof::internal::reader { + Reader::Reader(const uint8_t *source, size_t buffer_size) : + buffer_size_(buffer_size), + buffer_(source), + cursor_(0) {} + + std::string Reader::ReadString(size_t length) { + const char *content = reinterpret_cast(Extract(length)); + return {content, length}; + } + + std::string Reader::ReadNullTerminatedString() { + std::stringstream stream; + char current; + while ((current = static_cast(ReadU1())) != '\0') { + stream << current; + } + return stream.str(); + } + + uint64_t Reader::Read(size_t size) { + uint64_t ret = 0; + for (size_t i = 0; i < size; ++i) { + if (cursor_ >= buffer_size_) { + pub_fatal("reach the end of buffer"); + } + ret = (ret << 8) | buffer_[cursor_]; + cursor_ += sizeof(uint8_t); + } + return ret; + } + + void Reader::Skip(size_t size) { + cursor_ += size; + if (cursor_ > buffer_size_) { + pub_fatal("reach the end of buffer"); + } + } + + void Reader::ResetCursor() { + cursor_ = 0; + } + + const void *Reader::Extract(size_t size) { + const void *ret = reinterpret_cast(buffer_ + cursor_); + Skip(size); + return ret; + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/test/CMakeLists.txt new file mode 100644 index 000000000..54e2e794a --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +if (DEFINED ANDROID_NDK) + add_subdirectory(${ANDROID_NDK}/sources/third_party/googletest googletest.out) +elseif (DEFINED GOOGLE_TEST_DIR) + add_subdirectory(${GOOGLE_TEST_DIR} googletest.out) + include_directories(${GOOGLE_TEST_DIR}/googletest/include ${GOOGLE_TEST_DIR}/googletest) +else () + message("Parameter GOOGLE_TEST_DIR is not set. Please set it to the path to the Google Test framework source directory.") +endif () +include(GoogleTest) + +add_subdirectory(tools) + +add_subdirectory(analyzer) +add_subdirectory(heap) +add_subdirectory(parser) +add_subdirectory(reader) + +add_subdirectory(main) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/analyzer/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/test/analyzer/CMakeLists.txt new file mode 100644 index 000000000..ed0b2eca3 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/analyzer/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +add_executable(matrix_hprof_analyzer_analyzer_test analyzer.cpp) +target_link_libraries(matrix_hprof_analyzer_analyzer_test matrix_hprof_analyzer_analyzer) +target_link_libraries(matrix_hprof_analyzer_analyzer_test matrix_hprof_analyzer_heap) +target_link_libraries(matrix_hprof_analyzer_analyzer_test gtest_main) +target_link_libraries(matrix_hprof_analyzer_analyzer_test test_tools) + +gtest_discover_tests(matrix_hprof_analyzer_analyzer_test) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/analyzer/analyzer.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/analyzer/analyzer.cpp new file mode 100644 index 000000000..a77b08334 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/analyzer/analyzer.cpp @@ -0,0 +1,60 @@ +#include "analyzer.h" +#include "gtest/gtest.h" + +using namespace matrix::hprof::internal::analyzer; +using namespace matrix::hprof::internal::heap; + +TEST(analyzer, find_leak_chain) { + Heap heap; + { + heap.AddFieldReference(0x001, 0x101, 0x002); + heap.AddFieldReference(0x002, 0x102, 0x003); + heap.AddFieldReference(0x003, 0x103, 0x004); + heap.AddFieldReference(0x004, 0x104, 0x005); + heap.AddFieldReference(0x006, 0x105, 0x007); + heap.AddFieldReference(0x007, 0x106, 0x008); + heap.AddFieldReference(0x008, 0x107, 0x007); + heap.AddFieldReference(0x008, 0x108, 0x005); + heap.AddFieldReference(0x001, 0x109, 0x009); + heap.AddFieldReference(0x009, 0x10a, 0x00a); + + heap.MarkGcRoot(0x001, gc_root_type_t::kRootUnknown); + heap.MarkGcRoot(0x006, gc_root_type_t::kRootUnknown); + } + + const std::map>>> result = + find_leak_chains(heap, {0x005, 0x00a, 0x00b}); + + { // Chain of 0x005 (including circle references). + EXPECT_EQ(0x006, result.at(0x005)[0].first); + EXPECT_EQ(0x105, result.at(0x005)[0].second.value().field_name_id); + + EXPECT_EQ(0x007, result.at(0x005)[1].first); + EXPECT_EQ(0x106, result.at(0x005)[1].second.value().field_name_id); + + EXPECT_EQ(0x008, result.at(0x005)[2].first); + EXPECT_EQ(0x108, result.at(0x005)[2].second.value().field_name_id); + + EXPECT_EQ(0x005, result.at(0x005)[3].first); + EXPECT_EQ(std::nullopt, result.at(0x005)[3].second); + } + + { // Chain of 0x00a. + EXPECT_EQ(0x001, result.at(0x00a)[0].first); + EXPECT_EQ(0x109, result.at(0x00a)[0].second.value().field_name_id); + + EXPECT_EQ(0x009, result.at(0x00a)[1].first); + EXPECT_EQ(0x10a, result.at(0x00a)[1].second.value().field_name_id); + + EXPECT_EQ(0x00a, result.at(0x00a)[2].first); + EXPECT_EQ(std::nullopt, result.at(0x00a)[2].second); + } + + { // Chain of 0x009 (in graph but not a target leak). + EXPECT_THROW(result.at(0x009), std::out_of_range); + } + + { // Chain of 0x00b (not in graph). + EXPECT_THROW(result.at(0x00b), std::out_of_range); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/heap/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/test/heap/CMakeLists.txt new file mode 100644 index 000000000..c8d5d0503 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/heap/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +add_executable(matrix_hprof_analyzer_heap_test heap.cpp primitive.cpp) +target_link_libraries(matrix_hprof_analyzer_heap_test matrix_hprof_analyzer_heap) +target_link_libraries(matrix_hprof_analyzer_heap_test gtest_main) + +gtest_discover_tests(matrix_hprof_analyzer_heap_test) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/heap/heap.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/heap/heap.cpp new file mode 100644 index 000000000..5135399d1 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/heap/heap.cpp @@ -0,0 +1,522 @@ +#include "heap.h" +#include "gtest/gtest.h" + +using namespace matrix::hprof::internal::heap; +using namespace matrix::hprof::internal::reader; + +TEST(heap_value_type, get_size) { + EXPECT_EQ(0, get_value_type_size(value_type_t::kObject)); + EXPECT_EQ(1, get_value_type_size(value_type_t::kBoolean)); + EXPECT_EQ(2, get_value_type_size(value_type_t::kChar)); + EXPECT_EQ(4, get_value_type_size(value_type_t::kFloat)); + EXPECT_EQ(8, get_value_type_size(value_type_t::kDouble)); + EXPECT_EQ(1, get_value_type_size(value_type_t::kByte)); + EXPECT_EQ(2, get_value_type_size(value_type_t::kShort)); + EXPECT_EQ(4, get_value_type_size(value_type_t::kInt)); + EXPECT_EQ(8, get_value_type_size(value_type_t::kLong)); +} + +TEST(heap_value_type, cast) { + EXPECT_EQ(value_type_t::kObject, value_type_cast(2)); + EXPECT_EQ(value_type_t::kBoolean, value_type_cast(4)); + EXPECT_EQ(value_type_t::kChar, value_type_cast(5)); + EXPECT_EQ(value_type_t::kFloat, value_type_cast(6)); + EXPECT_EQ(value_type_t::kDouble, value_type_cast(7)); + EXPECT_EQ(value_type_t::kByte, value_type_cast(8)); + EXPECT_EQ(value_type_t::kShort, value_type_cast(9)); + EXPECT_EQ(value_type_t::kInt, value_type_cast(10)); + EXPECT_EQ(value_type_t::kLong, value_type_cast(11)); + EXPECT_THROW(value_type_cast(0), std::runtime_error); +} + +TEST(heap_fields_data, construct) { + const uint8_t buffer[] = {0x00, 0x00, 0x00, 0x01, 0x02}; + Reader source_reader(buffer, sizeof(buffer)); + HeapFieldsData data(0x001, 0x011, 4, &source_reader); + EXPECT_EQ(0x001, data.GetInstanceId()); + EXPECT_EQ(0x011, data.GetClassId()); + EXPECT_EQ(4, data.GetSize()); + EXPECT_EQ(1, data.GetScopeReader().ReadU4()); + EXPECT_EQ(2, source_reader.ReadU1()); +} + +TEST(heap_fields_data, scope_reader_independence) { + const uint8_t buffer[] = {0x01, 0x02}; + Reader source_reader(buffer, sizeof(buffer)); + HeapFieldsData data(0x001, 0x011, 2, &source_reader); + Reader scope_reader_1 = data.GetScopeReader(); + Reader scope_reader_2 = data.GetScopeReader(); + EXPECT_EQ(0x01, scope_reader_1.ReadU1()); + EXPECT_EQ(0x01, scope_reader_2.ReadU1()); + EXPECT_EQ(0x02, scope_reader_1.ReadU1()); + EXPECT_EQ(0x02, scope_reader_2.ReadU1()); +} + +TEST(heap, id_size) { + Heap heap; + EXPECT_THROW(auto i = heap.GetIdSize(), std::runtime_error); + EXPECT_THROW(heap.InitializeIdSize(0), std::runtime_error); + heap.InitializeIdSize(3); + EXPECT_EQ(3, heap.GetIdSize()); + EXPECT_THROW(heap.InitializeIdSize(2), std::runtime_error); +} + +TEST(heap, class_name) { + Heap heap; + heap.AddString(0x101, "test.SampleClass"); + heap.AddClassNameRecord(0x001, 0x101); + EXPECT_EQ(0x101, heap.GetClassNameId(0x001)); + EXPECT_EQ("test.SampleClass", heap.GetClassName(0x001)); + EXPECT_EQ(std::nullopt, heap.GetClassNameId(0x002)); + EXPECT_EQ(std::nullopt, heap.GetClassName(0x002)); + EXPECT_EQ(0x001, heap.FindClassByName("test.SampleClass")); + EXPECT_EQ(0x001, heap.FindClassByName("test.SampleClass")); // Test memorized. + EXPECT_EQ(std::nullopt, heap.FindClassByName("test.UnknownClass")); + EXPECT_EQ(std::nullopt, heap.FindClassByName("test.UnknownClass")); // Test memorized. +} + +TEST(heap, inheritance) { + Heap heap; + heap.AddInheritanceRecord(0x001, 0x002); + heap.AddInheritanceRecord(0x002, 0x003); + EXPECT_EQ(0x002, heap.GetSuperClass(0x001)); + EXPECT_EQ(0x003, heap.GetSuperClass(0x002)); + EXPECT_EQ(std::nullopt, heap.GetSuperClass(0x003)); + EXPECT_TRUE(heap.ChildClassOf(0x001, 0x001)); + EXPECT_TRUE(heap.ChildClassOf(0x001, 0x003)); + EXPECT_FALSE(heap.ChildClassOf(0x001, 0x004)); +} + +TEST(heap, instance_field) { + Heap heap; + const std::vector expected = { + field_t{.name_id=0x101, .type=value_type_t::kInt}, + field_t{.name_id=0x102, .type=value_type_t::kObject} + }; + + heap.AddInstanceFieldRecord(0x001, expected[0]); + heap.AddInstanceFieldRecord(0x001, expected[1]); + + std::vector actual = heap.GetInstanceFields(0x001); + EXPECT_EQ(2, actual.size()); + std::sort(actual.begin(), actual.end(), [](const field_t &a, const field_t &b) { + return a.name_id < b.name_id; + }); + EXPECT_EQ(expected, actual); + + EXPECT_EQ(std::vector(), heap.GetInstanceFields(0x002)); +} + +TEST(heap, instance_type) { + Heap heap; + // Upgrade. + heap.AddInstanceTypeRecord(0x001, object_type_t::kInstance); + EXPECT_EQ(object_type_t::kInstance, heap.GetInstanceType(0x001)); + // Exception thrown if absent. + EXPECT_THROW(auto i = heap.GetInstanceType(0x002), std::runtime_error); +} + +TEST(heap, instance_class) { + Heap heap; + heap.AddInheritanceRecord(0x011, 0x012); + heap.AddInstanceClassRecord(0x001, 0x011); + heap.AddInstanceClassRecord(0x002, 0x011); + EXPECT_EQ(0x011, heap.GetClass(0x001)); + EXPECT_EQ(0x011, heap.GetClass(0x002)); + EXPECT_EQ(std::nullopt, heap.GetClass(0x003)); + std::vector actual = heap.GetInstances(0x011); + std::sort(actual.begin(), actual.end()); + EXPECT_EQ(0x001, actual[0]); + EXPECT_EQ(0x002, actual[1]); + EXPECT_EQ(std::vector(), heap.GetInstances(0x012)); + EXPECT_TRUE(heap.InstanceOf(0x001, 0x011)); + EXPECT_TRUE(heap.InstanceOf(0x001, 0x012)); + EXPECT_FALSE(heap.InstanceOf(0x001, 0x013)); +} + +TEST(heap, gc_root) { + Heap heap; + heap.MarkGcRoot(0x001, gc_root_type_t::kRootJniLocal); + heap.MarkGcRoot(0x002, gc_root_type_t::kRootJniGlobal); + heap.MarkGcRoot(0x003, gc_root_type_t::kRootJniMonitor); + std::vector actual = heap.GetGcRoots(); + std::sort(actual.begin(), actual.end()); + EXPECT_EQ(std::vector({0x001, 0x002, 0x003}), actual); + EXPECT_EQ(gc_root_type_t::kRootJniLocal, heap.GetGcRootType(0x001)); + EXPECT_EQ(gc_root_type_t::kRootJniGlobal, heap.GetGcRootType(0x002)); + EXPECT_EQ(gc_root_type_t::kRootJniMonitor, heap.GetGcRootType(0x003)); + EXPECT_THROW(auto i = heap.GetGcRootType(0x004), std::runtime_error); +} + +TEST(heap, thread_reference) { + Heap heap; + heap.AddThreadReferenceRecord(0x001, 0x201); + EXPECT_EQ(0x201, heap.GetThreadReference(0x001)); + EXPECT_THROW(auto i = heap.GetThreadReference(0x002), std::runtime_error); +} + +TEST(heap, thread_object) { + Heap heap; + heap.AddThreadObjectRecord(0x001, 0x201); + EXPECT_EQ(0x001, heap.GetThreadObject(0x201)); + EXPECT_THROW(auto i = heap.GetThreadObject(0x202), std::runtime_error); +} + +TEST(heap, string) { + Heap heap; + heap.AddString(0x101, "Hello world!"); + heap.AddString(0x102, "Hello slice!XXX", 12); + EXPECT_EQ("Hello world!", heap.GetString(0x101)); + EXPECT_EQ(0x101, heap.FindStringId("Hello world!")); + EXPECT_EQ(0x101, heap.FindStringId("Hello world!")); // Test memorized. + EXPECT_EQ(0x102, heap.FindStringId("Hello slice!")); + EXPECT_EQ(0x102, heap.FindStringId("Hello slice!")); // Test memorized. + EXPECT_EQ(std::nullopt, heap.GetString(0x103)); + EXPECT_EQ(std::nullopt, heap.FindStringId("Unknown")); + EXPECT_EQ(std::nullopt, heap.FindStringId("Unknown")); // Test memorized. +} + +TEST(heap, fields_data) { + Heap heap; + const uint8_t buffer[] = {0x00, 0x00, 0x00, 0x01, 0x02}; + Reader source_reader(buffer, sizeof(buffer)); + heap.ReadFieldsData(0x001, 0x011, 4, &source_reader); + const HeapFieldsData *data = heap.ScopedGetFieldsData(0x001); + ASSERT_NE(nullptr, data); + EXPECT_EQ(0x001, data->GetInstanceId()); + EXPECT_EQ(0x011, data->GetClassId()); + EXPECT_EQ(4, data->GetSize()); + EXPECT_EQ(1, data->GetScopeReader().ReadU4()); + EXPECT_EQ(2, source_reader.ReadU1()); + EXPECT_EQ(nullptr, heap.ScopedGetFieldsData(0x002)); +} + +TEST(heap, fields_data_list) { + Heap heap; + const uint8_t buffer[] = {0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}; + Reader source_reader(buffer, sizeof(buffer)); + heap.ReadFieldsData(0x001, 0x011, 4, &source_reader); + heap.ReadFieldsData(0x002, 0x012, 4, &source_reader); + std::vector actual = heap.ScopedGetFieldsDataList(); + std::sort(actual.begin(), actual.end(), [](const HeapFieldsData *a, const HeapFieldsData *b) { + return a->GetInstanceId() < b->GetInstanceId(); + }); + EXPECT_EQ(0x001, actual[0]->GetInstanceId()); + EXPECT_EQ(0x011, actual[0]->GetClassId()); + EXPECT_EQ(4, actual[0]->GetSize()); + EXPECT_EQ(1, actual[0]->GetScopeReader().ReadU4()); + EXPECT_EQ(0x002, actual[1]->GetInstanceId()); + EXPECT_EQ(0x012, actual[1]->GetClassId()); + EXPECT_EQ(4, actual[1]->GetSize()); + EXPECT_EQ(2, actual[1]->GetScopeReader().ReadU4()); +} + +TEST(heap, primitive_data) { + Heap heap; + const uint8_t buffer[] = { + 0x01, // bool: true + 0x00, 0x61, // char: 'a' + 0x40, 0x40, 0x00, 0x00, // float: 3.0 + 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // double: 4.0 + 0x05, // byte: 5 + 0x00, 0x06, // short: 6 + 0x00, 0x00, 0x00, 0x07, // int: 7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // long: 8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // unused + }; + Reader source_reader(buffer, sizeof(buffer)); + heap.ReadPrimitive(0x001, 0x101, value_type_t::kBoolean, &source_reader); + heap.ReadPrimitive(0x002, 0x102, value_type_t::kChar, &source_reader); + heap.ReadPrimitive(0x003, 0x103, value_type_t::kFloat, &source_reader); + heap.ReadPrimitive(0x004, 0x104, value_type_t::kDouble, &source_reader); + heap.ReadPrimitive(0x005, 0x105, value_type_t::kByte, &source_reader); + heap.ReadPrimitive(0x006, 0x106, value_type_t::kShort, &source_reader); + heap.ReadPrimitive(0x007, 0x107, value_type_t::kInt, &source_reader); + heap.ReadPrimitive(0x008, 0x108, value_type_t::kLong, &source_reader); + + // Boolean. + { + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x001, 0x101); + EXPECT_EQ(value_type_t::kBoolean, data->GetType()); + EXPECT_EQ(true, data->GetValue()); + } + // Char. + { + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x002, 0x102); + EXPECT_EQ(value_type_t::kChar, data->GetType()); + EXPECT_EQ('a', data->GetValue()); + } + // Float. + { + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x003, 0x103); + EXPECT_EQ(value_type_t::kFloat, data->GetType()); + EXPECT_EQ(3.0f, data->GetValue()); + } + // Double. + { + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x004, 0x104); + EXPECT_EQ(value_type_t::kDouble, data->GetType()); + EXPECT_EQ(4.0, data->GetValue()); + } + // Byte. + { + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x005, 0x105); + EXPECT_EQ(value_type_t::kByte, data->GetType()); + EXPECT_EQ(5, data->GetValue()); + } + // Short. + { + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x006, 0x106); + EXPECT_EQ(value_type_t::kShort, data->GetType()); + EXPECT_EQ(6, data->GetValue()); + } + // Int. + { + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x007, 0x107); + EXPECT_EQ(value_type_t::kInt, data->GetType()); + EXPECT_EQ(7, data->GetValue()); + } + // Long. + { + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x008, 0x108); + EXPECT_EQ(value_type_t::kLong, data->GetType()); + EXPECT_EQ(8, data->GetValue()); + } + // Not primitive. + EXPECT_THROW(HeapPrimitiveData(value_type_t::kObject, &source_reader), std::runtime_error); +} + +static bool array_equal(const uint8_t *l, const uint8_t *r, size_t size) { + for (size_t i = 0; i < size; ++i) { + if (l[i] != r[i]) { + return false; + } + } + return true; +} + +TEST(heap, primitive_array_data) { + Heap heap; + // Random data. + const uint8_t buffer[] = { + 0xf1, 0xe7, 0x83, 0xe2, 0xc2, 0x4a, 0x39, 0x2d, + 0x86, 0x49, 0x0e, 0x4e, 0xf2, 0x32, 0x02, 0xef, + 0x73, 0x46, 0x87, 0xda, 0x5e, 0xdf, 0xd0, 0x06, + 0xc3, 0x44, 0x50, 0x08, 0xbf, 0x81, 0x26, 0x5b, + 0xdf, 0x77, 0x0b, 0xc5, 0x6d, 0xe4, 0xc1, 0x7d, + 0xe6, 0x7a, 0x06, 0xf4, 0x4a, 0x28, 0x0f, 0xab, + 0xb4, 0x1f, 0x17, 0x72, 0x9c, 0xc4, 0x53, 0x0e, + 0x2f, 0x71, 0xbe, 0xc4, 0x6f, 0x26, 0x85, 0x91 + }; + // Primitive. + { + Reader source_reader(buffer, sizeof(buffer)); + heap.ReadPrimitiveArray(0x001, value_type_t::kBoolean, sizeof(buffer), &source_reader); + const HeapPrimitiveArrayData *data = heap.ScopedGetPrimitiveArrayData(0x001); + ASSERT_NE(nullptr, data); + EXPECT_EQ(value_type_t::kBoolean, data->GetType()); + EXPECT_EQ(sizeof(buffer), data->GetSize()); + EXPECT_EQ(64, data->GetLength()); + EXPECT_TRUE(array_equal(buffer, data->GetData(), sizeof(buffer))); + } + // Not primitive. + { + Reader source_reader(buffer, sizeof(buffer)); + EXPECT_THROW(heap.ReadPrimitiveArray(0x002, value_type_t::kObject, sizeof(buffer), &source_reader), + std::runtime_error); + } +} + +TEST(heap, field_primitive) { + const uint8_t buffer[] = {0xff}; + Heap heap; + { + heap.AddString(0x101, "primitive"); + Reader reader(buffer, sizeof(buffer)); + heap.ReadPrimitive(0x001, 0x101, value_type_t::kByte, &reader); + } + const HeapPrimitiveData *data = heap.ScopedGetPrimitiveData(0x001, 0x101); + ASSERT_NE(nullptr, data); + EXPECT_EQ(value_type_t::kByte, data->GetType()); + EXPECT_EQ(0xff, data->GetValue()); + EXPECT_EQ(nullptr, heap.ScopedGetPrimitiveData(0x001, 0x102)); + + EXPECT_EQ(0xff, heap.GetFieldPrimitive(0x001, "primitive")); + EXPECT_EQ(std::nullopt, heap.GetFieldPrimitive(0x001, "unknown")); +} + +TEST(heap, array_primitive) { + const uint8_t buffer[] = { + 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00, + 0x64, 0x00, 0x65, 0x00, 0x66 + }; + Reader reader(buffer, sizeof(buffer)); + + Heap heap; + heap.ReadPrimitiveArray(0x001, value_type_t::kChar, sizeof(buffer), &reader); + + const HeapPrimitiveArrayData *data = heap.ScopedGetPrimitiveArrayData(0x001); + EXPECT_NE(nullptr, data); + EXPECT_EQ(value_type_t::kChar, data->GetType()); + EXPECT_EQ(sizeof(buffer), data->GetSize()); + EXPECT_EQ(sizeof(buffer) / get_value_type_size(value_type_t::kChar), data->GetLength()); + + EXPECT_EQ(std::vector({'a', 'b', 'c', 'd', 'e', 'f'}), heap.GetArrayPrimitive(0x001).value()); + EXPECT_EQ(std::nullopt, heap.GetArrayPrimitive(0x002)); +} + +TEST(heap, field_reference) { + Heap heap; + { + heap.AddString(0x101, "reference"); + heap.AddString(0x102, "reference_excluded"); + heap.AddString(0x103, "reference_not_recorded"); + heap.AddFieldReference(0x001, 0x101, 0x002); + heap.AddFieldExcludedReference(0x001, 0x102, 0x003); + } + EXPECT_EQ(0x101, heap.GetLeakReferenceGraph().at(0x001).at(0x002).field_name_id); + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x001).at(0x003), std::out_of_range); + + EXPECT_EQ(0x002, heap.GetFieldReference(0x001, "reference")); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x001, "reference_excluded")); + EXPECT_EQ(0x003, heap.GetFieldReference(0x001, "reference_excluded", true)); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x001, "unknown")); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x001, "reference_not_recorded")); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x002, "reference")); +} + +TEST(heap, array_reference) { + Heap heap; + heap.AddArrayReference(0x001, 0, 0x002); + + EXPECT_EQ(0, heap.GetLeakReferenceGraph().at(0x001).at(0x002).index); + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x001).at(0x003), std::out_of_range); + + EXPECT_EQ(0x002, heap.GetArrayReference(0x001, 0)); + EXPECT_EQ(std::nullopt, heap.GetArrayReference(0x001, 1)); + EXPECT_EQ(std::nullopt, heap.GetArrayReference(0x001, 2)); + EXPECT_EQ(std::nullopt, heap.GetArrayReference(0x002, 0)); +} + +TEST(heap, exclude_references) { + Heap heap; + heap.AddString(0x101, "reference"); + heap.AddFieldReference(0x001, 0x101, 0x002); + EXPECT_NO_THROW(heap.GetLeakReferenceGraph().at(0x001).at(0x002)); + heap.ExcludeReferences(0x001); + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x001), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x001, "reference")); + EXPECT_EQ(0x002, heap.GetFieldReference(0x001, "reference", true)); +} + +TEST(heap, value_from_string_instance) { + // Below Android API 23. + { + Heap heap; + heap.AddString(0x101, "java.lang.String"); + heap.AddString(0x102, "count"); + heap.AddString(0x103, "offset"); + heap.AddString(0x104, "value"); + heap.AddClassNameRecord(0x011, 0x101); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id=0x102, .type=value_type_t::kInt}); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id=0x103, .type=value_type_t::kInt}); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id=0x104, .type=value_type_t::kObject}); + + const uint8_t fields_data[] = { + // count: 12 + 0x00, 0x00, 0x00, 0x0c, + // offset: 3 + 0x00, 0x00, 0x00, 0x03 + + }; + Reader fields_data_reader(fields_data, sizeof(fields_data)); + const uint8_t string_content[] = { + // prefix: "???" + 0x00, 0x3f, 0x00, 0x3f, 0x00, 0x3f, + // value: "Hello world!" + 0x00, 0x48, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, + 0x00, 0x6f, 0x00, 0x20, 0x00, 0x77, 0x00, 0x6f, + 0x00, 0x72, 0x00, 0x6c, 0x00, 0x64, 0x00, 0x21 + }; + Reader string_content_reader(string_content, sizeof(string_content)); + + heap.ReadPrimitive(0x001, 0x102, value_type_t::kInt, &fields_data_reader); + heap.ReadPrimitive(0x001, 0x103, value_type_t::kInt, &fields_data_reader); + heap.AddFieldReference(0x001, 0x104, 0x002); + heap.AddInstanceClassRecord(0x001, 0x011); + + heap.ReadPrimitiveArray(0x002, value_type_t::kChar, sizeof(string_content), &string_content_reader); + + EXPECT_EQ("Hello world!", heap.GetValueFromStringInstance(0x001).value()); + EXPECT_EQ(std::nullopt, heap.GetValueFromStringInstance(0x003)); + } + // On and above Android API 23. + { + Heap heap; + heap.AddString(0x101, "java.lang.String"); + heap.AddString(0x102, "test.ChildString"); + heap.AddString(0x103, "value"); + heap.AddClassNameRecord(0x011, 0x101); + heap.AddClassNameRecord(0x012, 0x102); + heap.AddInheritanceRecord(0x012, 0x011); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id=0x103, .type=value_type_t::kObject}); + + const uint8_t string_content[] = { + // value: "Hello world!" + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, + 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21 + }; + Reader string_content_reader(string_content, sizeof(string_content)); + heap.ReadPrimitiveArray(0x003, value_type_t::kByte, + sizeof(string_content), &string_content_reader); + // Self. + { + heap.AddInstanceClassRecord(0x001, 0x011); + heap.AddFieldReference(0x001, 0x103, 0x003); + } + // Child. + { + heap.AddInstanceClassRecord(0x002, 0x012); + heap.AddFieldReference(0x002, 0x103, 0x003); + } + + EXPECT_EQ("Hello world!", heap.GetValueFromStringInstance(0x001).value()); + EXPECT_EQ("Hello world!", heap.GetValueFromStringInstance(0x002).value()); + EXPECT_EQ(std::nullopt, heap.GetValueFromStringInstance(0x003)); + } + // Error handle. + { + // Not child class. + { + Heap heap; + heap.AddString(0x101, "java.lang.String"); + heap.AddString(0x102, "java.lang.Object"); + heap.AddClassNameRecord(0x011, 0x101); + heap.AddClassNameRecord(0x012, 0x102); + heap.AddInstanceClassRecord(0x001, 0x012); + EXPECT_EQ(std::nullopt, heap.GetValueFromStringInstance(0x001)); + } + // Unknown value array type. + { + Heap heap; + heap.AddString(0x101, "java.lang.String"); + heap.AddString(0x102, "value"); + heap.AddClassNameRecord(0x011, 0x101); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id=0x102, .type=value_type_t::kObject}); + + const uint8_t string_content[] = { + // value: "Hello world!" + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, + 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21 + }; + Reader string_content_reader(string_content, sizeof(string_content)); + heap.ReadPrimitiveArray(0x002, value_type_t::kInt, + sizeof(string_content), &string_content_reader); + + heap.AddInstanceClassRecord(0x001, 0x011); + heap.AddFieldReference(0x001, 0x102, 0x002); + + EXPECT_THROW(auto i = heap.GetValueFromStringInstance(0x001), std::runtime_error); + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/heap/primitive.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/heap/primitive.cpp new file mode 100644 index 000000000..cd063b85c --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/heap/primitive.cpp @@ -0,0 +1,170 @@ +#include "heap.h" +#include "gtest/gtest.h" + +using namespace matrix::hprof::internal::heap; +using namespace matrix::hprof::internal::reader; + +TEST(heap_primitive_data, construct) { + const uint8_t buffer[] = { + 0x01, // bool: true + 0x00, 0x61, // char: 'a' + 0x40, 0x40, 0x00, 0x00, // float: 3.0 + 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // double: 4.0 + 0x05, // byte: 5 + 0x00, 0x06, // short: 6 + 0x00, 0x00, 0x00, 0x07, // int: 7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // long: 8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // unused + }; + Reader source_reader(buffer, sizeof(buffer)); + // Boolean. + { + const HeapPrimitiveData data(value_type_t::kBoolean, &source_reader); + EXPECT_EQ(value_type_t::kBoolean, data.GetType()); + EXPECT_EQ(true, data.GetValue()); + } + // Char. + { + const HeapPrimitiveData data(value_type_t::kChar, &source_reader); + EXPECT_EQ(value_type_t::kChar, data.GetType()); + EXPECT_EQ('a', data.GetValue()); + } + // Float. + { + const HeapPrimitiveData data(value_type_t::kFloat, &source_reader); + EXPECT_EQ(value_type_t::kFloat, data.GetType()); + EXPECT_EQ(3.0f, data.GetValue()); + } + // Double. + { + const HeapPrimitiveData data(value_type_t::kDouble, &source_reader); + EXPECT_EQ(value_type_t::kDouble, data.GetType()); + EXPECT_EQ(4.0, data.GetValue()); + } + // Byte. + { + const HeapPrimitiveData data(value_type_t::kByte, &source_reader); + EXPECT_EQ(value_type_t::kByte, data.GetType()); + EXPECT_EQ(5, data.GetValue()); + } + // Short. + { + const HeapPrimitiveData data(value_type_t::kShort, &source_reader); + EXPECT_EQ(value_type_t::kShort, data.GetType()); + EXPECT_EQ(6, data.GetValue()); + } + // Int. + { + const HeapPrimitiveData data(value_type_t::kInt, &source_reader); + EXPECT_EQ(value_type_t::kInt, data.GetType()); + EXPECT_EQ(7, data.GetValue()); + } + // Long. + { + const HeapPrimitiveData data(value_type_t::kLong, &source_reader); + EXPECT_EQ(value_type_t::kLong, data.GetType()); + EXPECT_EQ(8, data.GetValue()); + } + // Not primitive. + EXPECT_THROW(HeapPrimitiveData(value_type_t::kObject, &source_reader), std::runtime_error); +} + +static bool array_equal(const uint8_t *l, const uint8_t *r, size_t size) { + for (size_t i = 0; i < size; ++i) { + if (l[i] != r[i]) { + return false; + } + } + return true; +} + +TEST(heap_primitive_array_data, construct) { + // Random data. + const uint8_t buffer[] = { + 0xf1, 0xe7, 0x83, 0xe2, 0xc2, 0x4a, 0x39, 0x2d, + 0x86, 0x49, 0x0e, 0x4e, 0xf2, 0x32, 0x02, 0xef, + 0x73, 0x46, 0x87, 0xda, 0x5e, 0xdf, 0xd0, 0x06, + 0xc3, 0x44, 0x50, 0x08, 0xbf, 0x81, 0x26, 0x5b, + 0xdf, 0x77, 0x0b, 0xc5, 0x6d, 0xe4, 0xc1, 0x7d, + 0xe6, 0x7a, 0x06, 0xf4, 0x4a, 0x28, 0x0f, 0xab, + 0xb4, 0x1f, 0x17, 0x72, 0x9c, 0xc4, 0x53, 0x0e, + 0x2f, 0x71, 0xbe, 0xc4, 0x6f, 0x26, 0x85, 0x91 + }; + // Boolean. + { + Reader source_reader(buffer, sizeof(buffer)); + const HeapPrimitiveArrayData data(value_type_t::kBoolean, sizeof(buffer), &source_reader); + EXPECT_EQ(value_type_t::kBoolean, data.GetType()); + EXPECT_EQ(sizeof(buffer), data.GetSize()); + EXPECT_EQ(64, data.GetLength()); + EXPECT_TRUE(array_equal(buffer, data.GetData(), sizeof(buffer))); + } + // Char. + { + Reader source_reader(buffer, sizeof(buffer)); + const HeapPrimitiveArrayData data(value_type_t::kChar, sizeof(buffer), &source_reader); + EXPECT_EQ(value_type_t::kChar, data.GetType()); + EXPECT_EQ(sizeof(buffer), data.GetSize()); + EXPECT_EQ(32, data.GetLength()); + EXPECT_TRUE(array_equal(buffer, data.GetData(), sizeof(buffer))); + } + // Float. + { + Reader source_reader(buffer, sizeof(buffer)); + const HeapPrimitiveArrayData data(value_type_t::kFloat, sizeof(buffer), &source_reader); + EXPECT_EQ(value_type_t::kFloat, data.GetType()); + EXPECT_EQ(sizeof(buffer), data.GetSize()); + EXPECT_EQ(16, data.GetLength()); + EXPECT_TRUE(array_equal(buffer, data.GetData(), sizeof(buffer))); + } + // Double. + { + Reader source_reader(buffer, sizeof(buffer)); + const HeapPrimitiveArrayData data(value_type_t::kDouble, sizeof(buffer), &source_reader); + EXPECT_EQ(value_type_t::kDouble, data.GetType()); + EXPECT_EQ(sizeof(buffer), data.GetSize()); + EXPECT_EQ(8, data.GetLength()); + EXPECT_TRUE(array_equal(buffer, data.GetData(), sizeof(buffer))); + } + // Byte. + { + Reader source_reader(buffer, sizeof(buffer)); + const HeapPrimitiveArrayData data(value_type_t::kByte, sizeof(buffer), &source_reader); + EXPECT_EQ(value_type_t::kByte, data.GetType()); + EXPECT_EQ(sizeof(buffer), data.GetSize()); + EXPECT_EQ(64, data.GetLength()); + EXPECT_TRUE(array_equal(buffer, data.GetData(), sizeof(buffer))); + } + // Short. + { + Reader source_reader(buffer, sizeof(buffer)); + const HeapPrimitiveArrayData data(value_type_t::kShort, sizeof(buffer), &source_reader); + EXPECT_EQ(value_type_t::kShort, data.GetType()); + EXPECT_EQ(sizeof(buffer), data.GetSize()); + EXPECT_EQ(32, data.GetLength()); + EXPECT_TRUE(array_equal(buffer, data.GetData(), sizeof(buffer))); + } + // Int. + { + Reader source_reader(buffer, sizeof(buffer)); + const HeapPrimitiveArrayData data(value_type_t::kInt, sizeof(buffer), &source_reader); + EXPECT_EQ(value_type_t::kInt, data.GetType()); + EXPECT_EQ(sizeof(buffer), data.GetSize()); + EXPECT_EQ(16, data.GetLength()); + EXPECT_TRUE(array_equal(buffer, data.GetData(), sizeof(buffer))); + } + // Long. + { + Reader source_reader(buffer, sizeof(buffer)); + const HeapPrimitiveArrayData data(value_type_t::kLong, sizeof(buffer), &source_reader); + EXPECT_EQ(value_type_t::kLong, data.GetType()); + EXPECT_EQ(sizeof(buffer), data.GetSize()); + EXPECT_EQ(8, data.GetLength()); + EXPECT_TRUE(array_equal(buffer, data.GetData(), sizeof(buffer))); + } + // Not primitive. + { + Reader source_reader(buffer, sizeof(buffer)); + EXPECT_THROW(HeapPrimitiveArrayData(value_type_t::kObject, sizeof(buffer), &source_reader), std::runtime_error); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/main/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/test/main/CMakeLists.txt new file mode 100644 index 000000000..c319f3b3b --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/main/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +add_executable(matrix_hprof_analyzer_test analyze.cpp chain.cpp heap.cpp) +target_link_libraries(matrix_hprof_analyzer_test hprof_analyzer) +target_link_libraries(matrix_hprof_analyzer_test matrix_hprof_analyzer_heap matrix_hprof_analyzer_parser) +target_link_libraries(matrix_hprof_analyzer_test gtest_main) +target_link_libraries(matrix_hprof_analyzer_test test_tools) + +gtest_discover_tests(matrix_hprof_analyzer_test) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/main/analyze.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/main/analyze.cpp new file mode 100644 index 000000000..6a3cf79f5 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/main/analyze.cpp @@ -0,0 +1,264 @@ +#include "main_analyzer.h" +#include "gtest/gtest.h" + +#include +#include + +#include "heap.h" + +#include "errorha.h" + +using namespace matrix::hprof::internal::heap; + +namespace matrix::hprof { + + TEST(main_analyzer, construct) { + const uint8_t buffer[] = "magic"; + int fd = fileno(tmpfile()); + write(fd, buffer, sizeof(buffer)); + + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + + EXPECT_EQ(sizeof(buffer), impl->data_size_); + ASSERT_NE(nullptr, impl->data_); + EXPECT_TRUE(strcmp(reinterpret_cast(buffer), reinterpret_cast(impl->data_)) == 0); + + close(fd); + } + + static void test_error_listener(const char *message) { + throw std::runtime_error(message); + } + + TEST(main_analyzer, construct_error_handle) { + HprofAnalyzer::SetErrorListener(test_error_listener); + // unknown file descriptor + { + EXPECT_THROW(HprofAnalyzer impl(-1), std::runtime_error); + } + // not regular file + { + char name_template[] = "matrix-hprof-test-temp-XXXXXX"; + char *temp_dir_path = mkdtemp(name_template); + if (temp_dir_path == nullptr) FAIL() << "Unsupported test platform: Failed to create temporary directory."; + int fd = open(temp_dir_path, O_RDONLY); + EXPECT_THROW(HprofAnalyzer impl(fd), std::runtime_error); + } + } + + TEST(main_analyzer, exclude_matchers) { + HprofAnalyzer::SetErrorListener(test_error_listener); + // instance field + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeInstanceFieldReference("test.SampleClass", "reference"); + EXPECT_FALSE(impl->exclude_matcher_group_.instance_fields_[0].FullMatchClassName()); + EXPECT_EQ("test.SampleClass", impl->exclude_matcher_group_.instance_fields_[0].GetClassName()); + EXPECT_FALSE(impl->exclude_matcher_group_.instance_fields_[0].FullMatchFieldName()); + EXPECT_EQ("reference", impl->exclude_matcher_group_.instance_fields_[0].GetFieldName()); + } + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeInstanceFieldReference("test.SampleClass", "*"); + EXPECT_FALSE(impl->exclude_matcher_group_.instance_fields_[0].FullMatchClassName()); + EXPECT_EQ("test.SampleClass", impl->exclude_matcher_group_.instance_fields_[0].GetClassName()); + EXPECT_TRUE(impl->exclude_matcher_group_.instance_fields_[0].FullMatchFieldName()); + EXPECT_EQ("", impl->exclude_matcher_group_.instance_fields_[0].GetFieldName()); + } + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeInstanceFieldReference("*", "reference"); + EXPECT_TRUE(impl->exclude_matcher_group_.instance_fields_[0].FullMatchClassName()); + EXPECT_EQ("", impl->exclude_matcher_group_.instance_fields_[0].GetClassName()); + EXPECT_FALSE(impl->exclude_matcher_group_.instance_fields_[0].FullMatchFieldName()); + EXPECT_EQ("reference", impl->exclude_matcher_group_.instance_fields_[0].GetFieldName()); + } + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeInstanceFieldReference("*", "*"); + EXPECT_TRUE(impl->exclude_matcher_group_.instance_fields_[0].FullMatchClassName()); + EXPECT_EQ("", impl->exclude_matcher_group_.instance_fields_[0].GetClassName()); + EXPECT_TRUE(impl->exclude_matcher_group_.instance_fields_[0].FullMatchFieldName()); + EXPECT_EQ("", impl->exclude_matcher_group_.instance_fields_[0].GetFieldName()); + } + // static field + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeStaticFieldReference("test.SampleClass", "reference"); + EXPECT_FALSE(impl->exclude_matcher_group_.static_fields_[0].FullMatchClassName()); + EXPECT_EQ("test.SampleClass", impl->exclude_matcher_group_.static_fields_[0].GetClassName()); + EXPECT_FALSE(impl->exclude_matcher_group_.static_fields_[0].FullMatchFieldName()); + EXPECT_EQ("reference", impl->exclude_matcher_group_.static_fields_[0].GetFieldName()); + } + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeStaticFieldReference("test.SampleClass", "*"); + EXPECT_FALSE(impl->exclude_matcher_group_.static_fields_[0].FullMatchClassName()); + EXPECT_EQ("test.SampleClass", impl->exclude_matcher_group_.static_fields_[0].GetClassName()); + EXPECT_TRUE(impl->exclude_matcher_group_.static_fields_[0].FullMatchFieldName()); + EXPECT_EQ("", impl->exclude_matcher_group_.static_fields_[0].GetFieldName()); + } + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeStaticFieldReference("*", "reference"); + EXPECT_TRUE(impl->exclude_matcher_group_.static_fields_[0].FullMatchClassName()); + EXPECT_EQ("", impl->exclude_matcher_group_.static_fields_[0].GetClassName()); + EXPECT_FALSE(impl->exclude_matcher_group_.static_fields_[0].FullMatchFieldName()); + EXPECT_EQ("reference", impl->exclude_matcher_group_.static_fields_[0].GetFieldName()); + } + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeStaticFieldReference("*", "*"); + EXPECT_TRUE(impl->exclude_matcher_group_.static_fields_[0].FullMatchClassName()); + EXPECT_EQ("", impl->exclude_matcher_group_.static_fields_[0].GetClassName()); + EXPECT_TRUE(impl->exclude_matcher_group_.static_fields_[0].FullMatchFieldName()); + EXPECT_EQ("", impl->exclude_matcher_group_.static_fields_[0].GetFieldName()); + } + // thread + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeThreadReference("sample_thread"); + EXPECT_FALSE(impl->exclude_matcher_group_.threads_[0].FullMatchThreadName()); + EXPECT_EQ("sample_thread", impl->exclude_matcher_group_.threads_[0].GetThreadName()); + } + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeThreadReference("*"); + EXPECT_TRUE(impl->exclude_matcher_group_.threads_[0].FullMatchThreadName()); + EXPECT_EQ("", impl->exclude_matcher_group_.threads_[0].GetThreadName()); + } + // native global + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeNativeGlobalReference("test.SampleClass"); + EXPECT_FALSE(impl->exclude_matcher_group_.native_globals_[0].FullMatchClassName()); + EXPECT_EQ("test.SampleClass", impl->exclude_matcher_group_.native_globals_[0].GetClassName()); + } + { + int fd = fileno(tmpfile()); + write(fd, "test", sizeof("test")); + HprofAnalyzer analyzer(fd); + HprofAnalyzerImpl *impl = analyzer.impl_.get(); + analyzer.ExcludeNativeGlobalReference("*"); + EXPECT_TRUE(impl->exclude_matcher_group_.native_globals_[0].FullMatchClassName()); + EXPECT_EQ("", impl->exclude_matcher_group_.native_globals_[0].GetClassName()); + } + } + + TEST(main_analyzer_impl, analyzerlyze) { + Heap heap; + { + heap.AddString(0x111, "test.Root"); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddInstanceClassRecord(0x001, 0x011); + heap.MarkGcRoot(0x001, gc_root_type_t::kRootJavaFrame); + heap.AddInstanceTypeRecord(0x001, object_type_t::kInstance); + + heap.AddString(0x112, "test.NodeClass"); + heap.AddClassNameRecord(0x012, 0x112); + heap.AddInstanceTypeRecord(0x012, object_type_t::kClass); + + heap.AddString(0x113, "test.Leak[]"); + heap.AddClassNameRecord(0x013, 0x113); + heap.AddInstanceClassRecord(0x003, 0x013); + heap.AddInstanceTypeRecord(0x003, object_type_t::kObjectArray); + + heap.AddString(0x114, "test.Leak"); + heap.AddClassNameRecord(0x014, 0x114); + heap.AddInstanceClassRecord(0x004, 0x014); + heap.AddInstanceTypeRecord(0x004, object_type_t::kInstance); + + heap.AddString(0x101, "instanceRef"); + heap.AddString(0x102, "staticRef"); + + heap.AddFieldReference(0x001, 0x101, 0x012); + heap.AddFieldReference(0x012, 0x102, 0x003, true); + heap.AddArrayReference(0x003, 0, 0x004); + } + + std::vector>> chain; + chain.emplace_back(0x001, reference_t{.type = internal::heap::kInstanceField, .field_name_id = 0x101}); + chain.emplace_back(0x002, reference_t{.type = internal::heap::kStaticField, .field_name_id = 0x102}); + chain.emplace_back(0x003, reference_t{.type = internal::heap::kArrayElement, .index = 0}); + chain.emplace_back(0x004, std::nullopt); + + std::vector leak_chains = HprofAnalyzerImpl::Analyze(heap, {0x003, 0x004, 0x005}); + std::sort(leak_chains.begin(), leak_chains.end(), [](const LeakChain &lhs, const LeakChain &rhs) { + return lhs.GetDepth() < lhs.GetDepth(); + }); + ASSERT_EQ(2, leak_chains.size()); + + { + const LeakChain &leak_chain = leak_chains[0]; + + EXPECT_EQ(LeakChain::GcRoot::Type::kRootJavaFrame, leak_chain.GetGcRoot().GetType()); + EXPECT_EQ("test.Root", leak_chain.GetGcRoot().GetName()); + + EXPECT_EQ(LeakChain::Node::ObjectType::kClass, leak_chain.GetNodes()[0].GetObjectType()); + EXPECT_EQ("test.NodeClass", leak_chain.GetNodes()[0].GetObject()); + EXPECT_EQ(LeakChain::Node::ReferenceType::kInstanceField, leak_chain.GetNodes()[0].GetReferenceType()); + EXPECT_EQ("instanceRef", leak_chain.GetNodes()[0].GetReference()); + + EXPECT_EQ(LeakChain::Node::ObjectType::kObjectArray, leak_chain.GetNodes()[1].GetObjectType()); + EXPECT_EQ("test.Leak[]", leak_chain.GetNodes()[1].GetObject()); + EXPECT_EQ(LeakChain::Node::ReferenceType::kStaticField, leak_chain.GetNodes()[1].GetReferenceType()); + EXPECT_EQ("staticRef", leak_chain.GetNodes()[1].GetReference()); + } + + { + const LeakChain &leak_chain = leak_chains[1]; + + EXPECT_EQ(LeakChain::GcRoot::Type::kRootJavaFrame, leak_chain.GetGcRoot().GetType()); + EXPECT_EQ("test.Root", leak_chain.GetGcRoot().GetName()); + + EXPECT_EQ(LeakChain::Node::ObjectType::kClass, leak_chain.GetNodes()[0].GetObjectType()); + EXPECT_EQ("test.NodeClass", leak_chain.GetNodes()[0].GetObject()); + EXPECT_EQ(LeakChain::Node::ReferenceType::kInstanceField, leak_chain.GetNodes()[0].GetReferenceType()); + EXPECT_EQ("instanceRef", leak_chain.GetNodes()[0].GetReference()); + + EXPECT_EQ(LeakChain::Node::ObjectType::kObjectArray, leak_chain.GetNodes()[1].GetObjectType()); + EXPECT_EQ("test.Leak[]", leak_chain.GetNodes()[1].GetObject()); + EXPECT_EQ(LeakChain::Node::ReferenceType::kStaticField, leak_chain.GetNodes()[1].GetReferenceType()); + EXPECT_EQ("staticRef", leak_chain.GetNodes()[1].GetReference()); + + EXPECT_EQ(LeakChain::Node::ObjectType::kInstance, leak_chain.GetNodes()[2].GetObjectType()); + EXPECT_EQ("test.Leak", leak_chain.GetNodes()[2].GetObject()); + EXPECT_EQ(LeakChain::Node::ReferenceType::kArrayElement, leak_chain.GetNodes()[2].GetReferenceType()); + EXPECT_EQ("0", leak_chain.GetNodes()[2].GetReference()); + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/main/chain.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/main/chain.cpp new file mode 100644 index 000000000..e8925c797 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/main/chain.cpp @@ -0,0 +1,62 @@ +#include "main_chain.h" +#include "gtest/gtest.h" + +#include "heap.h" + +using namespace matrix::hprof; +using namespace matrix::hprof::internal::heap; + +TEST(main_chain, convert_gc_root_type) { + EXPECT_EQ(LeakChain::GcRoot::Type::kRootJniGlobal, + convert_gc_root_type(gc_root_type_t::kRootJniGlobal)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootJniLocal, + convert_gc_root_type(gc_root_type_t::kRootJniLocal)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootJavaFrame, + convert_gc_root_type(gc_root_type_t::kRootJavaFrame)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootNativeStack, + convert_gc_root_type(gc_root_type_t::kRootNativeStack)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootStickyClass, + convert_gc_root_type(gc_root_type_t::kRootStickyClass)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootThreadBlock, + convert_gc_root_type(gc_root_type_t::kRootThreadBlock)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootMonitorUsed, + convert_gc_root_type(gc_root_type_t::kRootMonitorUsed)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootThreadObject, + convert_gc_root_type(gc_root_type_t::kRootThreadObject)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootInternedString, + convert_gc_root_type(gc_root_type_t::kRootInternedString)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootFinalizing, + convert_gc_root_type(gc_root_type_t::kRootFinalizing)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootDebugger, + convert_gc_root_type(gc_root_type_t::kRootDebugger)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootReferenceCleanup, + convert_gc_root_type(gc_root_type_t::kRootReferenceCleanup)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootVMInternal, + convert_gc_root_type(gc_root_type_t::kRootVMInternal)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootJniMonitor, + convert_gc_root_type(gc_root_type_t::kRootJniMonitor)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootUnknown, + convert_gc_root_type(gc_root_type_t::kRootUnknown)); + EXPECT_EQ(LeakChain::GcRoot::Type::kRootUnreachable, + convert_gc_root_type(gc_root_type_t::kRootUnreachable)); +} + +TEST(main_chain, convert_reference_type) { + EXPECT_EQ(LeakChain::Node::ReferenceType::kStaticField, + convert_reference_type(reference_type_t::kStaticField)); + EXPECT_EQ(LeakChain::Node::ReferenceType::kInstanceField, + convert_reference_type(reference_type_t::kInstanceField)); + EXPECT_EQ(LeakChain::Node::ReferenceType::kArrayElement, + convert_reference_type(reference_type_t::kArrayElement)); +} + +TEST(main_chain, convert_object_type) { + EXPECT_EQ(LeakChain::Node::ObjectType::kClass, + convert_object_type(object_type_t::kClass)); + EXPECT_EQ(LeakChain::Node::ObjectType::kInstance, + convert_object_type(object_type_t::kInstance)); + EXPECT_EQ(LeakChain::Node::ObjectType::kObjectArray, + convert_object_type(object_type_t::kObjectArray)); + EXPECT_EQ(LeakChain::Node::ObjectType::kPrimitiveArray, + convert_object_type(object_type_t::kPrimitiveArray)); +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/main/heap.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/main/heap.cpp new file mode 100644 index 000000000..bb068f1e9 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/main/heap.cpp @@ -0,0 +1,87 @@ +#include "main_heap.h" +#include "gtest/gtest.h" + +using namespace matrix::hprof::internal::heap; + +namespace matrix::hprof { + + TEST(main_heap, delegate) { + { + Heap heap; + heap.AddString(0x111, "test.SampleClass"); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddInheritanceRecord(0x011, 0x012); + heap.AddInstanceClassRecord(0x001, 0x011); + + HprofHeap hprof_heap(new HprofHeapImpl(heap)); + EXPECT_EQ(0x011, hprof_heap.FindClassByName("test.SampleClass")); + EXPECT_EQ("test.SampleClass", hprof_heap.GetClassName(0x011)); + EXPECT_EQ(0x012, hprof_heap.GetSuperClass(0x011)); + EXPECT_TRUE(hprof_heap.ChildClassOf(0x011, 0x012)); + } + { + Heap heap; + heap.AddInheritanceRecord(0x011, 0x012); + heap.AddInstanceClassRecord(0x001, 0x011); + + HprofHeap hprof_heap(new HprofHeapImpl(heap)); + EXPECT_EQ(0x011, hprof_heap.GetClass(0x001)); + EXPECT_EQ(std::vector{0x001}, hprof_heap.GetInstances(0x011)); + EXPECT_TRUE(hprof_heap.InstanceOf(0x001, 0x012)); + } + { + Heap heap; + heap.AddString(0x101, "reference"); + heap.AddString(0x102, "referenceExclude"); + heap.AddFieldReference(0x001, 0x101, 0x002); + heap.AddFieldExcludedReference(0x001, 0x102, 0x003); + heap.AddArrayReference(0x002, 0, 0x004); + + HprofHeap hprof_heap(new HprofHeapImpl(heap)); + EXPECT_EQ(0x002, hprof_heap.GetFieldReference(0x001, "reference")); + EXPECT_EQ(0x003, hprof_heap.GetFieldReference(0x001, "referenceExclude")); + EXPECT_EQ(0x004, hprof_heap.GetArrayReference(0x002, 0)); + } + { + const uint8_t buffer[] = {0x1}; + internal::reader::Reader reader(buffer, sizeof(buffer)); + Heap heap; + heap.AddString(0x101, "primitive"); + heap.ReadPrimitive(0x001, 0x101, value_type_t::kBoolean, &reader); + HprofHeap hprof_heap(new HprofHeapImpl(heap)); + + EXPECT_EQ(true, hprof_heap.GetFieldPrimitive(0x001, "primitive")); + } + { + const uint8_t buffer[] = {0x0, 0x1, 0x0, 0x2, 0x0, 0x3}; + internal::reader::Reader reader(buffer, sizeof(buffer)); + Heap heap; + heap.AddString(0x101, "primitive"); + heap.ReadPrimitiveArray(0x001, value_type_t::kShort, sizeof(buffer), &reader); + + HprofHeap hprof_heap(new HprofHeapImpl(heap)); + EXPECT_EQ(std::vector({1, 2, 3}), hprof_heap.GetArrayPrimitive(0x001)); + } + { + Heap heap; + heap.AddString(0x101, "java.lang.String"); + heap.AddString(0x103, "value"); + heap.AddClassNameRecord(0x011, 0x101); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id=0x103, .type=value_type_t::kObject}); + heap.AddInstanceClassRecord(0x001, 0x011); + heap.AddFieldReference(0x001, 0x103, 0x003); + + const uint8_t string_content[] = { + // value: "Hello world!" + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, + 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21 + }; + internal::reader::Reader string_content_reader(string_content, sizeof(string_content)); + heap.ReadPrimitiveArray(0x003, value_type_t::kByte, + sizeof(string_content), &string_content_reader); + + HprofHeap hprof_heap(new HprofHeapImpl(heap)); + EXPECT_EQ("Hello world!", hprof_heap.GetValueFromStringInstance(0x001).value()); + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/parser/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/CMakeLists.txt new file mode 100644 index 000000000..b360a91dc --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +add_executable(matrix_hprof_analyzer_parser_test mock/mock_engine.cpp engine.cpp parser.cpp) +target_link_libraries(matrix_hprof_analyzer_parser_test matrix_hprof_analyzer_parser) +target_link_libraries(matrix_hprof_analyzer_parser_test gtest_main gmock gmock_main) +target_link_libraries(matrix_hprof_analyzer_parser_test test_tools) + +gtest_discover_tests(matrix_hprof_analyzer_parser_test) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/parser/engine.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/engine.cpp new file mode 100644 index 000000000..3465c7e69 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/engine.cpp @@ -0,0 +1,1136 @@ +#include "engine.h" +#include "gtest/gtest.h" + +#include "buffer_generator.h" + +#include "mock/mock_engine.h" + +#include "errorha.h" + +#include + +using namespace matrix::hprof::internal::heap; +using namespace matrix::hprof::internal::parser; +using namespace matrix::hprof::internal::reader; + +using namespace test::tools; +using namespace test::mock; + +using namespace testing; + +typedef size_t identifier_t; + +static constexpr size_t kMaxRecordSize = (1 << 5) - 1; +static constexpr size_t kMaxItemCount = 0xf; + +static const ExcludeMatcherGroup empty_exclude_matcher_group; + +TEST(parser_engine, main) { + HeapParserEngineImpl engine; + Heap heap_unused; + + NiceMock mock_engine(sizeof(identifier_t)); + + const size_t string_records_count = 5; + const size_t heap_content_records_count = 5; + const std::string buffer = ({ + BufferGenerator generator; + // Header. + { + generator.WriteNullTerminatedString("Version String"); + generator.Write(sizeof(identifier_t)); + generator.WriteZero(sizeof(uint64_t)); + } + // Strings. + { + for (size_t i = 0; i < string_records_count; ++i) { + generator.Write(0x01); + generator.WriteZero(sizeof(uint32_t)); + const size_t content_size = kMaxRecordSize; + generator.Write(content_size); + generator.WriteZero(content_size); + } + } + // Load classes. + { + generator.Write(0x02); + generator.WriteZero(sizeof(uint32_t)); + const size_t content_size = kMaxRecordSize; + generator.Write(content_size); + generator.WriteZero(content_size); + } + // Heap content. + { + for (size_t i = 0; i < heap_content_records_count; ++i) { + const uint8_t tag = (i & 1) == 0 ? 0x0c : 0x1c; + generator.Write(tag); + generator.WriteZero(sizeof(uint32_t)); + const size_t content_size = kMaxRecordSize; + generator.Write(content_size); + generator.WriteZero(content_size); + } + } + // Others. + { + generator.Write(0xff); + generator.WriteZero(sizeof(uint32_t)); + const size_t content_size = kMaxRecordSize; + generator.Write(content_size); + generator.WriteZero(content_size); + } + // End. + { + generator.Write(0x2c); + generator.WriteZero(sizeof(uint32_t)); + generator.Write(0); + } + generator.GetContent(); + }); + + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + + EXPECT_CALL(mock_engine, ParseHeader(_, _)); + EXPECT_CALL(mock_engine, ParseStringRecord(_, _, _)) + .Times(string_records_count); + EXPECT_CALL(mock_engine, ParseLoadClassRecord(_, _, _)); + EXPECT_CALL(mock_engine, ParseHeapContent(_, _, _, _, _)) + .Times(heap_content_records_count); + EXPECT_CALL(mock_engine, LazyParse(_, _)); + + engine.Parse(reader, heap_unused, empty_exclude_matcher_group, mock_engine); +} + +TEST(parser_engine, header) { + HeapParserEngineImpl engine; + { + const std::string buffer = ({ + BufferGenerator generator; + generator.WriteNullTerminatedString("JAVA PROFILE 1.0"); + generator.Write(3); + generator.WriteZero(sizeof(uint64_t)); + generator.GetContent(); + }); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + Heap heap; + EXPECT_NO_THROW(engine.ParseHeader(reader, heap)); + EXPECT_EQ(heap.GetIdSize(), 3); + } + { + const std::string buffer = ({ + BufferGenerator generator; + generator.WriteNullTerminatedString("JAVA PROFILE 1.0.1"); + generator.Write(4); + generator.WriteZero(sizeof(uint64_t)); + generator.GetContent(); + }); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + Heap heap; + EXPECT_NO_THROW(engine.ParseHeader(reader, heap)); + EXPECT_EQ(heap.GetIdSize(), 4); + } + { + const std::string buffer = ({ + BufferGenerator generator; + generator.WriteNullTerminatedString("JAVA PROFILE 1.0.2"); + generator.Write(5); + generator.WriteZero(sizeof(uint64_t)); + generator.GetContent(); + }); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + Heap heap; + EXPECT_NO_THROW(engine.ParseHeader(reader, heap)); + EXPECT_EQ(heap.GetIdSize(), 5); + } + { + const std::string buffer = ({ + BufferGenerator generator; + generator.WriteNullTerminatedString("JAVA PROFILE 1.0.3"); + generator.Write(7); + generator.WriteZero(sizeof(uint64_t)); + generator.GetContent(); + }); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + Heap heap; + EXPECT_NO_THROW(engine.ParseHeader(reader, heap)); + EXPECT_EQ(heap.GetIdSize(), 7); + } + { + const std::string buffer = ({ + BufferGenerator generator; + generator.WriteNullTerminatedString("JAVA PROFILE 1.0.0"); + generator.Write(2); + generator.WriteZero(sizeof(uint64_t)); + generator.GetContent(); + }); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + Heap heap; + EXPECT_THROW(engine.ParseHeader(reader, heap), std::runtime_error); + } +} + +TEST(parser_engine, strings) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x101); + generator.WriteString("Hello world!"); + generator.GetContent(); + }); + + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + engine.ParseStringRecord(reader, heap, buffer.size()); + EXPECT_EQ("Hello world!", heap.GetString(0x101)); +} + +TEST(parser_engine, load_class) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.WriteZero(sizeof(uint32_t)); + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t)); + generator.Write(0x101); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + engine.ParseLoadClassRecord(reader, heap, buffer.size()); + EXPECT_EQ(0x101, heap.GetClassNameId(0x001)); +} + +TEST(parser_engine, heap_content) { + const std::string buffer = ({ + BufferGenerator generator; + // root unknown + generator.Write(0xff); + generator.WriteZero(sizeof(identifier_t)); + // root Jni global + generator.Write(0x01); + generator.WriteZero(sizeof(identifier_t) + sizeof(identifier_t)); + // root Jni local + generator.Write(0x02); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(uint32_t)); + // root Java frame + generator.Write(0x03); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(uint32_t)); + // root native stack + generator.Write(0x04); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t)); + // root sticky class + generator.Write(0x05); + generator.WriteZero(sizeof(identifier_t)); + // root thread block + generator.Write(0x06); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t)); + // root monitor used + generator.Write(0x07); + generator.WriteZero(sizeof(identifier_t)); + // root thread object + generator.Write(0x08); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(uint32_t)); + // root interned string + generator.Write(0x89); + generator.WriteZero(sizeof(identifier_t)); + // root finalizing + generator.Write(0x8a); + generator.WriteZero(sizeof(identifier_t)); + // root debugger + generator.Write(0x8b); + generator.WriteZero(sizeof(identifier_t)); + // root reference cleanup + generator.Write(0x8c); + generator.WriteZero(sizeof(identifier_t)); + // root VM internal + generator.Write(0x8d); + generator.WriteZero(sizeof(identifier_t)); + // root Jni monitor + generator.Write(0x8e); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(uint32_t)); + // root unreachable + generator.Write(0x90); + generator.WriteZero(sizeof(identifier_t)); + // class + generator.Write(0x20); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(identifier_t) * 6 + + sizeof(uint32_t) + sizeof(uint16_t) * 3); + // instance + generator.Write(0x21); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(identifier_t) + sizeof(uint32_t)); + // object array + generator.Write(0x22); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(identifier_t)); + // primitive array + generator.Write(0x23); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(uint32_t)); + generator.Write(static_cast(value_type_t::kByte)); + // primitive array no data + generator.Write(0xc3); + generator.WriteZero(sizeof(identifier_t) + sizeof(uint32_t) + sizeof(uint32_t)); + generator.Write(static_cast(value_type_t::kByte)); + // heap dump info + generator.Write(0xfe); + generator.WriteZero(sizeof(uint32_t) + sizeof(identifier_t)); + + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + + HeapParserEngineImpl engine; + MockEngine mock_engine(sizeof(identifier_t)); + + EXPECT_CALL(mock_engine, ParseHeapContentRootUnknownSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootJniGlobalSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootJniLocalSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootJavaFrameSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootNativeStackSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootStickyClassSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootThreadBlockSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootMonitorUsedSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootThreadObjectSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootInternedStringSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootFinalizingSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootDebuggerSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootReferenceCleanupSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootVMInternalSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootJniMonitorSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentRootUnreachableSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentClassSubRecord(_, _, _)); + EXPECT_CALL(mock_engine, ParseHeapContentInstanceSubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentObjectArraySubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentPrimitiveArraySubRecord(_, _)); + EXPECT_CALL(mock_engine, ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(_, _)); + EXPECT_CALL(mock_engine, SkipHeapContentInfoSubRecord(_, _)); + + engine.ParseHeapContent(reader, heap, buffer.size(), empty_exclude_matcher_group, mock_engine); +} + +TEST(parser_engine, lazy_parse_instances) { + // Prepare heap. + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + { // classes + heap.AddString(0x111, "test.A"); + heap.AddClassNameRecord(0x011, 0x111); + + heap.AddString(0x112, "test.B"); + heap.AddClassNameRecord(0x012, 0x112); + + heap.AddString(0x113, "test.C"); + heap.AddClassNameRecord(0x013, 0x113); + + heap.AddString(0x114, "test.ExcludeByField"); + heap.AddClassNameRecord(0x014, 0x114); + + heap.AddInheritanceRecord(0x012, 0x011); + heap.AddInheritanceRecord(0x013, 0x012); + heap.AddInheritanceRecord(0x014, 0x013); + } + { // fields + heap.AddString(0x101, "instanceInclude"); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id = 0x101, .type = value_type_t::kObject}); + + heap.AddString(0x102, "instanceExclude"); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id = 0x102, .type = value_type_t::kObject}); + + heap.AddString(0x103, "instanceExcludeByClass"); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id = 0x103, .type = value_type_t::kObject}); + + heap.AddString(0x104, "primitive"); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id = 0x104, .type = value_type_t::kByte}); + } + // instance + const std::string instance_fields_data = ({ + BufferGenerator generator; + generator.Write(0x005); + generator.Write(0x006); + generator.Write(0x007); + generator.Write(0x1); + generator.GetContent(); + }); + { + heap.AddInstanceTypeRecord(0x001, object_type_t::kInstance); + heap.AddInstanceClassRecord(0x001, 0x011); + Reader fields_data_reader(reinterpret_cast(instance_fields_data.data()), + instance_fields_data.size()); + heap.ReadFieldsData(0x001, 0x011, instance_fields_data.size(), &fields_data_reader); + } + { + heap.AddInstanceTypeRecord(0x002, object_type_t::kInstance); + heap.AddInstanceClassRecord(0x002, 0x012); + Reader fields_data_reader(reinterpret_cast(instance_fields_data.data()), + instance_fields_data.size()); + heap.ReadFieldsData(0x002, 0x012, instance_fields_data.size(), &fields_data_reader); + } + { + heap.AddInstanceTypeRecord(0x003, object_type_t::kInstance); + heap.AddInstanceClassRecord(0x003, 0x013); + Reader fields_data_reader(reinterpret_cast(instance_fields_data.data()), + instance_fields_data.size()); + heap.ReadFieldsData(0x003, 0x013, instance_fields_data.size(), &fields_data_reader); + } + { + heap.AddInstanceTypeRecord(0x004, object_type_t::kInstance); + heap.AddInstanceClassRecord(0x004, 0x014); + Reader fields_data_reader(reinterpret_cast(instance_fields_data.data()), + instance_fields_data.size()); + heap.ReadFieldsData(0x004, 0x014, instance_fields_data.size(), &fields_data_reader); + } + HeapParserEngineImpl engine; + ExcludeMatcherGroup exclude_matcher_group; + { + exclude_matcher_group.instance_fields_.emplace_back("test.B", "instanceExclude"); + exclude_matcher_group.instance_fields_.emplace_back("test.ExcludeByField", "*"); + exclude_matcher_group.instance_fields_.emplace_back("*", "instanceExcludeByClass"); + } + engine.LazyParse(heap, exclude_matcher_group); + { // primitives + { + const auto *data = heap.ScopedGetPrimitiveData(0x001, 0x104); + EXPECT_EQ(value_type_t::kByte, data->GetType()); + EXPECT_EQ(0x1, data->GetValue()); + } + { + const auto *data = heap.ScopedGetPrimitiveData(0x002, 0x104); + EXPECT_EQ(value_type_t::kByte, data->GetType()); + EXPECT_EQ(0x1, data->GetValue()); + } + { + const auto *data = heap.ScopedGetPrimitiveData(0x003, 0x104); + EXPECT_EQ(value_type_t::kByte, data->GetType()); + EXPECT_EQ(0x1, data->GetValue()); + } + { + const auto *data = heap.ScopedGetPrimitiveData(0x004, 0x104); + EXPECT_EQ(value_type_t::kByte, data->GetType()); + EXPECT_EQ(0x1, data->GetValue()); + } + } + { // exclude + { + EXPECT_EQ(0x101, heap.GetLeakReferenceGraph().at(0x001).at(0x005).field_name_id); + EXPECT_EQ(reference_type_t::kInstanceField, heap.GetLeakReferenceGraph().at(0x001).at(0x005).type); + EXPECT_EQ(0x005, heap.GetFieldReference(0x001, "instanceInclude")); + + EXPECT_EQ(0x102, heap.GetLeakReferenceGraph().at(0x001).at(0x006).field_name_id); + EXPECT_EQ(reference_type_t::kInstanceField, heap.GetLeakReferenceGraph().at(0x001).at(0x006).type); + EXPECT_EQ(0x006, heap.GetFieldReference(0x001, "instanceExclude")); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x001).at(0x007), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x001, "instanceExcludeByClass")); + EXPECT_EQ(0x007, heap.GetFieldReference(0x001, "instanceExcludeByClass", true)); + } + { + EXPECT_EQ(0x101, heap.GetLeakReferenceGraph().at(0x002).at(0x005).field_name_id); + EXPECT_EQ(reference_type_t::kInstanceField, heap.GetLeakReferenceGraph().at(0x002).at(0x005).type); + EXPECT_EQ(0x005, heap.GetFieldReference(0x002, "instanceInclude")); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x002).at(0x006), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x002, "instanceExclude")); + EXPECT_EQ(0x006, heap.GetFieldReference(0x002, "instanceExclude", true)); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x002).at(0x007), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x002, "instanceExcludeByClass")); + EXPECT_EQ(0x007, heap.GetFieldReference(0x002, "instanceExcludeByClass", true)); + } + { + EXPECT_EQ(0x101, heap.GetLeakReferenceGraph().at(0x003).at(0x005).field_name_id); + EXPECT_EQ(reference_type_t::kInstanceField, heap.GetLeakReferenceGraph().at(0x003).at(0x005).type); + EXPECT_EQ(0x005, heap.GetFieldReference(0x003, "instanceInclude")); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x003).at(0x006), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x003, "instanceExclude")); + EXPECT_EQ(0x006, heap.GetFieldReference(0x003, "instanceExclude", true)); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x003).at(0x007), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x003, "instanceExcludeByClass")); + EXPECT_EQ(0x007, heap.GetFieldReference(0x003, "instanceExcludeByClass", true)); + } + { + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x004).at(0x005), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x004, "instanceInclude")); + EXPECT_EQ(0x005, heap.GetFieldReference(0x004, "instanceInclude", true)); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x004).at(0x006), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x004, "instanceExclude")); + EXPECT_EQ(0x006, heap.GetFieldReference(0x004, "instanceExclude", true)); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x004).at(0x007), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x004, "instanceExcludeByClass")); + EXPECT_EQ(0x007, heap.GetFieldReference(0x004, "instanceExcludeByClass", true)); + } + } +} + +TEST(parser_engine, lazy_parse_exclude_thread) { + Heap heap; + { // strings + const uint8_t include_buffer[] = { + // string: "include" + 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65 + }; + const uint8_t exclude_buffer[] = { + // string: "exclude" + 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65 + }; + + heap.AddString(0x111, "java.lang.String"); + heap.AddString(0x101, "value"); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddInstanceFieldRecord(0x011, field_t{.name_id = 0x101, .type = value_type_t::kObject}); + + // string: "include" + Reader include_reader(include_buffer, sizeof(include_buffer)); + heap.AddInstanceClassRecord(0x001, 0x011); + heap.ReadPrimitiveArray(0x002, value_type_t::kByte, sizeof(include_buffer), &include_reader); + heap.AddFieldReference(0x001, 0x101, 0x002); + // string: "exclude" + Reader exclude_reader(exclude_buffer, sizeof(exclude_buffer)); + heap.AddInstanceClassRecord(0x003, 0x011); + heap.ReadPrimitiveArray(0x004, value_type_t::kByte, sizeof(exclude_buffer), &exclude_reader); + heap.AddFieldReference(0x003, 0x101, 0x004); + } + { // threads + heap.AddString(0x112, "java.lang.Thread"); + heap.AddString(0x102, "name"); + heap.AddClassNameRecord(0x012, 0x112); + heap.AddInstanceFieldRecord(0x012, field_t{.name_id = 0x102, .type = value_type_t::kObject}); + + // thread: "include" + heap.AddInstanceClassRecord(0x005, 0x012); + heap.AddFieldReference(0x005, 0x102, 0x001); + heap.AddThreadObjectRecord(0x005, 0x201); + // thread: "exclude" + heap.AddInstanceClassRecord(0x006, 0x012); + heap.AddFieldReference(0x006, 0x102, 0x003); + heap.AddThreadObjectRecord(0x006, 0x202); + } + { // GC roots + heap.AddThreadReferenceRecord(0x007, 0x201); + heap.MarkGcRoot(0x007, heap::gc_root_type_t::kRootJavaFrame); + heap.AddThreadReferenceRecord(0x008, 0x202); + heap.MarkGcRoot(0x008, heap::gc_root_type_t::kRootJavaFrame); + } + { // leaks + heap.AddFieldReference(0x007, 0x103, 0x009); + heap.AddFieldReference(0x008, 0x103, 0x00a); + } + ExcludeMatcherGroup exclude_matcher_group; + exclude_matcher_group.threads_.emplace_back("exclude"); + + HeapParserEngineImpl engine; + engine.LazyParse(heap, exclude_matcher_group); + + EXPECT_EQ(0x103, heap.GetLeakReferenceGraph().at(0x007).at(0x009).field_name_id); + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x008).at(0x00a), std::out_of_range); +} + +TEST(parser_engine, lazy_parse_exclude_native_global) { + Heap heap; + { // strings and classes + heap.AddString(0x111, "test.ExcludeClass"); + heap.AddInstanceTypeRecord(0x011, object_type_t::kClass); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddString(0x112, "test.ExcludeArrayElement[]"); + heap.AddInstanceTypeRecord(0x012, object_type_t::kClass); + heap.AddClassNameRecord(0x012, 0x112); + } + { // instances + heap.AddInstanceClassRecord(0x001, 0x011); + heap.MarkGcRoot(0x001, heap::gc_root_type_t::kRootJniGlobal); + heap.AddInstanceClassRecord(0x002, 0x012); + heap.MarkGcRoot(0x002, heap::gc_root_type_t::kRootJniGlobal); + } + heap.AddFieldReference(0x001, 0x101, 0x003); + heap.AddArrayReference(0x002, 0, 0x004); + + ExcludeMatcherGroup exclude_matcher_group; + exclude_matcher_group.native_globals_.emplace_back("test.ExcludeClass"); + exclude_matcher_group.native_globals_.emplace_back("test.ExcludeArrayElement[]"); + + HeapParserEngineImpl engine; + engine.LazyParse(heap, exclude_matcher_group); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x001).at(0x003), std::out_of_range); + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x002).at(0x004), std::out_of_range); +} + +TEST(parser_engine, heap_content_error_handle) { + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x00); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + HeapParserEngineImpl engine; + MockEngine mock_engine(sizeof(identifier_t)); + EXPECT_THROW(engine.ParseHeapContent( + reader, heap, buffer.size(), + empty_exclude_matcher_group, mock_engine + ), std::runtime_error); +} + +TEST(parser_engine, root_unknown) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootUnknownSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootUnknown, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_jni_global) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(identifier_t)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootJniGlobalSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootJniGlobal, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_jni_local) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t) + sizeof(uint32_t)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootJniLocalSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootJniLocal, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_java_frame) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.Write(0x201); + generator.WriteZero(sizeof(uint32_t)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootJavaFrameSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootJavaFrame, heap.GetGcRootType(0x001)); + EXPECT_EQ(0x201, heap.GetThreadReference(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_native_stack) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootNativeStackSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootNativeStack, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_sticky_class) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootStickyClassSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootStickyClass, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_thread_block) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootThreadBlockSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootThreadBlock, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_monitor_used) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootMonitorUsedSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootMonitorUsed, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_thread_object) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.Write(0x201); + generator.WriteZero(sizeof(uint32_t)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootThreadObjectSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootThreadObject, heap.GetGcRootType(0x001)); + EXPECT_EQ(0x001, heap.GetThreadObject(0x201)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_interned_string) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootInternedStringSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootInternedString, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_finalizing) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootFinalizingSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootFinalizing, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_debugger) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootDebuggerSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootDebugger, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_reference_cleanup) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootReferenceCleanupSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootReferenceCleanup, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_vm_internal) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootVMInternalSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootVMInternal, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_jni_monitor) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t) + sizeof(uint32_t)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootJniMonitorSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootJniMonitor, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, root_unreachable) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentRootUnreachableSubRecord(reader, heap); + const std::vector gc_roots = heap.GetGcRoots(); + EXPECT_TRUE(std::find(gc_roots.begin(), gc_roots.end(), 0x001) != gc_roots.end()); + EXPECT_EQ(heap::gc_root_type_t::kRootUnreachable, heap.GetGcRootType(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, record_class) { + HeapParserEngineImpl engine; + const size_t constant_pool_count = kMaxItemCount; + const size_t static_fields_object_count = 2; + const size_t static_fields_primitive_count = kMaxItemCount; + const size_t instance_fields_count = kMaxItemCount; + + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x011); + generator.WriteZero(sizeof(uint32_t)); + generator.Write(0x012); + generator.WriteZero(sizeof(identifier_t) * 5); + generator.WriteZero(sizeof(uint32_t)); + generator.Write(constant_pool_count); + for (size_t i = 0; i < constant_pool_count; ++i) { + generator.Write(i); + generator.Write(static_cast(heap::value_type_t::kByte)); + generator.Write(i); + } + generator.Write(static_fields_object_count + static_fields_primitive_count); + for (size_t i = 0; i < static_fields_object_count; ++i) { + generator.Write(0x101 + i); + generator.Write(static_cast(heap::value_type_t::kObject)); + generator.Write(0x003 + i); + } + for (size_t i = 0; i < static_fields_primitive_count; ++i) { + generator.Write(0x120 + i); + generator.Write(static_cast(heap::value_type_t::kInt)); + generator.Write(i); + } + generator.Write(instance_fields_count); + for (size_t i = 0; i < instance_fields_count; ++i) { + generator.Write(0x120 + i); + generator.Write(static_cast(heap::value_type_t::kByte)); + } + generator.GetContent(); + }); + + { // default + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddString(0x111, "test.SampleClass"); + heap.AddString(0x101, "staticA"); + heap.AddString(0x102, "staticB"); + + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + + const size_t parse_size = engine.ParseHeapContentClassSubRecord(reader, heap, empty_exclude_matcher_group); + + EXPECT_EQ(heap::object_type_t::kClass, heap.GetInstanceType(0x011)); + EXPECT_EQ(0x012, heap.GetSuperClass(0x011)); + + EXPECT_EQ(0x101, heap.GetLeakReferenceGraph().at(0x011).at(003).field_name_id); + EXPECT_EQ(reference_type_t::kStaticField, heap.GetLeakReferenceGraph().at(0x011).at(003).type); + EXPECT_EQ(0x003, heap.GetFieldReference(0x011, "staticA")); + + EXPECT_EQ(0x102, heap.GetLeakReferenceGraph().at(0x011).at(0x004).field_name_id); + EXPECT_EQ(reference_type_t::kStaticField, heap.GetLeakReferenceGraph().at(0x011).at(0x004).type); + EXPECT_EQ(0x004, heap.GetFieldReference(0x011, "staticB")); + + std::vector instance_fields = heap.GetInstanceFields(0x011); + std::sort(instance_fields.begin(), instance_fields.end(), [](const field_t &a, const field_t &b) { + return a.name_id < b.name_id; + }); + for (size_t i = 0; i < instance_fields_count; ++i) { + EXPECT_EQ(0x120 + i, instance_fields.at(i).name_id); + } + } + { // exclude current + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddString(0x111, "test.SampleClass"); + heap.AddString(0x101, "staticInclude"); + heap.AddString(0x102, "staticExclude"); + + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + + ExcludeMatcherGroup exclude_matcher_group; + exclude_matcher_group.static_fields_.emplace_back("test.SampleClass", "staticExclude"); + + const size_t parse_size = engine.ParseHeapContentClassSubRecord(reader, heap, exclude_matcher_group); + + EXPECT_EQ(0x101, heap.GetLeakReferenceGraph().at(0x011).at(003).field_name_id); + EXPECT_EQ(reference_type_t::kStaticField, heap.GetLeakReferenceGraph().at(0x011).at(003).type); + EXPECT_EQ(0x003, heap.GetFieldReference(0x011, "staticInclude")); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x011).at(0x004), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x011, "staticExclude")); + EXPECT_EQ(0x004, heap.GetFieldReference(0x011, "staticExclude", true)); + } + { // exclude super + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddString(0x111, "test.SampleClass"); + heap.AddClassNameRecord(0x012, 0x112); + heap.AddString(0x112, "test.SuperClass"); + heap.AddString(0x101, "staticInclude"); + heap.AddString(0x102, "staticExclude"); + + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + + ExcludeMatcherGroup exclude_matcher_group; + exclude_matcher_group.static_fields_.emplace_back("test.SuperClass", "staticExclude"); + + const size_t parse_size = engine.ParseHeapContentClassSubRecord(reader, heap, exclude_matcher_group); + + EXPECT_EQ(0x101, heap.GetLeakReferenceGraph().at(0x011).at(003).field_name_id); + EXPECT_EQ(reference_type_t::kStaticField, heap.GetLeakReferenceGraph().at(0x011).at(003).type); + EXPECT_EQ(0x003, heap.GetFieldReference(0x011, "staticInclude")); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x011).at(0x004), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x011, "staticExclude")); + EXPECT_EQ(0x004, heap.GetFieldReference(0x011, "staticExclude", true)); + } + { // exclude all classes + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddString(0x111, "test.SampleClass"); + heap.AddString(0x101, "staticInclude"); + heap.AddString(0x102, "staticExclude"); + + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + + ExcludeMatcherGroup exclude_matcher_group; + exclude_matcher_group.static_fields_.emplace_back("*", "staticExclude"); + + const size_t parse_size = engine.ParseHeapContentClassSubRecord(reader, heap, exclude_matcher_group); + + EXPECT_EQ(0x101, heap.GetLeakReferenceGraph().at(0x011).at(003).field_name_id); + EXPECT_EQ(reference_type_t::kStaticField, heap.GetLeakReferenceGraph().at(0x011).at(003).type); + EXPECT_EQ(0x003, heap.GetFieldReference(0x011, "staticInclude")); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x011).at(0x004), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x011, "staticExclude")); + EXPECT_EQ(0x004, heap.GetFieldReference(0x011, "staticExclude", true)); + } + { // exclude all fields + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + heap.AddClassNameRecord(0x011, 0x111); + heap.AddString(0x111, "test.SampleClass"); + heap.AddString(0x101, "staticInclude"); + heap.AddString(0x102, "staticExclude"); + + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + + ExcludeMatcherGroup exclude_matcher_group; + exclude_matcher_group.static_fields_.emplace_back("test.SampleClass", "*"); + + const size_t parse_size = engine.ParseHeapContentClassSubRecord(reader, heap, exclude_matcher_group); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x011).at(003), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x011, "staticInclude")); + EXPECT_EQ(0x003, heap.GetFieldReference(0x011, "staticInclude", true)); + + EXPECT_THROW(heap.GetLeakReferenceGraph().at(0x011).at(0x004), std::out_of_range); + EXPECT_EQ(std::nullopt, heap.GetFieldReference(0x011, "staticExclude")); + EXPECT_EQ(0x004, heap.GetFieldReference(0x011, "staticExclude", true)); + } +} + +TEST(parser_engine, record_instance) { + HeapParserEngineImpl engine; + const size_t fields_data_size = kMaxRecordSize; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t)); + generator.Write(0x002); + generator.Write(fields_data_size); + generator.WriteZero(fields_data_size); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentInstanceSubRecord(reader, heap); + EXPECT_EQ(heap::object_type_t::kInstance, heap.GetInstanceType(0x001)); + EXPECT_EQ(0x002, heap.GetClass(0x001)); + const heap::HeapFieldsData *fields_data = heap.ScopedGetFieldsData(0x001); + EXPECT_NE(nullptr, fields_data); + EXPECT_EQ(0x001, fields_data->GetInstanceId()); + EXPECT_EQ(0x002, fields_data->GetClassId()); + EXPECT_EQ(fields_data_size, fields_data->GetSize()); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, record_object_array) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t)); + generator.Write(2); + generator.Write(0x011); + generator.Write(0x002); + generator.Write(0x003); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentObjectArraySubRecord(reader, heap); + EXPECT_EQ(heap::object_type_t::kObjectArray, heap.GetInstanceType(0x001)); + EXPECT_EQ(0x011, heap.GetClass(0x001)); + EXPECT_EQ(0, heap.GetLeakReferenceGraph().at(0x001).at(0x002).index); + EXPECT_EQ(reference_type_t::kArrayElement, heap.GetLeakReferenceGraph().at(0x001).at(0x002).type); + EXPECT_EQ(1, heap.GetLeakReferenceGraph().at(0x001).at(0x003).index); + EXPECT_EQ(reference_type_t::kArrayElement, heap.GetLeakReferenceGraph().at(0x001).at(0x003).type); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, record_primitive_array) { + HeapParserEngineImpl engine; + const size_t array_length = 10; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t)); + generator.Write(array_length); + generator.Write(static_cast(heap::value_type_t::kInt)); + generator.WriteZero(array_length * get_value_type_size(heap::value_type_t::kInt)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentPrimitiveArraySubRecord(reader, heap); + EXPECT_EQ(heap::object_type_t::kPrimitiveArray, heap.GetInstanceType(0x001)); + const HeapPrimitiveArrayData *array_data = heap.ScopedGetPrimitiveArrayData(0x001); + EXPECT_NE(nullptr, array_data); + EXPECT_EQ(heap::value_type_t::kInt, array_data->GetType()); + EXPECT_EQ(array_length, array_data->GetLength()); + EXPECT_EQ(array_length * get_value_type_size(heap::value_type_t::kInt), array_data->GetSize()); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, record_primitive_array_no_data) { + HeapParserEngineImpl engine; + const size_t array_length = 10; + const std::string buffer = ({ + BufferGenerator generator; + generator.Write(0x001); + generator.WriteZero(sizeof(uint32_t)); + generator.Write(array_length); + generator.Write(static_cast(heap::value_type_t::kFloat)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + const size_t parse_size = engine.ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader, heap); + EXPECT_EQ(heap::object_type_t::kPrimitiveArray, heap.GetInstanceType(0x001)); + EXPECT_EQ(nullptr, heap.ScopedGetPrimitiveArrayData(0x001)); + EXPECT_EQ(buffer.size(), parse_size); +} + +TEST(parser_engine, record_heap_info) { + HeapParserEngineImpl engine; + const std::string buffer = ({ + BufferGenerator generator; + generator.WriteZero(sizeof(uint32_t) + sizeof(identifier_t)); + generator.GetContent(); + }); + Heap heap; + heap.InitializeIdSize(sizeof(identifier_t)); + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + EXPECT_EQ(buffer.size(), engine.SkipHeapContentInfoSubRecord(reader, heap)); +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/parser/mock/mock_engine.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/mock/mock_engine.cpp new file mode 100644 index 000000000..fe723881d --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/mock/mock_engine.cpp @@ -0,0 +1,225 @@ +#include "mock_engine.h" + +using namespace matrix::hprof::internal; + +namespace test::mock { + + MockEngineImpl::MockEngineImpl(size_t id_size) : + parser::HeapParserEngine(), + id_size_(id_size) {} + + void MockEngineImpl::Parse(reader::Reader &reader, heap::Heap &heap, + const parser::ExcludeMatcherGroup &exclude_matchers, + const HeapParserEngine &next) const { + } + + void MockEngineImpl::ParseHeader(reader::Reader &reader, + heap::Heap &heap) const { + reader.ReadNullTerminatedString(); + reader.Skip(sizeof(uint32_t) * 3); + } + + void MockEngineImpl::ParseStringRecord(reader::Reader &reader, heap::Heap &heap, size_t record_length) const { + reader.Skip(record_length); + } + + void MockEngineImpl::ParseLoadClassRecord(reader::Reader &reader, heap::Heap &heap, size_t record_length) const { + reader.Skip(record_length); + } + + void MockEngineImpl::ParseHeapContent(reader::Reader &reader, heap::Heap &heap, size_t record_length, + const parser::ExcludeMatcherGroup &, const HeapParserEngine &) const { + reader.Skip(record_length); + } + + void MockEngineImpl::LazyParse(heap::Heap &heap, const parser::ExcludeMatcherGroup &exclude_matcher_group) const { + } + + size_t MockEngineImpl::ParseHeapContentRootUnknownSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootJniGlobalSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_ + id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootJniLocalSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_ + sizeof(uint32_t) + sizeof(uint32_t); + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootJavaFrameSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_ + sizeof(uint32_t) + sizeof(uint32_t); + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootNativeStackSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_ + sizeof(uint32_t); + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootStickyClassSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootThreadBlockSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_ + sizeof(uint32_t); + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootMonitorUsedSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootThreadObjectSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_ + sizeof(uint32_t) + sizeof(uint32_t); + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootInternedStringSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootFinalizingSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootDebuggerSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t + MockEngineImpl::ParseHeapContentRootReferenceCleanupSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootVMInternalSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootJniMonitorSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_ + sizeof(uint32_t) + sizeof(uint32_t); + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentRootUnreachableSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_; + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::ParseHeapContentClassSubRecord(reader::Reader &reader, heap::Heap &heap, + const parser::ExcludeMatcherGroup &exclude_matcher_group) const { + // class ID + stack trace serial number + super class ID + class loader object ID + + // signers object ID + protection domain object ID + reserved * 2 + instance size + const size_t skip_header_size = id_size_ + sizeof(uint32_t) + id_size_ * 6 + sizeof(uint32_t); + reader.Skip(skip_header_size); + + // Skip constant pool. + size_t skip_constant_pool_size = 0; + { + const uint16_t constant_pool_count = reader.ReadU2(); // constant pool elements count + skip_constant_pool_size += sizeof(uint16_t); + for (size_t i = 0; i < constant_pool_count; ++i) { + reader.SkipU2(); // constant pool index + skip_constant_pool_size += sizeof(uint16_t); + const heap::value_type_t type = heap::value_type_cast(reader.ReadU1()); // constant type + skip_constant_pool_size += sizeof(uint8_t); + const size_t type_size = + type == heap::value_type_t::kObject ? id_size_ : heap::get_value_type_size(type); + reader.Skip(type_size); // constant value + skip_constant_pool_size += type_size; + } + } + + // Skip static fields. + size_t skip_static_fields_size = 0; + { + const uint16_t static_fields_count = reader.ReadU2(); // static fields count + skip_static_fields_size += sizeof(uint16_t); + for (size_t i = 0; i < static_fields_count; ++i) { + reader.Skip(id_size_); // static field name ID + skip_static_fields_size += id_size_; + const heap::value_type_t type = heap::value_type_cast(reader.ReadU1()); // static field type + skip_static_fields_size += sizeof(uint8_t); + const size_t type_size = + type == heap::value_type_t::kObject ? id_size_ : heap::get_value_type_size(type); + reader.Skip(type_size); // static field value + skip_static_fields_size += type_size; + } + } + + // Skip instance fields. + const uint16_t instance_fields_count = reader.ReadU2(); // instance fields count + reader.Skip(instance_fields_count * (id_size_ + sizeof(uint8_t))); // instance fields elements + const size_t skip_instance_fields_size = + sizeof(uint16_t) + instance_fields_count * (id_size_ + sizeof(uint8_t)); + + return skip_header_size + skip_constant_pool_size + skip_static_fields_size + skip_instance_fields_size; + } + + size_t MockEngineImpl::ParseHeapContentInstanceSubRecord(reader::Reader &reader, heap::Heap &heap) const { + reader.Skip(id_size_); // object ID + reader.SkipU4(); // stack trace serial number + reader.Skip(id_size_); // class ID + const size_t fields_data_size = reader.ReadU4(); // fields data size + reader.Skip(fields_data_size); // fields data content + return id_size_ + sizeof(uint32_t) + id_size_ + sizeof(uint32_t) + fields_data_size; + } + + size_t MockEngineImpl::ParseHeapContentObjectArraySubRecord(reader::Reader &reader, heap::Heap &heap) const { + reader.Skip(id_size_); // array object ID + reader.SkipU4(); // stack trace serial number + const size_t array_length = reader.ReadU4(); // array length + reader.Skip(id_size_); // array element class ID + reader.Skip(array_length * id_size_); // array content + return id_size_ + sizeof(uint32_t) + sizeof(uint32_t) + id_size_ + array_length * id_size_; + } + + size_t MockEngineImpl::ParseHeapContentPrimitiveArraySubRecord(reader::Reader &reader, heap::Heap &heap) const { + reader.Skip(id_size_); // array object ID + reader.SkipU4(); // stack trace serial number + const size_t array_length = reader.ReadU4(); // array length + const heap::value_type_t type = heap::value_type_cast(reader.ReadU1()); // array type + const size_t type_size = heap::get_value_type_size(type); + reader.Skip(array_length * type_size); // array content + return id_size_ + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint8_t) + array_length * type_size; + } + + size_t + MockEngineImpl::ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader::Reader &reader, heap::Heap &heap) const { + const size_t skip_size = id_size_ + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint8_t); + reader.Skip(skip_size); + return skip_size; + } + + size_t MockEngineImpl::SkipHeapContentInfoSubRecord(reader::Reader &reader, const heap::Heap &heap) const { + const size_t skip_size = sizeof(uint32_t) + id_size_; + reader.Skip(skip_size); + return skip_size; + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/parser/mock/mock_engine.h b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/mock/mock_engine.h new file mode 100644 index 000000000..43c6973a4 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/mock/mock_engine.h @@ -0,0 +1,382 @@ +#ifndef __matrix_hprof_analyzer_parser_test_mock_engine_h__ +#define __matrix_hprof_analyzer_parser_test_mock_engine_h__ + +#include "gmock/gmock.h" + +#include "engine.h" + +using namespace matrix::hprof::internal; + +namespace test::mock { + + class MockEngineImpl final : public parser::HeapParserEngine { + public: + explicit MockEngineImpl(size_t id_size); + + void Parse(reader::Reader &reader, heap::Heap &heap, const parser::ExcludeMatcherGroup &exclude_matchers, + const HeapParserEngine &next) const override; + + void ParseHeader(reader::Reader &reader, heap::Heap &heap) const override; + + void ParseStringRecord(reader::Reader &reader, heap::Heap &heap, size_t record_length) const override; + + void ParseLoadClassRecord(reader::Reader &reader, heap::Heap &heap, size_t record_length) const override; + + void ParseHeapContent(reader::Reader &reader, heap::Heap &heap, size_t record_length, + const parser::ExcludeMatcherGroup &exclude_matchers, + const HeapParserEngine &next) const override; + + void LazyParse(heap::Heap &heap, const parser::ExcludeMatcherGroup &exclude_matcher_group) const override; + + size_t ParseHeapContentRootUnknownSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootJniGlobalSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootJniLocalSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootJavaFrameSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootNativeStackSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootStickyClassSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootThreadBlockSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootMonitorUsedSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootThreadObjectSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootInternedStringSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootFinalizingSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootDebuggerSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootReferenceCleanupSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootVMInternalSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootJniMonitorSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentRootUnreachableSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentClassSubRecord(reader::Reader &reader, heap::Heap &heap, + const parser::ExcludeMatcherGroup &exclude_matcher_group) const override; + + size_t ParseHeapContentInstanceSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentObjectArraySubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t ParseHeapContentPrimitiveArraySubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t + ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader::Reader &reader, heap::Heap &heap) const override; + + size_t SkipHeapContentInfoSubRecord(reader::Reader &reader, const heap::Heap &heap) const override; + + private: + size_t id_size_; + }; + + class MockEngine : public parser::HeapParserEngine { + public: + explicit MockEngine(size_t id_size) : impl_(id_size) { + ON_CALL(*this, Parse) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap, + const parser::ExcludeMatcherGroup &exclude_matchers, + const HeapParserEngine &next) { + impl_.Parse(reader, heap, exclude_matchers, next); + } + ); + + ON_CALL(*this, ParseHeader) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + impl_.ParseHeader(reader, heap); + } + ); + + ON_CALL(*this, ParseStringRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap, size_t record_length) { + impl_.ParseStringRecord(reader, heap, record_length); + } + ); + + ON_CALL(*this, ParseLoadClassRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap, size_t record_length) { + impl_.ParseLoadClassRecord(reader, heap, record_length); + } + ); + + ON_CALL(*this, ParseHeapContent) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap, size_t record_length, + const parser::ExcludeMatcherGroup &exclude_matchers, const HeapParserEngine &next) { + impl_.ParseHeapContent(reader, heap, record_length, exclude_matchers, next); + } + ); + + ON_CALL(*this, LazyParse) + .WillByDefault( + [this](heap::Heap &heap, const parser::ExcludeMatcherGroup &exclude_matchers) { + impl_.LazyParse(heap, exclude_matchers); + } + ); + + ON_CALL(*this, ParseHeapContentRootUnknownSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootUnknownSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootJniGlobalSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootJniGlobalSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootJniLocalSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootJniLocalSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootJavaFrameSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootJavaFrameSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootNativeStackSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootNativeStackSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootStickyClassSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootStickyClassSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootThreadBlockSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootThreadBlockSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootMonitorUsedSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootMonitorUsedSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootThreadObjectSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootThreadObjectSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootInternedStringSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootInternedStringSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootFinalizingSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootFinalizingSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootDebuggerSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootDebuggerSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootReferenceCleanupSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootReferenceCleanupSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootVMInternalSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootVMInternalSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootJniMonitorSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootJniMonitorSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentRootUnreachableSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentRootUnreachableSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentClassSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap, + const parser::ExcludeMatcherGroup &exclude_matcher_group) { + return impl_.ParseHeapContentClassSubRecord(reader, heap, exclude_matcher_group); + } + ); + + ON_CALL(*this, ParseHeapContentInstanceSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentInstanceSubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentObjectArraySubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentObjectArraySubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentPrimitiveArraySubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentPrimitiveArraySubRecord(reader, heap); + } + ); + + ON_CALL(*this, ParseHeapContentPrimitiveArrayNoDataDumpSubRecord) + .WillByDefault( + [this](reader::Reader &reader, heap::Heap &heap) { + return impl_.ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader, heap); + } + ); + + ON_CALL(*this, SkipHeapContentInfoSubRecord) + .WillByDefault( + [this](reader::Reader &reader, const heap::Heap &heap) { + return impl_.SkipHeapContentInfoSubRecord(reader, heap); + } + ); + } + + MOCK_METHOD(void, Parse, + ((reader::Reader & reader), (heap::Heap & heap), + (const parser::ExcludeMatcherGroup &exclude_matcher_group), (const HeapParserEngine &next)), + (const, override)); + + MOCK_METHOD(void, ParseHeader, ((reader::Reader & reader), (heap::Heap & heap)), (const, override)); + + MOCK_METHOD(void, ParseStringRecord, ((reader::Reader & reader), (heap::Heap & heap), (size_t)), + (const, override)); + + MOCK_METHOD(void, ParseLoadClassRecord, + ((reader::Reader & reader), (heap::Heap & heap), (size_t)), (const, override)); + + MOCK_METHOD(void, ParseHeapContent, + ((reader::Reader & reader), (heap::Heap & heap), (size_t), + (const parser::ExcludeMatcherGroup &exclude_matcher_group), (const HeapParserEngine &next)), + (const, override)); + + MOCK_METHOD(void, LazyParse, ((heap::Heap & heap),(const parser::ExcludeMatcherGroup &exclude_matcher_group)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootUnknownSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootJniGlobalSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootJniLocalSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootJavaFrameSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootNativeStackSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootStickyClassSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootThreadBlockSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootMonitorUsedSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootThreadObjectSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootInternedStringSubRecord, + ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootFinalizingSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootDebuggerSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootReferenceCleanupSubRecord, + ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootVMInternalSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootJniMonitorSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentRootUnreachableSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentClassSubRecord, + ((reader::Reader & reader), (heap::Heap & heap), + (const parser::ExcludeMatcherGroup &exclude_matcher_group)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentInstanceSubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentObjectArraySubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentPrimitiveArraySubRecord, ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, ParseHeapContentPrimitiveArrayNoDataDumpSubRecord, + ((reader::Reader & reader), (heap::Heap & heap)), + (const, override)); + + MOCK_METHOD(size_t, SkipHeapContentInfoSubRecord, ((reader::Reader & reader),(const heap::Heap&)), + (const, override)); + + private: + MockEngineImpl impl_; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/parser/parser.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/parser.cpp new file mode 100644 index 000000000..b81f1413f --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/parser/parser.cpp @@ -0,0 +1,22 @@ +#include "parser.h" +#include "gtest/gtest.h" + +#include "reader.h" +#include "mock/mock_engine.h" + +using namespace test::mock; +using namespace testing; + +namespace matrix::hprof::internal::parser { + TEST(parser, parse) { + HeapParser parser(new NiceMock(sizeof(uint32_t))); + auto *mock_engine = reinterpret_cast *>(parser.engine_.get()); + EXPECT_CALL(*mock_engine, Parse(_, _, _, _)); + + reader::Reader unused_reader(nullptr, 0); + heap::Heap unused_heap; + ExcludeMatcherGroup exclude_matcher_group; + + parser.Parse(unused_reader, unused_heap, exclude_matcher_group); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/reader/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/test/reader/CMakeLists.txt new file mode 100644 index 000000000..42741646f --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/reader/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +add_executable(matrix_hprof_analyzer_reader_test reader.cpp) +target_link_libraries(matrix_hprof_analyzer_reader_test matrix_hprof_analyzer_reader) +target_link_libraries(matrix_hprof_analyzer_reader_test gtest_main) + +gtest_discover_tests(matrix_hprof_analyzer_reader_test) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/reader/reader.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/reader/reader.cpp new file mode 100644 index 000000000..681d0ea74 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/reader/reader.cpp @@ -0,0 +1,111 @@ +#include "reader.h" +#include "gtest/gtest.h" + +using namespace matrix::hprof::internal::reader; + +TEST(buffer_reader, read_and_skip_big_endian_u1) { + const uint8_t buffer[] = {0x01, 0x02}; + Reader reader(buffer, sizeof(buffer)); + reader.SkipU1(); + EXPECT_EQ(2, reader.ReadU1()); +} + +TEST(buffer_reader, read_and_skip_big_endian_u2) { + const uint8_t buffer[] = {0x00, 0x01, 0x00, 0x02}; + Reader reader(buffer, sizeof(buffer)); + reader.SkipU2(); + EXPECT_EQ(2, reader.ReadU2()); +} + +TEST(buffer_reader, read_and_skip_big_endian_u4) { + const uint8_t buffer[] = {0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}; + Reader reader(buffer, sizeof(buffer)); + reader.SkipU4(); + EXPECT_EQ(2, reader.ReadU4()); +} + +TEST(buffer_reader, read_and_skip_big_endian_u8) { + const uint8_t buffer[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}; + Reader reader(buffer, sizeof(buffer)); + reader.SkipU8(); + EXPECT_EQ(2, reader.ReadU8()); +} + +TEST(buffer_reader, read_big_endian_typed) { + const uint8_t buffer[] = { + 0x01, // bool: true + 0x00, 0x01, // short: 1 + 0x3f, 0x80, 0x00, 0x00, // float: 1.0 + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // double: 2.0 + }; + Reader reader(buffer, sizeof(buffer)); + EXPECT_EQ(true, reader.ReadTyped(sizeof(uint8_t))); + EXPECT_EQ(1, reader.ReadTyped(sizeof(uint16_t))); + EXPECT_EQ(1.0, reader.ReadTyped(sizeof(uint32_t))); + EXPECT_EQ(2.0, reader.ReadTyped(sizeof(uint64_t))); + EXPECT_THROW(reader.ReadTyped(sizeof(uint64_t) + sizeof(uint64_t)), std::runtime_error); +} + +TEST(buffer_reader, skip_dynamic) { + const uint8_t buffer[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + }; + Reader reader(buffer, sizeof(buffer)); + reader.Skip(15); + EXPECT_EQ(1, reader.ReadU1()); +} + +TEST(buffer_reader, buffer_overflow) { + const uint8_t buffer[] = {0x00}; + // Read end of buffer. + { + Reader reader(buffer, sizeof(buffer)); + reader.SkipU1(); + EXPECT_THROW(reader.ReadU1(), std::runtime_error); + } + // Read out of buffer. + { + Reader reader(buffer, sizeof(buffer)); + EXPECT_THROW(reader.ReadU2(), std::runtime_error); + } + // Skip end of buffer. + { + Reader reader(buffer, sizeof(buffer)); + reader.SkipU1(); + EXPECT_THROW(reader.SkipU1(), std::runtime_error); + } + // Skip out of buffer. + { + Reader reader(buffer, sizeof(buffer)); + EXPECT_THROW(reader.SkipU2(), std::runtime_error); + } +} + +TEST(buffer_reader, read_string) { + const std::string buffer = "Hello world!"; + Reader reader(reinterpret_cast(buffer.data()), buffer.size()); + EXPECT_EQ(buffer, reader.ReadString(buffer.size())); +} + +TEST(buffer_reader, read_null_terminated_string) { + const uint8_t buffer[] = { + // string: "Hello world!" + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, + 0x72, 0x6c, 0x64, 0x21, 0x00, + // byte: 1 + 0x01 + }; + Reader reader(buffer, sizeof(buffer)); + EXPECT_EQ("Hello world!", reader.ReadNullTerminatedString()); + EXPECT_EQ(1, reader.ReadU1()); +} + +TEST(buffer_reader, reset_cursor) { + const uint8_t buffer[] = {0x01, 0x02}; + Reader reader(buffer, sizeof(buffer)); + reader.SkipU1(); + reader.ResetCursor(); + EXPECT_EQ(1, reader.ReadU1()); +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/tools/CMakeLists.txt b/matrix/matrix-android/matrix-hprof-analyzer/test/tools/CMakeLists.txt new file mode 100644 index 000000000..af7a07532 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/tools/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix_hprof_analyzer) + +set(CMAKE_CXX_STANDARD 17) + +add_library(test_tools STATIC buffer_generator.cpp) +target_include_directories(test_tools INTERFACE include) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/tools/buffer_generator.cpp b/matrix/matrix-android/matrix-hprof-analyzer/test/tools/buffer_generator.cpp new file mode 100644 index 000000000..924572454 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/tools/buffer_generator.cpp @@ -0,0 +1,28 @@ +#include "include/buffer_generator.h" + +#include + +namespace test::tools { + + void BufferGenerator::WriteZero(size_t size) { + for (size_t i = 0; i < size; ++i) { + buffer_.sputc(0); + } + } + + void BufferGenerator::WriteString(const std::string &value) { + buffer_.sputn(value.c_str(), static_cast(value.length())); + } + + void BufferGenerator::WriteNullTerminatedString(const std::string &value) { + WriteString(value); + const size_t length = value.length(); + if (value[length - 1] != '\0') { + buffer_.sputc('\0'); + } + } + + std::string BufferGenerator::GetContent() const { + return std::move(buffer_.str()); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hprof-analyzer/test/tools/include/buffer_generator.h b/matrix/matrix-android/matrix-hprof-analyzer/test/tools/include/buffer_generator.h new file mode 100644 index 000000000..090c81cf0 --- /dev/null +++ b/matrix/matrix-android/matrix-hprof-analyzer/test/tools/include/buffer_generator.h @@ -0,0 +1,40 @@ +#ifndef __matrix_hprof_analyzer_test_tools_buffer_generator_h__ +#define __matrix_hprof_analyzer_test_tools_buffer_generator_h__ + +#include + +namespace test::tools { + class BufferGenerator { + public: + template + void Write(T value) { +#pragma clang diagnostic push +#pragma ide diagnostic ignored "Simplify" +#pragma ide diagnostic ignored "UnreachableCode" + if (BYTE_ORDER == BIG_ENDIAN) { + buffer_.sputn(reinterpret_cast(&value), sizeof(T)); + } else { + const size_t size = sizeof(T); + std::unique_ptr temp = std::make_unique(size); + for (size_t i = 0; i < size; ++i) { + temp[i] = reinterpret_cast(&value)[size - 1 - i]; + } + buffer_.sputn(reinterpret_cast(temp.get()), static_cast(size)); + } +#pragma clang diagnostic pop + } + + void WriteZero(size_t size); + + void WriteString(const std::string &value); + + void WriteNullTerminatedString(const std::string &value); + + std::string GetContent() const; + + private: + std::stringbuf buffer_; + }; +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-io-canary/CMakeLists.txt b/matrix/matrix-android/matrix-io-canary/CMakeLists.txt index 2ea1ec6f3..7e653f3cb 100644 --- a/matrix/matrix-android/matrix-io-canary/CMakeLists.txt +++ b/matrix/matrix-android/matrix-io-canary/CMakeLists.txt @@ -33,4 +33,5 @@ find_library(log-lib target_link_libraries(io-canary PRIVATE ${log-lib} - PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a) \ No newline at end of file + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libsemi_dlfcn.a) diff --git a/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeCtl.cpp b/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeCtl.cpp deleted file mode 100644 index d79cd10c1..000000000 --- a/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeCtl.cpp +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making wechat-matrix available. - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * 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. - */ - -// -// Created by Yves on 2020/7/15. -// - -#include -#include -#include -#include -#include -#include "EnhanceDlsym.h" -#include "JeLog.h" -#include "JeHooks.h" -#include -//#include "internal/arena_structs_b.h" - -#define JECTL_OK 0 -#define ERR_INIT_FAILED 1 -#define ERR_VERSION 2 -#define ERR_64_BIT 3 -#define ERR_CTL 4 -#define ERR_ALLOC_FAILED 5 - -#define CACHELINE 64 - -#define TAG "Matrix.JeCtl" - -#ifdef __cplusplus -extern "C" { -#endif - -typedef int (*mallctl_t)(const char *name, - void *oldp, - size_t *oldlenp, - void *newp, - size_t newlen); - -typedef void *(*arena_extent_alloc_large_t)(void *tsdn, - void *arena, - size_t usize, - size_t alignment, - bool *zero); - -typedef void (*large_dalloc_t)(void *tsdn, void *extent); - -typedef void *(*arena_choose_hard_t)(void *tsd, bool internal); - -typedef void (*arena_extent_dalloc_large_prep_t)(void *tsdn, void *arena, void *extent); - -typedef void (*arena_extents_dirty_dalloc_t)(void *tsdn, void *arena, - extent_hooks_t **r_extent_hooks, void *extent); - -#define MAX_RETRY_TIMES 10 - -void *handle = nullptr; -bool initialized = false; - -mallctl_t mallctl = nullptr; -arena_extent_alloc_large_t arena_extent_alloc_large = nullptr; -large_dalloc_t large_dalloc = nullptr; -arena_choose_hard_t arena_choose_hard = nullptr; -arena_extent_dalloc_large_prep_t arena_extent_dalloc_large_prep = nullptr; -arena_extents_dirty_dalloc_t arena_extents_dirty_dalloc = nullptr; -bool * p_je_opt_retain = nullptr; - - -static inline bool end_with(std::string const &value, std::string const &ending) { - if (ending.size() > value.size()) { - return false; - } - return std::equal(ending.rbegin(), ending.rend(), value.rbegin()); -} - -static bool init() { - - handle = enhance::dlopen("libc.so", 0); - - if (handle == nullptr) { - return false; - } - - mallctl = (mallctl_t) enhance::dlsym(handle, "je_mallctl"); - - if (!mallctl) { - return false; - } - - const char *version; - size_t size = sizeof(version); - mallctl("version", &version, &size, nullptr, 0); - LOGD(TAG, "jemalloc version: %s", version); - - if (0 != strncmp(version, "5.1.0", 5)) { - return false; - } - -// Dl_info mallctl_info{}; -// if (0 == dladdr((void *) mallctl, &mallctl_info) -// || !end_with(mallctl_info.dli_fname, "/libc.so")) { -// LOGD(TAG, "mallctl = %p, is a fault address, fname = %s, sname = %s", mallctl, -// mallctl_info.dli_fname, mallctl_info.dli_sname); -// mallctl = nullptr; -// return false; -// } - - p_je_opt_retain = (bool *) enhance::dlsym(handle, "je_opt_retain"); - if (!p_je_opt_retain) { - return false; - } - - arena_extent_alloc_large = - (arena_extent_alloc_large_t) enhance::dlsym(handle, "je_arena_extent_alloc_large"); - if (!arena_extent_alloc_large) { - return false; - } - - arena_choose_hard = (arena_choose_hard_t) enhance::dlsym(handle, "je_arena_choose_hard"); - if (!arena_choose_hard) { - return false; - } - - large_dalloc = (large_dalloc_t) enhance::dlsym(handle, "je_large_dalloc"); - if (!large_dalloc) { - return false; - } - - arena_extent_dalloc_large_prep = (arena_extent_dalloc_large_prep_t) enhance::dlsym(handle, - "je_arena_extent_dalloc_large_prep"); - if (!arena_extent_dalloc_large_prep) { - return false; - } - - arena_extents_dirty_dalloc = (arena_extents_dirty_dalloc_t) enhance::dlsym(handle, - "je_arena_extents_dirty_dalloc"); - if (!arena_extents_dirty_dalloc) { - return false; - } - - return true; -} - -static void flush_decay_purge() { - assert(mallctl != nullptr); - mallctl("thread.tcache.flush", nullptr, nullptr, nullptr, 0); - mallctl("arena.0.decay", nullptr, nullptr, nullptr, 0); - mallctl("arena.1.decay", nullptr, nullptr, nullptr, 0); - mallctl("arena.0.purge", nullptr, nullptr, nullptr, 0); - mallctl("arena.1.purge", nullptr, nullptr, nullptr, 0); -} - -static void enable_dss() { - int err = 0; - - char *stat_dss; - size_t stat_dss_size = sizeof(char *); - mallctl("stats.arenas.0.dss", &stat_dss, &stat_dss_size, nullptr, 0); - LOGD(TAG, "stat_dss.0 = %s", stat_dss); - - mallctl("stats.arenas.1.dss", &stat_dss, &stat_dss_size, nullptr, 0); - LOGD(TAG, "stat_dss.1 = %s", stat_dss); - -// *opt_retain = false; - const char *setting = "primary"; - size_t setting_size = sizeof(const char); - char *old_setting; - size_t old_setting_size = sizeof(char *); - mallctl("arena.0.dss", &old_setting, &old_setting_size, &setting, sizeof(const char *)); - mallctl("arena.1.dss", &old_setting, &old_setting_size, &setting, 8); - - mallctl("stats.arenas.0.dss", &stat_dss, &stat_dss_size, nullptr, 0); - LOGD(TAG, "after stat_dss.0 = %s", stat_dss); - - mallctl("stats.arenas.1.dss", &stat_dss, &stat_dss_size, nullptr, 0); - LOGD(TAG, "after stat_dss.1 = %s", stat_dss); - - bool old_val; - size_t old_val_size = sizeof(bool); - bool new_val = false; - mallctl("thread.tcache.enabled", &old_val, &old_val_size, &new_val, sizeof(new_val)); - LOGD(TAG, "thread.tcache.enabled: %d", old_val); - mallctl("thread.tcache.enabled", &old_val, &old_val_size, nullptr, 0); - LOGD(TAG, "thread.tcache.enabled: %d", old_val); - - ssize_t dirty_ms; - size_t dirty_ms_size = sizeof(ssize_t); - ssize_t new_dirty_ms = 0; - mallctl("arena.0.dirty_decay_ms", &dirty_ms, &dirty_ms_size, &new_dirty_ms, sizeof(ssize_t)); - LOGD(TAG, "arena.0.dirty_decay_ms: %zu", dirty_ms); - mallctl("arena.0.dirty_decay_ms", &dirty_ms, &dirty_ms_size, nullptr, 0); - LOGD(TAG, "arena.0.dirty_decay_ms: %zu", dirty_ms); - - bool background; - size_t background_size = sizeof(bool); - bool new_background = true; - err = mallctl("background_thread", &background, &background_size, &new_background, - background_size); - LOGD(TAG, "background_thread = %d, err = %d", background, err); - err = mallctl("background_thread", &background, &background_size, - nullptr, 0); - LOGD(TAG, "background_thread = %d, err = %d", background, err); - - bool opt_background; - size_t opt_background_size = sizeof(bool); - mallctl("opt.background_thread", &opt_background, &opt_background_size, nullptr, 0); - LOGD(TAG, "opt.background_thread = %d", opt_background); - - -} - -JNIEXPORT void JNICALL -Java_com_tencent_matrix_jectl_JeCtl_initNative(JNIEnv *env, jclass clazz) { -#ifdef __LP64__ - return ; -#else - if (!initialized) { - initialized = init(); - } -#endif -} - -JNIEXPORT jint JNICALL -Java_com_tencent_matrix_jectl_JeCtl_compactNative(JNIEnv *env, jclass clazz) { - -#ifdef __LP64__ - return ERR_64_BIT; -#else - - if (!initialized) { - return ERR_INIT_FAILED; - } - assert(mallctl != nullptr); - - flush_decay_purge(); - - return JECTL_OK; -#endif -} - -static void call_alloc_large_in_arena1(size_t __size) { - // 延迟时机, 失败也不影响 arena0 的预分配 - auto tsd_tsd = (pthread_t *) enhance::dlsym(handle, "je_tsd_tsd"); - if (!tsd_tsd) { - LOGE(TAG, "tsd_tsd not found"); - return; - } - - auto tsd = pthread_getspecific(*tsd_tsd); - if (tsd == nullptr) { - LOGE(TAG, "tsd id null"); - return; - } - - void *arena1 = arena_choose_hard(tsd, false); // choose 另一个 arena - - unsigned which_arena = 0; - size_t which_arena_size = sizeof(unsigned); - mallctl("thread.arena", &which_arena, &which_arena_size, nullptr, 0); - - bool zero = false; - LOGD(TAG, "args : tsd=%p, arena1=%p, size=%zu, align=%d, zero=%p", tsd, (void *) arena1, __size, - CACHELINE, &zero); - void *extent = arena_extent_alloc_large(tsd, (void *) arena1, __size, CACHELINE, &zero); -// large_dalloc(tsd, extent); - - arena_extent_dalloc_large_prep(tsd, arena1, extent); - extent_hooks_t *hooks = nullptr; - arena_extents_dirty_dalloc(tsd, arena1, &hooks, extent); -} - -static void *sub_routine(void *arg) { - LOGD(TAG, "arg = %zu ", *((size_t *) arg)); - - void *p = malloc(1024);// 确保当前线程的 tsd 已经初始化 - - unsigned which_arena = 0; - size_t which_arena_size = sizeof(unsigned); - - int ret = mallctl("thread.arena", &which_arena, &which_arena_size, nullptr, - 0); - LOGD(TAG, "thread.arena: which_arena = %u, ret = %d", which_arena, ret); - - if (which_arena == 0) { - call_alloc_large_in_arena1(*(size_t *) arg); - free(p); - free(arg); - flush_decay_purge(); - return nullptr; - } - - pthread_t next_thread; - pthread_create(&next_thread, nullptr, sub_routine, arg); - - pthread_join(next_thread, nullptr); - - free(p); - return nullptr; -} - -// Android: Force all huge allocations to always take place in the first arena. -// see: https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:external/jemalloc_new/src/large.c;l=46 -// so we have to hack jemalloc to make sure that the large allocation takes place in second arena -static int hack_prealloc_within_arena1(size_t size) { - LOGD(TAG, "hack_arena1"); - if (!initialized) { - LOGD(TAG, "hack_arena1:init fialed"); - return ERR_INIT_FAILED; - } - - LOGD(TAG, "size1 = %zu", size); - auto p_size = (size_t *) malloc(sizeof(size_t)); - *p_size = size; - - pthread_t next_thread; - pthread_create(&next_thread, nullptr, sub_routine, p_size); - - return JECTL_OK; -} - -void *arena0_alloc_opt_prevent; - -JNIEXPORT jint JNICALL -Java_com_tencent_matrix_jectl_JeCtl_preAllocRetainNative(JNIEnv *env, jclass clazz, jint __size0, - jint __size1, jint __limit0, - jint __limit1) { - -#ifdef __LP64__ - return ERR_64_BIT; -#else - if (!initialized) { - return ERR_INIT_FAILED; - } - assert(mallctl != nullptr); - - assert(__size0 > 0); - assert(__size1 > 0); - assert(__limit0 > 0); - assert(__limit1 > 0); - - int ctl_result = 0; - int ret_code = JECTL_OK; - - size_t dirty_ms; - size_t new_dirty_ms = 0; - size_t ms_size = sizeof(size_t); - ctl_result = mallctl("arena.0.muzzy_decay_ms", &dirty_ms, &ms_size, &new_dirty_ms, ms_size); - LOGD(TAG, "arena.0.muzzy_decay_ms ret = %d", ctl_result); - ctl_result = mallctl("arena.1.muzzy_decay_ms", &dirty_ms, &ms_size, &new_dirty_ms, ms_size); - LOGD(TAG, "arena.1.muzzy_decay_ms ret = %d", ctl_result); - -// ret = mallctl("arena.0.dirty_decay_ms", &dirty_ms, &dirty_ms_size, &new_dirty_ms, dirty_ms_size); -// LOGD(TAG, "arena.0.dirty_decay_ms ret = %d", ret); -// ret = mallctl("arena.1.dirty_decay_ms", &dirty_ms, &dirty_ms_size, &new_dirty_ms, dirty_ms_size); -// LOGD(TAG, "arena.1.dirty_decay_ms ret = %d", ret); - - size_t old_limit = 0; - size_t new_limit = __limit0; - size_t limit_size = sizeof(size_t); - ctl_result = mallctl("arena.0.retain_grow_limit", &old_limit, &limit_size, &new_limit, - limit_size); - LOGD(TAG, "arena.0.retain_grow_limit ret = %d, old limit = %zu", ctl_result, old_limit); - new_limit = __limit1; - ctl_result = mallctl("arena.1.retain_grow_limit", &old_limit, &limit_size, &new_limit, - limit_size); - LOGD(TAG, "arena.1.retain_grow_limit ret = %d, old limit = %zu", ctl_result, old_limit); - - LOGD(TAG, "prepare alloc"); - void *p = malloc(__size0); - if (!p) { - ret_code = ERR_ALLOC_FAILED; - } - arena0_alloc_opt_prevent = p; - LOGD(TAG, "prepare alloc arena0 done %p", p); - free(p); - hack_prealloc_within_arena1(__size1); - - flush_decay_purge(); - - return ret_code; -#endif -} - -JNIEXPORT jstring JNICALL -Java_com_tencent_matrix_jectl_JeCtl_getVersionNative(JNIEnv *env, jclass clazz) { -#ifdef __LP64__ - return env->NewStringUTF("64-bit"); -#else - if (!mallctl) { - return env->NewStringUTF("Error"); - } - - const char *version; - size_t size = sizeof(version); - mallctl("version", &version, &size, nullptr, 0); - LOGD(TAG, "jemalloc version: %s", version); - - return env->NewStringUTF(version); -#endif -} - -JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_jectl_JeCtl_setRetain(JNIEnv *env, jclass clazz, jboolean enable) { -#ifdef __LP64__ - return true; -#else - bool old = true; - if (initialized && p_je_opt_retain) { - old = *p_je_opt_retain; - *p_je_opt_retain = enable; - LOGD(TAG, "retain = %d", *p_je_opt_retain); - } - return old; -#endif -} - -#ifdef __cplusplus -} -#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeHooks.cpp b/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeHooks.cpp deleted file mode 100644 index 915a007d9..000000000 --- a/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeHooks.cpp +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making wechat-matrix available. - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * 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. - */ - -// -// Created by Yves on 2020/8/20. -// - -#include -#include "JeHooks.h" -#include "JeLog.h" - -#define TAG "Matrix.JeCtl.Hooks" - -extent_hooks_t *origin_extent_hooks; -//bool arena_1 = false; - -void *hook_extent_alloc(extent_hooks_t *extent_hooks, void *new_addr, size_t size, - size_t alignment, bool *zero, bool *commit, unsigned arena_ind) { - if (arena_ind != 0) { -// arena_1 = true; - } - LOGD(TAG, "hook_extent_alloc: size = %zu, arena_ind = %u", size, arena_ind); // should not log here - return origin_extent_hooks->alloc(origin_extent_hooks, new_addr, size, alignment, zero, commit, arena_ind); -} - -bool hook_extent_dalloc(extent_hooks_t *extent_hooks, void *addr, size_t size, bool committed, - unsigned arena_ind) { - return origin_extent_hooks->dalloc(origin_extent_hooks, addr, size, committed, arena_ind); -} - - -void hook_extent_destroy(extent_hooks_t *extent_hooks, void *addr, size_t size, bool committed, - unsigned arena_ind) { - origin_extent_hooks->destroy(origin_extent_hooks, addr, size, committed, arena_ind); -} - -bool hook_extent_commit(extent_hooks_t *extent_hooks, void *addr, size_t size, size_t offset, - size_t length, unsigned arena_ind) { - return origin_extent_hooks->commit(origin_extent_hooks, addr, size, offset, length, arena_ind); -} - - -bool hook_extent_decommit(extent_hooks_t *extent_hooks, void *addr, size_t size, size_t offset, - size_t length, unsigned arena_ind) { - return origin_extent_hooks->decommit(origin_extent_hooks, addr, size, offset, length, arena_ind); -} - -bool hook_extent_purge_lazy(extent_hooks_t *extent_hooks, void *addr, size_t size, size_t offset, - size_t length, unsigned arena_ind) { - if (origin_extent_hooks->purge_lazy) { - return origin_extent_hooks->purge_lazy(origin_extent_hooks, addr, size, offset, length, - arena_ind); - } - return true; // 当 default_extent_hooks->purge_lazy == NULL, 返回 true 以保持 jemalloc 的逻辑一致 -} - -bool hook_extent_purge_forced(extent_hooks_t *extent_hooks, void *addr, size_t size, size_t offset, - size_t length, unsigned arena_ind) { - if (origin_extent_hooks->purge_forced) { - return origin_extent_hooks->purge_forced(origin_extent_hooks, addr, size, offset, length, - arena_ind); - } - return true; // 当 default_extent_hooks->purge_forced == NULL, 返回 true 以保持 jemalloc 的逻辑一致 -} - -extent_split_t *original_split; - -bool hook_extent_split(extent_hooks_t *extent_hooks, void *addr, size_t size, size_t size_a, - size_t size_b, bool committed, unsigned arena_ind) { -// bool ret = original_split(origin_extent_hooks, addr, size, size_a, size_b, committed, arena_ind); -// LOGD(TAG, "hook_extent_split: arena_ind = %u, ret = %d", arena_ind, /*ret*/0); -// return ret; - return origin_extent_hooks->split(origin_extent_hooks, addr, size, size_a, size_b, committed, arena_ind); -} - - -bool hook_extent_merge(extent_hooks_t *extent_hooks, void *addr_a, size_t size_a, void *addr_b, - size_t size_b, bool committed, unsigned arena_ind) { - return origin_extent_hooks->merge(origin_extent_hooks, addr_a, size_a, addr_b, size_b, committed, - arena_ind); -} - -extent_hooks_t extent_hooks = { - hook_extent_alloc, - hook_extent_dalloc, - hook_extent_destroy, - hook_extent_commit, - hook_extent_decommit, - hook_extent_purge_lazy, - hook_extent_purge_forced, - hook_extent_split, - hook_extent_merge -}; \ No newline at end of file diff --git a/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeHooks.h b/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeHooks.h deleted file mode 100644 index da5fac90f..000000000 --- a/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeHooks.h +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making wechat-matrix available. - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * 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. - */ - -// -// Created by Yves on 2020/8/20. -// - -#ifndef LIBMATRIX_JNI_JEHOOKS_H -#define LIBMATRIX_JNI_JEHOOKS_H - -typedef struct extent_hooks_s extent_hooks_t; - -extern extent_hooks_t *origin_extent_hooks; -extern extent_hooks_t extent_hooks; - -/* - * void * - * extent_alloc(extent_hooks_t *extent_hooks, void *new_addr, size_t size, - * size_t alignment, bool *zero, bool *commit, unsigned arena_ind); - */ -typedef void *(extent_alloc_t)(extent_hooks_t *, void *, size_t, size_t, bool *, - bool *, unsigned); - -/* - * bool - * extent_dalloc(extent_hooks_t *extent_hooks, void *addr, size_t size, - * bool committed, unsigned arena_ind); - */ -typedef bool (extent_dalloc_t)(extent_hooks_t *, void *, size_t, bool, - unsigned); - -/* - * void - * extent_destroy(extent_hooks_t *extent_hooks, void *addr, size_t size, - * bool committed, unsigned arena_ind); - */ -typedef void (extent_destroy_t)(extent_hooks_t *, void *, size_t, bool, - unsigned); - -/* - * bool - * extent_commit(extent_hooks_t *extent_hooks, void *addr, size_t size, - * size_t offset, size_t length, unsigned arena_ind); - */ -typedef bool (extent_commit_t)(extent_hooks_t *, void *, size_t, size_t, size_t, - unsigned); - -/* - * bool - * extent_decommit(extent_hooks_t *extent_hooks, void *addr, size_t size, - * size_t offset, size_t length, unsigned arena_ind); - */ -typedef bool (extent_decommit_t)(extent_hooks_t *, void *, size_t, size_t, - size_t, unsigned); - -/* - * bool - * extent_purge(extent_hooks_t *extent_hooks, void *addr, size_t size, - * size_t offset, size_t length, unsigned arena_ind); - */ -typedef bool (extent_purge_t)(extent_hooks_t *, void *, size_t, size_t, size_t, - unsigned); - -/* - * bool - * extent_split(extent_hooks_t *extent_hooks, void *addr, size_t size, - * size_t size_a, size_t size_b, bool committed, unsigned arena_ind); - */ -typedef bool (extent_split_t)(extent_hooks_t *, void *, size_t, size_t, size_t, - bool, unsigned); - -/* - * bool - * extent_merge(extent_hooks_t *extent_hooks, void *addr_a, size_t size_a, - * void *addr_b, size_t size_b, bool committed, unsigned arena_ind); - */ -typedef bool (extent_merge_t)(extent_hooks_t *, void *, size_t, void *, size_t, - bool, unsigned); - - -struct extent_hooks_s { - extent_alloc_t *alloc; - extent_dalloc_t *dalloc; - extent_destroy_t *destroy; - extent_commit_t *commit; - extent_decommit_t *decommit; - extent_purge_t *purge_lazy; - extent_purge_t *purge_forced; - extent_split_t *split; - extent_merge_t *merge; -}; - -#endif //LIBMATRIX_JNI_JEHOOKS_H diff --git a/matrix/matrix-android/matrix-jectl/src/main/java/com/tencent/matrix/jectl/JeCtl.java b/matrix/matrix-android/matrix-jectl/src/main/java/com/tencent/matrix/jectl/JeCtl.java deleted file mode 100644 index 2d7086954..000000000 --- a/matrix/matrix-android/matrix-jectl/src/main/java/com/tencent/matrix/jectl/JeCtl.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making wechat-matrix available. - * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the BSD 3-Clause License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/BSD-3-Clause - * - * 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 com.tencent.matrix.jectl; - - -import androidx.annotation.Keep; - -import com.tencent.matrix.util.MatrixLog; - -/** - * Created by Yves on 2020/7/15 - */ -public class JeCtl { - private static final String TAG = "Matrix.JeCtl"; - - private static boolean initialized = false; - - static { - try { - System.loadLibrary("matrix-jectl"); - initNative(); - initialized = true; - } catch (Throwable e) { - MatrixLog.printErrStackTrace(TAG, e, ""); - } - } - - // 必须和 native 保持一致 - public static final int JECTL_OK = 0; - public static final int ERR_INIT_FAILED = 1; - public static final int ERR_VERSION = 2; - public static final int ERR_64_BIT = 3; - public static final int ERR_CTL = 4; - public static final int ERR_ALLOC_FAILED = 5; - - public synchronized static int compact() { - if (!initialized) { - MatrixLog.e(TAG, "JeCtl init failed! check if so exists"); - return ERR_INIT_FAILED; - } - return compactNative(); - } - - private static boolean hasAllocated; - private static int sLastPreAllocRet; - - public synchronized static int preAllocRetain(int size0, int size1, int limit0, int limit1) { - if (!initialized) { - MatrixLog.e(TAG, "JeCtl init failed! check if so exists"); - return ERR_INIT_FAILED; - } - if (!hasAllocated) { - hasAllocated = true; - sLastPreAllocRet = preAllocRetainNative(size0, size1, limit0, limit1); - } - - return sLastPreAllocRet; - } - - public synchronized static String version() { - if (!initialized) { - MatrixLog.e(TAG, "JeCtl init failed! check if so exists"); - return "VER_UNKNOWN"; - } - - return getVersionNative(); - } - - @Keep - private static native void initNative(); - - @Keep - private static native int compactNative(); - - @Keep - private static native int preAllocRetainNative(int size0, int size1, int limit0, int limit1); - - @Keep - private static native String getVersionNative(); - - @Keep - public static native boolean setRetain(boolean enable); -} diff --git a/matrix/matrix-android/matrix-jectl/CMakeLists.txt b/matrix/matrix-android/matrix-mallctl/CMakeLists.txt similarity index 95% rename from matrix/matrix-android/matrix-jectl/CMakeLists.txt rename to matrix/matrix-android/matrix-mallctl/CMakeLists.txt index 931537f6f..cf1e64739 100644 --- a/matrix/matrix-android/matrix-jectl/CMakeLists.txt +++ b/matrix/matrix-android/matrix-mallctl/CMakeLists.txt @@ -10,14 +10,13 @@ cmake_minimum_required(VERSION 3.4.1) # and CMake builds them for you. When you build your app, Gradle # automatically packages shared libraries with your APK. -set(TARGET matrix-jectl) +set(TARGET matrix-mallctl) set(SOURCE_DIR src/main/cpp) set( SOURCE_FILES - ${SOURCE_DIR}/jectl/JeCtl.cpp - ${SOURCE_DIR}/jectl/JeHooks.cpp + ${SOURCE_DIR}/mallctl/MallCtl.cpp ) add_library( # Specifies the name of the library. diff --git a/matrix/matrix-android/matrix-jectl/build.gradle b/matrix/matrix-android/matrix-mallctl/build.gradle similarity index 100% rename from matrix/matrix-android/matrix-jectl/build.gradle rename to matrix/matrix-android/matrix-mallctl/build.gradle diff --git a/matrix/matrix-android/matrix-jectl/consumer-rules.pro b/matrix/matrix-android/matrix-mallctl/consumer-rules.pro similarity index 100% rename from matrix/matrix-android/matrix-jectl/consumer-rules.pro rename to matrix/matrix-android/matrix-mallctl/consumer-rules.pro diff --git a/matrix/matrix-android/matrix-jectl/gradle.properties b/matrix/matrix-android/matrix-mallctl/gradle.properties similarity index 53% rename from matrix/matrix-android/matrix-jectl/gradle.properties rename to matrix/matrix-android/matrix-mallctl/gradle.properties index f90e4fc10..3606fb76f 100644 --- a/matrix/matrix-android/matrix-jectl/gradle.properties +++ b/matrix/matrix-android/matrix-mallctl/gradle.properties @@ -1,2 +1,2 @@ POM_NAME=Matrix jectl for jemalloc -POM_ARTIFACT_ID=matrix-jectl \ No newline at end of file +POM_ARTIFACT_ID=matrix-mallctl \ No newline at end of file diff --git a/matrix/matrix-android/matrix-jectl/proguard-rules.pro b/matrix/matrix-android/matrix-mallctl/proguard-rules.pro similarity index 100% rename from matrix/matrix-android/matrix-jectl/proguard-rules.pro rename to matrix/matrix-android/matrix-mallctl/proguard-rules.pro diff --git a/matrix/matrix-android/matrix-jectl/src/androidTest/java/com/tencent/matrix/jectl/ExampleInstrumentedTest.java b/matrix/matrix-android/matrix-mallctl/src/androidTest/java/com/tencent/matrix/jectl/ExampleInstrumentedTest.java similarity index 100% rename from matrix/matrix-android/matrix-jectl/src/androidTest/java/com/tencent/matrix/jectl/ExampleInstrumentedTest.java rename to matrix/matrix-android/matrix-mallctl/src/androidTest/java/com/tencent/matrix/jectl/ExampleInstrumentedTest.java diff --git a/matrix/matrix-android/matrix-jectl/src/main/AndroidManifest.xml b/matrix/matrix-android/matrix-mallctl/src/main/AndroidManifest.xml similarity index 65% rename from matrix/matrix-android/matrix-jectl/src/main/AndroidManifest.xml rename to matrix/matrix-android/matrix-mallctl/src/main/AndroidManifest.xml index 1ad0a915f..37af2b71f 100644 --- a/matrix/matrix-android/matrix-jectl/src/main/AndroidManifest.xml +++ b/matrix/matrix-android/matrix-mallctl/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ + package="com.tencent.matrix.mallctl"> \ No newline at end of file diff --git a/matrix/matrix-android/matrix-mallctl/src/main/cpp/mallctl/MallCtl.cpp b/matrix/matrix-android/matrix-mallctl/src/main/cpp/mallctl/MallCtl.cpp new file mode 100644 index 000000000..9ed4bc4bd --- /dev/null +++ b/matrix/matrix-android/matrix-mallctl/src/main/cpp/mallctl/MallCtl.cpp @@ -0,0 +1,175 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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. + */ + +// +// Created by Yves on 2020/7/15. +// + +#include +#include +#include +#include +#include +#include "EnhanceDlsym.h" +#include "MallLog.h" +#include + +#define TAG "Matrix.JeCtl" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int (*je_mallctl_t)(const char *name, + void *oldp, + size_t *oldlenp, + void *newp, + size_t newlen); + +typedef void *(*je_arena_extent_alloc_large_t)(void *tsdn, + void *arena, + size_t usize, + size_t alignment, + bool *zero); + +typedef void (*je_large_dalloc_t)(void *tsdn, void *extent); + +typedef void *(*je_arena_choose_hard_t)(void *tsd, bool internal); + +typedef void (*je_arena_extent_dalloc_large_prep_t)(void *tsdn, void *arena, void *extent); + +typedef int (*mallopt_t)(int param, int value); + +#define MAX_RETRY_TIMES 10 + +void *handle = nullptr; +bool initialized = false; + +je_mallctl_t je_mallctl = nullptr; +je_arena_extent_alloc_large_t je_arena_extent_alloc_large = nullptr; +je_large_dalloc_t je_large_dalloc = nullptr; +je_arena_choose_hard_t je_arena_choose_hard = nullptr; +je_arena_extent_dalloc_large_prep_t je_arena_extent_dalloc_large_prep = nullptr; + +bool *je_opt_retain_ptr = nullptr; + +mallopt_t libc_mallopt = nullptr; + +static bool init() { + + handle = enhance::dlopen("libc.so", 0); + + if (handle == nullptr) { + return false; + } + + libc_mallopt = (mallopt_t) enhance::dlsym(handle, "mallopt"); + + je_mallctl = (je_mallctl_t) enhance::dlsym(handle, "je_mallctl"); + + if (!je_mallctl) { + return false; + } + + const char *version; + size_t size = sizeof(version); + je_mallctl("version", &version, &size, nullptr, 0); + LOGD(TAG, "jemalloc version: %s", version); + + je_opt_retain_ptr = (bool *) enhance::dlsym(handle, "je_opt_retain"); + if (!je_opt_retain_ptr) { + return false; + } + + return true; +} + +static void flush_decay_purge() { + assert(je_mallctl != nullptr); + je_mallctl("thread.tcache.flush", nullptr, nullptr, nullptr, 0); + je_mallctl("arena.0.decay", nullptr, nullptr, nullptr, 0); + je_mallctl("arena.1.decay", nullptr, nullptr, nullptr, 0); + je_mallctl("arena.0.purge", nullptr, nullptr, nullptr, 0); + je_mallctl("arena.1.purge", nullptr, nullptr, nullptr, 0); +} + +JNIEXPORT void JNICALL +Java_com_tencent_matrix_mallctl_MallCtl_initNative(JNIEnv *env, jclass clazz) { + if (!initialized) { + initialized = init(); + } +} + +JNIEXPORT jstring JNICALL +Java_com_tencent_matrix_mallctl_MallCtl_getVersionNative(JNIEnv *env, jclass clazz) { +#ifdef __LP64__ + return env->NewStringUTF("64-bit"); +#else + if (!je_mallctl) { + return env->NewStringUTF("Error"); + } + + const char *version; + size_t size = sizeof(version); + je_mallctl("version", &version, &size, nullptr, 0); + LOGD(TAG, "jemalloc version: %s", version); + + return env->NewStringUTF(version); +#endif +} + +#define MALLOPT_SYM_NOT_FOUND -1 + +JNIEXPORT jint JNICALL +Java_com_tencent_matrix_mallctl_MallCtl_malloptNative(JNIEnv *env, jclass clazz) { + if (!libc_mallopt) { + return MALLOPT_SYM_NOT_FOUND; + } + // On success, mallopt() returns 1. On error, it returns 0. + int ret = libc_mallopt(M_PURGE, 0); + if (ret == 0) { + // try fallback je ctls + flush_decay_purge(); + } + return ret; +} + +JNIEXPORT jboolean JNICALL +Java_com_tencent_matrix_mallctl_MallCtl_setRetainNative(JNIEnv *env, jclass clazz, + jboolean enable) { +#ifdef __LP64__ + return true; +#else + bool old = true; + if (initialized && je_opt_retain_ptr) { + old = *je_opt_retain_ptr; + *je_opt_retain_ptr = enable; + LOGD(TAG, "retain = %d", *je_opt_retain_ptr); + } + return old; +#endif +} + +JNIEXPORT jint JNICALL +Java_com_tencent_matrix_mallctl_MallCtl_flushReadOnlyFilePagesNative(JNIEnv *env, jclass clazz, + jlong begin, jlong size) { + + return madvise((void *)begin, (size_t) size, MADV_DONTNEED); +} + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeLog.h b/matrix/matrix-android/matrix-mallctl/src/main/cpp/mallctl/MallLog.h similarity index 100% rename from matrix/matrix-android/matrix-jectl/src/main/cpp/jectl/JeLog.h rename to matrix/matrix-android/matrix-mallctl/src/main/cpp/mallctl/MallLog.h diff --git a/matrix/matrix-android/matrix-mallctl/src/main/java/com/tencent/matrix/mallctl/MallCtl.java b/matrix/matrix-android/matrix-mallctl/src/main/java/com/tencent/matrix/mallctl/MallCtl.java new file mode 100644 index 000000000..57d310424 --- /dev/null +++ b/matrix/matrix-android/matrix-mallctl/src/main/java/com/tencent/matrix/mallctl/MallCtl.java @@ -0,0 +1,158 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.mallctl; + + +import androidx.annotation.Keep; + +import com.tencent.matrix.util.MatrixLog; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by Yves on 2020/7/15 + */ +public class MallCtl { + private static final String TAG = "Matrix.JeCtl"; + + private static boolean initialized = false; + + static { + try { + System.loadLibrary("matrix-mallctl"); + initNative(); + initialized = true; + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + } + + public synchronized static String jeVersion() { + if (!initialized) { + MatrixLog.e(TAG, "JeCtl init failed! check if so exists"); + return "VER_UNKNOWN"; + } + + return getVersionNative(); + } + + public synchronized static boolean jeSetRetain(boolean enable) { + try { + return setRetainNative(enable); + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, "set retain failed"); + } + return false; + } + + public static final int MALLOPT_FAILED = 0; + public static final int MALLOPT_SUCCESS = 1; + public static final int MALLOPT_SYM_NOT_FOUND = -1; + public static final int MALLOPT_EXCEPTION = -2; + + /** + * @return On success, mallopt() returns 1. On error, it returns 0. + */ + public synchronized static int mallopt() { + try { + return malloptNative(); + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, "mallopt failed"); + } + return MALLOPT_EXCEPTION; + } + + public interface TrimPrediction { + boolean canBeTrim(String pathName, String permission); + } + + public static class DefaultPrediction implements TrimPrediction { + @Override + public boolean canBeTrim(String pathName, String permission) { + if (pathName.endsWith(" (deleted)")) { + pathName = pathName.substring(0, pathName.length() - " (deleted)".length()); + } else if (pathName.endsWith("]")) { + pathName = pathName.substring(0, pathName.length() - "]".length()); + } + return !permission.contains("w") + && (pathName.endsWith(".so") + || pathName.endsWith(".dex") + || pathName.endsWith(".apk") + || pathName.endsWith(".vdex") + || pathName.endsWith(".odex") + || pathName.endsWith(".oat") + || pathName.endsWith(".art") + || pathName.endsWith(".ttf") + || pathName.endsWith(".otf") + || pathName.endsWith(".jar")); + } + } + + public synchronized static void flushReadOnlyFilePages(TrimPrediction prediction) { + if (prediction == null) { + prediction = new DefaultPrediction(); + } + Pattern pattern = Pattern.compile("^([0-9a-f]+)-([0-9a-f]+)\\s+([rwxps-]{4})\\s+[0-9a-f]+\\s+[0-9a-f]+:[0-9a-f]+\\s+\\d+\\s*(.*)$"); + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/self/maps")))) { + String line = br.readLine(); + while (line != null) { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + String beginStr = matcher.group(1); + String endStr = matcher.group(2); + String permission = matcher.group(3); + String name = matcher.group(4); + if (name == null || name.isEmpty()) { + name = "[no-name]"; + } + if (prediction.canBeTrim(name, permission) && beginStr != null && endStr != null) { + try { + long beginPtr = Long.parseLong(beginStr, 16); + long endPtr = Long.parseLong(endStr, 16); + long size = endPtr - beginPtr; + flushReadOnlyFilePagesNative(beginPtr, size); + } catch (Throwable e) { + MatrixLog.printErrStackTrace(TAG, e, "%s-%s %s %s", beginStr, endStr, permission, name); + } + } + } + line = br.readLine(); + } + } catch (IOException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + } + + @Keep + private static native void initNative(); + + @Keep + private static native String getVersionNative(); + + @Keep + private static native int malloptNative(); + + @Keep + private static native boolean setRetainNative(boolean enable); + + private static native int flushReadOnlyFilePagesNative(long begin, long size); +} diff --git a/matrix/matrix-android/matrix-jectl/src/test/java/com/tencent/matrix/jectl/ExampleUnitTest.java b/matrix/matrix-android/matrix-mallctl/src/test/java/com/tencent/matrix/jectl/ExampleUnitTest.java similarity index 100% rename from matrix/matrix-android/matrix-jectl/src/test/java/com/tencent/matrix/jectl/ExampleUnitTest.java rename to matrix/matrix-android/matrix-mallctl/src/test/java/com/tencent/matrix/jectl/ExampleUnitTest.java diff --git a/matrix/matrix-android/matrix-memguard/CMakeLists.txt b/matrix/matrix-android/matrix-memguard/CMakeLists.txt new file mode 100644 index 000000000..a5988df49 --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.4.1) + +option(EnableLOG "Enable Logs" ON) +if(EnableLOG) + add_definitions(-DEnableLOG) +endif() + +set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp) + +find_library(log-lib log) + +################################### MemGuard #################################### +set(TARGET memguard_base) + +add_library( + ${TARGET} + STATIC + ${SOURCE_DIR}/memguard/port/Hook.cpp + ${SOURCE_DIR}/memguard/port/Log.cpp + ${SOURCE_DIR}/memguard/port/Memory.cpp + ${SOURCE_DIR}/memguard/port/Mutex.cpp + ${SOURCE_DIR}/memguard/port/Paths.cpp + ${SOURCE_DIR}/memguard/port/Random.cpp + ${SOURCE_DIR}/memguard/port/Unwind.cpp + ${SOURCE_DIR}/memguard/port/FdSanWrapper.cpp + ${SOURCE_DIR}/memguard/util/SignalHandler.cpp + ${SOURCE_DIR}/memguard/util/Interception.cpp + ${SOURCE_DIR}/memguard/util/PagePool.cpp + ${SOURCE_DIR}/memguard/util/Allocation.cpp + ${SOURCE_DIR}/memguard/util/Thread.cpp + ${SOURCE_DIR}/memguard/util/Issue.cpp + ${SOURCE_DIR}/memguard/MemGuard.cpp +) + +target_include_directories( + ${TARGET} + PRIVATE ${SOURCE_DIR}/memguard + PUBLIC ${SOURCE_DIR} + PUBLIC ${EXT_DEP}/include + PUBLIC ${EXT_DEP}/include/backtrace + PUBLIC ${EXT_DEP}/include/backtrace/common +) + +target_compile_options( + ${TARGET} + PRIVATE -Wall -Wextra -Werror -Wno-unused-function + PRIVATE $<$:-std=c17> + PRIVATE $<$:-std=c++17> + PUBLIC -fvisibility=hidden -fno-exceptions -fno-rtti -fdata-sections -ffunction-sections +) + +target_link_libraries( + ${TARGET} + PUBLIC -Wl,--gc-sections + PUBLIC ${EXT_DEP}/lib/${ANDROID_ABI}/libmatrix-hookcommon.so + PUBLIC ${EXT_DEP}/lib/${ANDROID_ABI}/libwechatbacktrace.so + PUBLIC ${EXT_DEP}/lib/${ANDROID_ABI}/libunwindstack.a +) + +set(TARGET matrix-memguard) + +add_library( + ${TARGET} + SHARED + ${SOURCE_DIR}/memguard/jni/com_tencent_mm_tools_memguard_MemGuard.cpp + ${SOURCE_DIR}/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.cpp + ${SOURCE_DIR}/memguard/jni/JNIAux.cpp + ${SOURCE_DIR}/memguard/jni/C2Java.cpp +) + +target_include_directories( + ${TARGET} + PRIVATE ${SOURCE_DIR}/memguard + PRIVATE ${EXT_DEP}/include + PRIVATE ${EXT_DEP}/include/backtrace + PRIVATE ${EXT_DEP}/include/backtrace/common + PRIVATE ${EXT_DEP}/include/fastunwind +) + +target_compile_options( + ${TARGET} + PRIVATE -Wall -Wextra -Werror -Wno-unused-function + PRIVATE -fvisibility=hidden -fno-exceptions -fno-rtti -fdata-sections -ffunction-sections + PRIVATE $<$:-std=c17> + PRIVATE $<$:-std=c++17> +) + +target_link_libraries( + ${TARGET} + PRIVATE -Wl,--gc-sections + PRIVATE -Wl,--version-script=${SOURCE_DIR}/memguard/memguard.map + PRIVATE ${log-lib} + PRIVATE memguard_base +) +################################################################################# \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memguard/build.gradle b/matrix/matrix-android/matrix-memguard/build.gradle new file mode 100644 index 000000000..0d4efe22f --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/build.gradle @@ -0,0 +1,55 @@ +apply plugin: 'com.android.library' + +apply from: rootProject.file('gradle/WeChatNativeDepend.gradle') + +android { + compileSdkVersion rootProject.compileSdkVersion + + defaultConfig { + minSdkVersion MIN_SDK_VERSION_FOR_HOOK + targetSdkVersion rootProject.targetSdkVersion + versionCode rootProject.VERSION_CODE + versionName rootProject.VERSION_NAME + + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + + ndk { + abiFilters rootProject.ABI_FILTERS as String[] + } + + externalNativeBuild { + cmake { + arguments = ['-DANDROID_STL=c++_shared', "-DEnableLOG=${gradle.enableLog() ? "ON" : "OFF"}" as String] + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + +} + +apply from: project.file('dependencies.gradle') + +group = rootProject.GROUP +version = rootProject.VERSION_NAME + +if("External" == rootProject.ext.PUBLISH_CHANNEL) { + apply from: rootProject.file('gradle/android-publish.gradle') +} +else { + apply from: rootProject.file('gradle/WeChatPublish.gradle') + wechatPublish { + artifactId = POM_ARTIFACT_ID + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memguard/dependencies.gradle b/matrix/matrix-android/matrix-memguard/dependencies.gradle new file mode 100644 index 000000000..b7d09a38d --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/dependencies.gradle @@ -0,0 +1,13 @@ +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + testImplementation 'junit:junit:4.12' + implementation 'androidx.annotation:annotation:1.0.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + + implementation project(':matrix-backtrace') + implementation project(':matrix-hooks') + implementation project(':matrix-android-commons') + implementation project(':matrix-android-lib') +} diff --git a/matrix/matrix-android/matrix-memguard/gradle.properties b/matrix/matrix-android/matrix-memguard/gradle.properties new file mode 100644 index 000000000..259c7ad0a --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/gradle.properties @@ -0,0 +1,5 @@ +POM_NAME=Matrix MemGuard +POM_ARTIFACT_ID=matrix-memguard + +android.enableJetifier=true +android.useAndroidX=true \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memguard/proguard-rules.pro b/matrix/matrix-android/matrix-memguard/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/matrix/matrix-android/matrix-memguard/src/androidTest/java/com/tencent/matrix/hook/ExampleInstrumentedTest.java b/matrix/matrix-android/matrix-memguard/src/androidTest/java/com/tencent/matrix/hook/ExampleInstrumentedTest.java new file mode 100644 index 000000000..97c4d0f52 --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/src/androidTest/java/com/tencent/matrix/hook/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.tencent.matrix.hook; + +import android.content.Context; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.tencent.mm.libfdwatchdog.test", appContext.getPackageName()); + } +} diff --git a/matrix/matrix-android/matrix-memguard/src/main/AndroidManifest.xml b/matrix/matrix-android/matrix-memguard/src/main/AndroidManifest.xml new file mode 100644 index 000000000..56fa76626 --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/MemGuard.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/MemGuard.cpp similarity index 98% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/MemGuard.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/MemGuard.cpp index e3c85f0d5..1c89ff846 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/MemGuard.cpp +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/MemGuard.cpp @@ -3,15 +3,15 @@ // #include +#include #include #include #include #include #include #include -#include #include -#include +#include #include "MemGuard.h" using namespace memguard; diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/MemGuard.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/MemGuard.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/MemGuard.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/MemGuard.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/Options.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/Options.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/Options.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/Options.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/Options.inc b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/Options.inc similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/Options.inc rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/Options.inc diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/C2Java.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/C2Java.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/C2Java.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/C2Java.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/C2Java.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/C2Java.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/C2Java.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/C2Java.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/JNIAux.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/JNIAux.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/JNIAux.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/JNIAux.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/JNIAux.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/JNIAux.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/JNIAux.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/JNIAux.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/jni/com_tencent_mm_tools_memguard_MemGuard_00024Options.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/memguard.map b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/memguard.map similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/memguard.map rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/memguard.map diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/FdSanWrapper.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/FdSanWrapper.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/FdSanWrapper.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/FdSanWrapper.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Hook.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Hook.cpp similarity index 50% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Hook.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Hook.cpp index cf5deac03..77aaa223f 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Hook.cpp +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Hook.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include using namespace memguard; @@ -18,8 +18,8 @@ bool memguard::BeginHook() { return true; } -bool memguard::DoHook(const char* pathname_regex, const char* sym_name, void* handler_func, void** original_func) { - int ret = xhook_grouped_register(HOOK_REQUEST_GROUPID_MEMGUARD, pathname_regex, sym_name, handler_func, original_func); +bool memguard::DoHook(int rule_group_id, const char* pathname_regex, const char* sym_name, void* handler_func, void** original_func) { + int ret = xhook_grouped_register(rule_group_id, pathname_regex, sym_name, handler_func, original_func); if (UNLIKELY(ret != 0)) { LOGE(LOG_TAG, "Fail to hook symbol '%s' of libs match pattern '%s', ret: %d", sym_name, pathname_regex, ret); return false; @@ -28,33 +28,49 @@ bool memguard::DoHook(const char* pathname_regex, const char* sym_name, void* ha return true; } -bool memguard::EndHook(const std::vector& ignore_pathname_regex_list) { +bool memguard::EndHook(int rule_group_id, const std::vector& ignore_pathname_regex_list) { int ret = 0; for (auto & pattern : ignore_pathname_regex_list) { - if ((ret = xhook_grouped_ignore(HOOK_REQUEST_GROUPID_MEMGUARD, pattern.c_str(), nullptr)) != 0) { + if ((ret = xhook_grouped_ignore(rule_group_id, pattern.c_str(), nullptr)) != 0) { LOGE(LOG_TAG, "Fail to ignore all symbols in library matches pattern %s, ret: %d", pattern.c_str(), ret); return false; } } - if ((ret = xhook_grouped_ignore(HOOK_REQUEST_GROUPID_MEMGUARD, ".*/libmemguard\\.so$", nullptr)) != 0) { + if ((ret = xhook_grouped_ignore(rule_group_id, ".*/libmatrix-memguard\\.so$", nullptr)) != 0) { LOGE(LOG_TAG, "Fail to ignore all symbols in libmemguard.so, ret: %d", ret); return false; } #if defined(__LP64__) - if ((ret = xhook_grouped_ignore(HOOK_REQUEST_GROUPID_MEMGUARD, ".*/linker64$", nullptr)) != 0) { + if ((ret = xhook_grouped_ignore(rule_group_id, ".*/linker64$", nullptr)) != 0) { LOGE(LOG_TAG, "Fail to ignore all symbols in linker64, ret: %d", ret); return false; } #else - if ((ret = xhook_grouped_ignore(HOOK_REQUEST_GROUPID_MEMGUARD, ".*/linker$", nullptr)) != 0) { + if ((ret = xhook_grouped_ignore(rule_group_id, ".*/linker$", nullptr)) != 0) { LOGE(LOG_TAG, "Fail to ignore all symbols in linker, ret: %d", ret); return false; } #endif - if ((ret = xhook_grouped_ignore(HOOK_REQUEST_GROUPID_MEMGUARD, ".*/libc\\.so", nullptr)) != 0) { + if ((ret = xhook_grouped_ignore(rule_group_id, ".*/libc\\.so$", nullptr)) != 0) { LOGE(LOG_TAG, "Fail to ignore all symbols in libc.so, ret: %d", ret); return false; } + if ((ret = xhook_grouped_ignore(rule_group_id, ".*/libwechatcrash\\.so$", nullptr)) != 0) { + LOGE(LOG_TAG, "Fail to ignore all symbols in libwechatcrash.so, ret: %d", ret); + return false; + } + if ((ret = xhook_grouped_ignore(rule_group_id, ".*/liblog\\.so$", nullptr)) != 0) { + LOGE(LOG_TAG, "Fail to ignore all symbols in liblog.so, ret: %d", ret); + return false; + } + if ((ret = xhook_grouped_ignore(rule_group_id, ".*/libmatrix-hookcommon\\.so$", nullptr)) != 0) { + LOGE(LOG_TAG, "Fail to ignore all symbols in libmatrix-hookcommon.so, ret: %d", ret); + return false; + } + if ((ret = xhook_grouped_ignore(rule_group_id, ".*/libwechatbacktrace\\.so$", nullptr)) != 0) { + LOGE(LOG_TAG, "Fail to ignore all symbols in libwechatbacktrace.so, ret: %d", ret); + return false; + } xhook_enable_debug(0); xhook_enable_sigsegv_protection(1); if (!UpdateHook()) { diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Log.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Log.cpp similarity index 75% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Log.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Log.cpp index 5b872d9dc..c21dad6bb 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Log.cpp +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Log.cpp @@ -2,9 +2,8 @@ // Created by tomystang on 2020/11/24. // -#include -#include -// #include +#include +#include using namespace memguard; @@ -17,5 +16,5 @@ void memguard::log::PrintLog(int level, const char* tag, const char* fmt, ...) { void memguard::log::PrintLogV(int level, const char* tag, const char* fmt, va_list args) { __android_log_vprint(level, tag, fmt, args); - // internal_hook_vlogger(level, tag, fmt, args); + internal_hook_vlogger(level, tag, fmt, args); } \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Memory.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Memory.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Memory.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Memory.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Mutex.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Mutex.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Mutex.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Mutex.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Paths.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Paths.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Paths.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Paths.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Random.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Random.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Random.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Random.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Unwind.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Unwind.cpp similarity index 93% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Unwind.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Unwind.cpp index 350b577cf..ad6f2284f 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/port/Unwind.cpp +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/port/Unwind.cpp @@ -32,11 +32,7 @@ int memguard::unwind::UnwindStack(void** pcs, size_t max_count) { } int memguard::unwind::UnwindStack(void* ucontext, void** pcs, size_t max_count) { -#if defined(__aarch64__) || defined(__i386__) - return fastunwind::Unwind(ucontext, pcs, max_count); -#else return sLocalUnwinder->Unwind(ucontext, pcs, max_count); -#endif } char* memguard::unwind::GetStackElementDescription(const void* pc, char* desc_out, size_t max_length) { diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Allocation.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Allocation.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Allocation.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Allocation.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Allocation.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Allocation.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Allocation.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Allocation.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Auxiliary.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Auxiliary.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Auxiliary.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Auxiliary.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/FdSanWrapper.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/FdSanWrapper.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/FdSanWrapper.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/FdSanWrapper.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Hook.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Hook.h similarity index 51% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Hook.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Hook.h index 01d3179a1..79f246a83 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Hook.h +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Hook.h @@ -11,8 +11,8 @@ namespace memguard { extern bool BeginHook(); - extern bool DoHook(const char* pathname_regex, const char* sym_name, void* handler_func, void** original_func); - extern bool EndHook(const std::vector& ignore_pathname_regex_list); + extern bool DoHook(int rule_group_id, const char* pathname_regex, const char* sym_name, void* handler_func, void** original_func); + extern bool EndHook(int rule_group_id, const std::vector& ignore_pathname_regex_list); extern bool UpdateHook(); } diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Interception.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Interception.cpp similarity index 95% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Interception.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Interception.cpp index db67f9f11..ce29b3f04 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Interception.cpp +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Interception.cpp @@ -8,8 +8,8 @@ #include #include #include -#include -#include +#include +#include #include "Auxiliary.h" #include "Allocation.h" #include "Hook.h" @@ -174,6 +174,7 @@ static void* HandleMAlloc(size_t size) { LOGD(LOG_TAG, "HandleMAlloc(%" PRIu32 ") = %p, skipped.", size, res); } else { LOGD(LOG_TAG, "HandleMAlloc(%" PRIu32 ") = %p, guarded.", size, res); + errno = 0; } return res; } @@ -185,6 +186,7 @@ static void* HandleCAlloc(size_t elem_count, size_t elem_size) { LOGD(LOG_TAG, "HandleCAlloc(%" PRIu32 ",%" PRIu32 ") = %p, skipped.", elem_count, elem_size, res); } else { memset(res, 0, elem_count * elem_size); + errno = 0; LOGD(LOG_TAG, "HandleCAlloc(%" PRIu32 ",%" PRIu32 ") = %p, guarded.", elem_count, elem_size, res); } return res; @@ -192,20 +194,21 @@ static void* HandleCAlloc(size_t elem_count, size_t elem_size) { static void* HandleReAlloc(void* ptr, size_t size) { if (ptr == nullptr) { - return HandleMAlloc(size); + return ORIGINAL_FUNCTION(malloc)(size); } if (size == 0) { HandleFree(ptr); - return ptr; + errno = 0; + return nullptr; } if (LIKELY(!allocation::IsAllocatedByThisAllocator(ptr))) { void* res = ORIGINAL_FUNCTION(realloc)(ptr, size); LOGD(LOG_TAG, "HandleReAlloc(%p,%" PRIu32 ") = %p, skipped.", ptr, size, res); + errno = 0; return res; } - void* newPtr = HandleMAlloc(size); - if (newPtr == nullptr) { + if (UNLIKELY(newPtr == nullptr)) { LOGD(LOG_TAG, "HandleReAlloc(%p,%" PRIu32 ") = %p, failure.", ptr, size, newPtr); errno = ENOMEM; return nullptr; @@ -214,6 +217,7 @@ static void* HandleReAlloc(void* ptr, size_t size) { memcpy(newPtr, ptr, (oldSize <= size ? oldSize : size)); allocation::Free(ptr); LOGD(LOG_TAG, "HandleReAlloc(%p,%" PRIu32 ") = %p, moved, guarded.", ptr, size, newPtr); + errno = 0; return newPtr; } @@ -224,6 +228,7 @@ static void* HandleMemAlign(size_t alignment, size_t byte_count) { } void* res = allocation::AlignedAllocate(byte_count, alignment); if (res != nullptr) { + errno = 0; LOGD(LOG_TAG, "HandleMemAlign(%" PRIu32 ",%" PRIu32 ") = %p, guarded.", alignment, byte_count, res); return res; } else { @@ -242,6 +247,7 @@ static int HandlePosixMemAlign(void** ptr, size_t alignment, size_t size) { LOGD(LOG_TAG, "HandlePosixMemAlign(%p,%" PRIu32 ",%" PRIu32 ") = 0 (ptr_res:%p), guarded.", ptr, alignment, size, res); *ptr = res; + errno = 0; return 0; } else { int originalFnRet = ORIGINAL_FUNCTION(posix_memalign)(ptr, alignment, size); @@ -264,6 +270,7 @@ static char* HandleStrDup(const char* str) { char* buf = (char*) allocation::Allocate(len + 1); if (buf != nullptr) { ::memcpy(buf, str, len + 1); + errno = 0; LOGD(LOG_TAG, "HandleStrDup(%p) = %p, guarded.", str, buf); } else { buf = ORIGINAL_FUNCTION(strdup)(str); @@ -279,6 +286,7 @@ static char* HandleStrNDup(const char* str, size_t n) { if (buf != nullptr) { ::memcpy(buf, str, dupLen); buf[dupLen] = '\0'; + errno = 0; LOGD(LOG_TAG, "HandleStrNDup(%p, %" PRIu32 ") = %p, guarded.", str, n, buf); } else { buf = ORIGINAL_FUNCTION(strndup)(str, n); @@ -304,6 +312,7 @@ static void HandleFree(void* ptr) { ORIGINAL_FUNCTION(free)(ptr); LOGD(LOG_TAG, "HandleFree(%p), skipped.", ptr); } else { + errno = 0; LOGD(LOG_TAG, "HandleFree(%p), recycled.", ptr); } } @@ -582,26 +591,34 @@ bool memguard::interception::Install() { return false; } + if (xhook_export_symtable_hook("libc.so", "free", + reinterpret_cast(HandleFree), nullptr) != 0) { + LOGE(LOG_TAG, "Fail to do export symtab hook for 'free'."); + return false; + } + + if (xhook_export_symtable_hook("libc.so", "realloc", + reinterpret_cast(HandleReAlloc), nullptr) != 0) { + LOGE(LOG_TAG, "Fail to do export symtab hook for 'realloc'."); + return false; + } + #define X(pattern, ret_type, sym, args, handler, def_lib_handle) \ - if (!DoHook(pattern, #sym, (void*) handler, nullptr)) { \ + if (!DoHook(HOOK_REQUEST_GROUPID_MEMGUARD, pattern, #sym, (void*) handler, nullptr)) { \ return false; \ } for (auto & pattern : gOpts.targetSOPatterns) { ENUM_C_ALLOC_FUNCTIONS(pattern.c_str()) - ENUM_C_DEALLOC_AND_OTHER_FUNCTIONS(pattern.c_str()) - ENUM_CPP_ALLOC_FUNCTIONS(pattern.c_str()) - ENUM_CPP_DEALLOC_FUNCTIONS(pattern.c_str()) + // ENUM_CPP_ALLOC_FUNCTIONS(pattern.c_str()) } - ENUM_C_DEALLOC_AND_OTHER_FUNCTIONS(".*/libstlport_shared\\.so$") - ENUM_C_DEALLOC_AND_OTHER_FUNCTIONS(".*/libc++_shared\\.so$") - ENUM_C_DEALLOC_AND_OTHER_FUNCTIONS(".*/libc++\\.so$") - ENUM_C_DEALLOC_AND_OTHER_FUNCTIONS(".*/libgnustl_shared\\.so$") + ENUM_C_DEALLOC_AND_OTHER_FUNCTIONS(".*\\.so$") + // ENUM_CPP_DEALLOC_FUNCTIONS(".*\\.so$") #undef X - if (xhook_export_symtable_hook("libc.so", "free", reinterpret_cast(HandleFree), nullptr) != 0) { - LOGE(LOG_TAG, "Fail to do export symtab hook for 'free'."); + if (!DoHook(HOOK_REQUEST_GROUPID_MEMGUARD, ".*\\.so$", "realloc", + reinterpret_cast(HandleReAlloc), nullptr)) { return false; } @@ -614,7 +631,5 @@ bool memguard::interception::Install() { // INTERCEPT_DLOPEN(".*/libjavacore\\.so$", false); // INTERCEPT_DLOPEN(".*/libnativehelper\\.so$", false); - NOTIFY_COMMON_IGNORE_LIBS(HOOK_REQUEST_GROUPID_MEMGUARD); - - return EndHook(gOpts.ignoredSOPatterns); + return EndHook(HOOK_REQUEST_GROUPID_MEMGUARD, gOpts.ignoredSOPatterns); } \ No newline at end of file diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Interception.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Interception.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Interception.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Interception.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Issue.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Issue.cpp similarity index 96% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Issue.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Issue.cpp index 69ecac57c..52b0848e5 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Issue.cpp +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Issue.cpp @@ -54,34 +54,35 @@ static std::vector GetReadableStackTrace(const void** pcs, size_t c static void PrintLineV(int fd, const char* fmt, va_list args) { char line[1024] = {}; int bytesPrint = vsnprintf(line, sizeof(line), fmt, args); + if (bytesPrint < 0) { + return; + } + if (static_cast(bytesPrint) < sizeof(line)) { + line[bytesPrint++] = '\n'; + } else { + line[bytesPrint - 1] = '\n'; + } if (fd >= 0) { TEMP_FAILURE_RETRY(syscall(__NR_write, fd, line, bytesPrint)); - TEMP_FAILURE_RETRY(syscall(__NR_write, fd, "\n", 1)); + syscall(__NR_fsync, fd); } } static void PrintLine(int fd, const char* fmt, ...) { va_list args; va_start(args, fmt); - char line[1024] = {}; - int bytesPrint = vsnprintf(line, sizeof(line), fmt, args); - if (fd >= 0) { - TEMP_FAILURE_RETRY(syscall(__NR_write, fd, line, bytesPrint)); - TEMP_FAILURE_RETRY(syscall(__NR_write, fd, "\n", 1)); - } + PrintLineV(fd, fmt, args); va_end(args); } static void PrintStackTrace(int fd, const std::vector& stack_trace, const char* header_fmt, ...) { va_list headerArgs; va_start(headerArgs, header_fmt); - ON_SCOPE_EXIT(va_end(headerArgs)); - PrintLineV(fd, header_fmt, headerArgs); for (size_t i = 0; i < stack_trace.size(); ++i) { PrintLine(fd, " #%02d %s", i, stack_trace[i].c_str()); } - fsync(fd); + va_end(headerArgs); } static pagepool::slot_t GetNearestSlotID(const void* addr) { diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Issue.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Issue.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Issue.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Issue.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Log.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Log.h similarity index 91% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Log.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Log.h index 6d44afa89..64188c6ef 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Log.h +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Log.h @@ -8,9 +8,15 @@ #include #include "Auxiliary.h" +#include "LogFn.h" -#ifndef MEMGUARD_LOG_LEVEL -#define MEMGUARD_LOG_LEVEL LOG_DEBUG +#ifdef EnableLOG + #ifndef MEMGUARD_LOG_LEVEL + #define MEMGUARD_LOG_LEVEL LOG_VERBOSE + #endif +#else + #undef MEMGUARD_LOG_LEVEL + #define MEMGUARD_LOG_LEVEL LOG_SILENT #endif #define LOG_VERBOSE 2 @@ -82,12 +88,5 @@ static inline void OmitLogV(const char* tag, const char* fmt, va_list va_args) { } \ } while (0) -namespace memguard { - namespace log { - extern void PrintLog(int level, const char* tag, const char* fmt, ...); - extern void PrintLogV(int level, const char* tag, const char* fmt, va_list args); - } -} - #endif //__MEMGUARD_LOG_H__ diff --git a/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/LogFn.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/LogFn.h new file mode 100644 index 000000000..4a8d163f2 --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/LogFn.h @@ -0,0 +1,19 @@ +// +// Created by YinSheng Tang on 2022/1/23. +// + +#ifndef __MEMGUARD_LOGFN_H__ +#define __MEMGUARD_LOGFN_H__ + + +#include + +namespace memguard { + namespace log { + extern void PrintLog(int level, const char* tag, const char* fmt, ...); + extern void PrintLogV(int level, const char* tag, const char* fmt, va_list args); + } +} + + +#endif //__MEMGUARD_LOGFN_H__ diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Memory.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Memory.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Memory.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Memory.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Mutex.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Mutex.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Mutex.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Mutex.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/PagePool.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/PagePool.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/PagePool.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/PagePool.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/PagePool.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/PagePool.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/PagePool.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/PagePool.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Paths.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Paths.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Paths.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Paths.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/SignalHandler.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/SignalHandler.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/SignalHandler.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/SignalHandler.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/SignalHandler.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/SignalHandler.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/SignalHandler.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/SignalHandler.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Thread.cpp b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Thread.cpp similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Thread.cpp rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Thread.cpp diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Thread.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Thread.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Thread.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Thread.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Unwind.h b/matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Unwind.h similarity index 100% rename from matrix/matrix-android/matrix-hooks/src/main/cpp/memguard/util/Unwind.h rename to matrix/matrix-android/matrix-memguard/src/main/cpp/memguard/util/Unwind.h diff --git a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/memguard/MemGuard.java b/matrix/matrix-android/matrix-memguard/src/main/java/com/tencent/matrix/memguard/MemGuard.java similarity index 91% rename from matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/memguard/MemGuard.java rename to matrix/matrix-android/matrix-memguard/src/main/java/com/tencent/matrix/memguard/MemGuard.java index 6d8079421..39dd2a6e8 100644 --- a/matrix/matrix-android/matrix-hooks/src/main/java/com/tencent/matrix/memguard/MemGuard.java +++ b/matrix/matrix-android/matrix-memguard/src/main/java/com/tencent/matrix/memguard/MemGuard.java @@ -4,7 +4,6 @@ import android.app.ActivityManager; import android.content.Context; import android.os.Process; -import android.text.TextUtils; import androidx.annotation.Keep; import androidx.annotation.NonNull; @@ -17,6 +16,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; +import java.io.FilenameFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -26,9 +26,11 @@ public final class MemGuard { private static final String TAG = "MemGuard"; + private static final String HOOK_COMMON_NATIVE_LIB_NAME = "matrix-hookcommon"; private static final String NATIVE_LIB_NAME = "matrix-memguard"; private static final String ISSUE_CALLBACK_THREAD_NAME = "MemGuard.IssueCB"; private static final long ISSUE_CALLBACK_TIMEOUT_MS = 5000; + private static final String DEFAULT_DUMP_FILE_EXT = ".txt"; private static final boolean[] sInstalled = {false}; @@ -90,8 +92,10 @@ public static boolean install(@NonNull Options opts, boolean success = false; try { if (soLoader != null) { + soLoader.loadLibrary(HOOK_COMMON_NATIVE_LIB_NAME); soLoader.loadLibrary(NATIVE_LIB_NAME); } else { + System.loadLibrary(HOOK_COMMON_NATIVE_LIB_NAME); System.loadLibrary(NATIVE_LIB_NAME); } @@ -106,6 +110,7 @@ public static boolean install(@NonNull Options opts, } if (success) { MatrixLog.i(TAG, "Install MemGuard successfully with " + opts); + MemoryHook.INSTANCE.notifyMemGuardInstalled(); } else { MatrixLog.e(TAG, "Install MemGuard failed with " + opts); } @@ -120,14 +125,19 @@ public static boolean isInstalled() { } } - @Nullable - public static File getLastIssueDumpFileIfExists() { - final String issueDumpFilePath = nativeGetIssueDumpFilePath(); - if (TextUtils.isEmpty(issueDumpFilePath)) { - return null; + @NonNull + public static List getLastIssueDumpFilesInDefaultDir(@NonNull Context context) { + final File[] subFiles = new File(getDefaultIssueDumpDir(context)).listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(DEFAULT_DUMP_FILE_EXT); + } + }); + if (subFiles != null) { + return Collections.unmodifiableList(Arrays.asList(subFiles)); + } else { + return Collections.emptyList(); } - final File result = new File(issueDumpFilePath); - return result.exists() ? result : null; } private static native boolean nativeInstall(@NonNull Options opts); @@ -445,13 +455,15 @@ private static String getProcessSuffix(@NonNull Context context) { final List runningProcs = am.getRunningAppProcesses(); final int myUid = Process.myUid(); final int myPid = Process.myPid(); - for (ActivityManager.RunningAppProcessInfo procInfo : runningProcs) { - if (procInfo.uid == myUid && procInfo.pid == myPid) { - final int colIdx = procInfo.processName.lastIndexOf(':'); - if (colIdx >= 0) { - return procInfo.processName.substring(colIdx + 1); - } else { - return "main"; + if (runningProcs != null) { + for (ActivityManager.RunningAppProcessInfo procInfo : runningProcs) { + if (procInfo.uid == myUid && procInfo.pid == myPid) { + final int colIdx = procInfo.processName.lastIndexOf(':'); + if (colIdx >= 0) { + return procInfo.processName.substring(colIdx + 1); + } else { + return "main"; + } } } } @@ -459,6 +471,7 @@ private static String getProcessSuffix(@NonNull Context context) { } private static String generateIssueDumpFilePath(Context context, String dirPath) { - return new File(dirPath, "memguard_issue_in_proc_" + getProcessSuffix(context) + ".txt").getAbsolutePath(); + return new File(dirPath, "memguard_issue_in_proc_" + + getProcessSuffix(context) + "_" + Process.myPid() + ".txt").getAbsolutePath(); } } diff --git a/matrix/matrix-android/matrix-memguard/src/test/java/com/tencent/matrix/hook/ExampleUnitTest.java b/matrix/matrix-android/matrix-memguard/src/test/java/com/tencent/matrix/hook/ExampleUnitTest.java new file mode 100644 index 000000000..988c4773e --- /dev/null +++ b/matrix/matrix-android/matrix-memguard/src/test/java/com/tencent/matrix/hook/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.tencent.matrix.hook; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-dump/build.gradle b/matrix/matrix-android/matrix-memory-canary/build.gradle similarity index 53% rename from matrix/matrix-android/matrix-memory-dump/build.gradle rename to matrix/matrix-android/matrix-memory-canary/build.gradle index d5428139a..a3f5a5121 100644 --- a/matrix/matrix-android/matrix-memory-dump/build.gradle +++ b/matrix/matrix-android/matrix-memory-canary/build.gradle @@ -3,12 +3,12 @@ plugins { id 'kotlin-android' } -apply from: rootProject.file('gradle/WeChatNativeDepend.gradle') - android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion + useLibrary 'android.test.base' + defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion @@ -25,33 +25,33 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - - externalNativeBuild { - cmake { - path "CMakeLists.txt" - } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 } } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$gradle.KOTLIN_VERSION" - implementation project(':matrix-android-lib') - implementation project(':matrix-android-commons') - implementation project(':matrix-backtrace') - testImplementation 'junit:junit:4.13.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${gradle.KOTLIN_VERSION}" + testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} -version rootProject.ext.VERSION_NAME -group rootProject.ext.GROUP + implementation project(":matrix-android-lib") +} -if ("External" == rootProject.ext.PUBLISH_CHANNEL) { - apply from: rootProject.file('gradle/android-publish.gradle') -} else { - apply from: rootProject.file('gradle/WeChatPublish.gradle') - wechatPublish { - artifactId = POM_ARTIFACT_ID +version = rootProject.ext.VERSION_NAME +group = rootProject.ext.GROUP + +if (rootProject.file('gradle/WeChatPublish.gradle').exists()) { + if("External" == rootProject.ext.PUBLISH_CHANNEL){ + apply from: rootProject.file('gradle/android-publish.gradle') + }else { + //uploading to WeChat maven repo + apply from: rootProject.file('gradle/WeChatPublish.gradle') + wechatPublish { + artifactId=POM_ARTIFACT_ID + } } -} \ No newline at end of file +} diff --git a/matrix/matrix-android/matrix-memory-dump/consumer-rules.pro b/matrix/matrix-android/matrix-memory-canary/consumer-rules.pro similarity index 100% rename from matrix/matrix-android/matrix-memory-dump/consumer-rules.pro rename to matrix/matrix-android/matrix-memory-canary/consumer-rules.pro diff --git a/matrix/matrix-android/matrix-memory-canary/gradle.properties b/matrix/matrix-android/matrix-memory-canary/gradle.properties new file mode 100644 index 000000000..5b41d139b --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/gradle.properties @@ -0,0 +1,2 @@ +POM_NAME=Matrix Memory Canary +POM_ARTIFACT_ID=matrix-memory-canary \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-dump/proguard-rules.pro b/matrix/matrix-android/matrix-memory-canary/proguard-rules.pro similarity index 100% rename from matrix/matrix-android/matrix-memory-dump/proguard-rules.pro rename to matrix/matrix-android/matrix-memory-canary/proguard-rules.pro diff --git a/matrix/matrix-android/matrix-memory-canary/src/androidTest/java/com/tencent/matrix_memory_canary/ExampleInstrumentedTest.kt b/matrix/matrix-android/matrix-memory-canary/src/androidTest/java/com/tencent/matrix_memory_canary/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..bd21cdeb2 --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/src/androidTest/java/com/tencent/matrix_memory_canary/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.tencent.matrix_memory_canary + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.tencent.matrix_memory_canary.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/AndroidManifest.xml b/matrix/matrix-android/matrix-memory-canary/src/main/AndroidManifest.xml similarity index 71% rename from matrix/matrix-android/matrix-memory-dump/src/main/AndroidManifest.xml rename to matrix/matrix-android/matrix-memory-canary/src/main/AndroidManifest.xml index fd63856aa..4e2b3abc6 100644 --- a/matrix/matrix-android/matrix-memory-dump/src/main/AndroidManifest.xml +++ b/matrix/matrix-android/matrix-memory-canary/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.tencent.matrix.memory.canary"> \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/MemoryCanaryPlugin.kt b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/MemoryCanaryPlugin.kt new file mode 100644 index 000000000..7dc9a908e --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/MemoryCanaryPlugin.kt @@ -0,0 +1,62 @@ +package com.tencent.matrix.memory.canary + +import com.tencent.matrix.lifecycle.owners.ProcessDeepBackgroundOwner +import com.tencent.matrix.lifecycle.owners.ProcessStagedBackgroundOwner +import com.tencent.matrix.lifecycle.supervisor.AppDeepBackgroundOwner +import com.tencent.matrix.lifecycle.supervisor.AppStagedBackgroundOwner +import com.tencent.matrix.lifecycle.supervisor.ProcessSupervisor +import com.tencent.matrix.memory.canary.monitor.AppBgSumPssMonitor +import com.tencent.matrix.memory.canary.monitor.AppBgSumPssMonitorConfig +import com.tencent.matrix.memory.canary.monitor.ProcessBgMemoryMonitor +import com.tencent.matrix.memory.canary.monitor.ProcessBgMemoryMonitorConfig +import com.tencent.matrix.memory.canary.trim.TrimMemoryConfig +import com.tencent.matrix.memory.canary.trim.TrimMemoryNotifier +import com.tencent.matrix.plugin.Plugin +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.MatrixUtil +import com.tencent.matrix.util.safeLet + +@Suppress("ArrayInDataClass") +data class MemoryCanaryConfig( + val appBgSumPssMonitorConfigs: Array = arrayOf( + AppBgSumPssMonitorConfig(bgStatefulOwner = AppStagedBackgroundOwner), + AppBgSumPssMonitorConfig(bgStatefulOwner = AppDeepBackgroundOwner) + ), + val processBgMemoryMonitorConfigs: Array = arrayOf( + ProcessBgMemoryMonitorConfig(bgStatefulOwner = ProcessStagedBackgroundOwner), + ProcessBgMemoryMonitorConfig(bgStatefulOwner = ProcessDeepBackgroundOwner) + ), + val trimMemoryConfig: TrimMemoryConfig = TrimMemoryConfig() +) + +class MemoryCanaryPlugin( + private val memoryCanaryConfig: MemoryCanaryConfig = MemoryCanaryConfig() +) : Plugin() { + + override fun start() { + if (status == PLUGIN_STARTED) { + MatrixLog.e(tag, "already started") + return + } + super.start() + + memoryCanaryConfig.apply { + val isSupervisor = safeLet(tag, defVal = false) { + ProcessSupervisor.isSupervisor // throws Exception when Supervisor disabled + } + if (isSupervisor) { + MatrixLog.d(tag, "supervisor is ${MatrixUtil.getProcessName(application)}") + + AppBgSumPssMonitor.init(appBgSumPssMonitorConfigs) + } + processBgMemoryMonitorConfigs.forEach { + ProcessBgMemoryMonitor(it).init() + } + TrimMemoryNotifier.init(trimMemoryConfig) + } + } + + override fun getTag(): String { + return "Matrix.MemoryCanaryPlugin" + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/AppBgSumPssMonitor.kt b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/AppBgSumPssMonitor.kt new file mode 100644 index 000000000..85b46c1b7 --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/AppBgSumPssMonitor.kt @@ -0,0 +1,126 @@ +package com.tencent.matrix.memory.canary.monitor + +import com.tencent.matrix.lifecycle.IBackgroundStatefulOwner +import com.tencent.matrix.lifecycle.IMatrixBackgroundCallback +import com.tencent.matrix.lifecycle.supervisor.AppStagedBackgroundOwner +import com.tencent.matrix.lifecycle.supervisor.ProcessSupervisor +import com.tencent.matrix.util.* +import java.util.concurrent.TimeUnit + +private const val TAG = "Matrix.monitor.AppBgSumPssMonitor" + +/** + * Created by Yves on 2021/10/27 + */ +class AppBgSumPssMonitorConfig( + val enable: Boolean = true, + val bgStatefulOwner: IBackgroundStatefulOwner = AppStagedBackgroundOwner, + val checkInterval: Long = TimeUnit.MINUTES.toMillis(3), + amsPssThresholdKB: Long = Long.MAX_VALUE, + debugPssThresholdKB: Long = Long.MAX_VALUE, + checkTimes: Int = 3, + val callback: (amsPssSumKB: Int, debugPssSumKB: Int, amsMemInfos: Array, debugMemInfos: Array) -> Unit = { amsSumPssKB, debugSumPssKB, amsMemInfos, debugMemInfos -> + MatrixLog.e( + TAG, + "sum pss of all process over threshold: amsSumPss = $amsSumPssKB KB, debugSumPss = $debugSumPssKB KB " + + "amsMemDetail: ${amsMemInfos.contentToString()}" + + "\n==========\n" + + "debugMemDetail: ${debugMemInfos.contentToString()}" + ) + }, + val extraPssFactory: () -> Array = { emptyArray() } /*{ arrayOf(MemInfo(processInfo = ProcessInfo(pid = -1, name = "extra_isolate", activity = "none"), amsPssInfo = PssInfo(totalPssK = 0)))}*/ +) { + internal val amsPssThresholdKB = + amsPssThresholdKB.asThreshold(checkTimes, TimeUnit.MINUTES.toMillis(5)) + internal val debugPssThresholdKB = debugPssThresholdKB.asThreshold(checkTimes) + + // internal val + override fun toString(): String { + return "AppBgSumPssMonitorConfig(enable=$enable, bgStatefulOwner=$bgStatefulOwner, checkInterval=$checkInterval, callback=${callback.javaClass.name}, thresholdKB=$amsPssThresholdKB, extraPssFactory=${extraPssFactory.javaClass.name})" + } +} + +internal class AppBgSumPssMonitor( + private val checkInterval: Long, + private val bgStatefulOwner: IBackgroundStatefulOwner, + private val configs: Array +) { + companion object { + fun init(configs: Array) { + configs.groupBy { cfg -> cfg.checkInterval }.forEach { e -> + e.value.groupBy { it.bgStatefulOwner }.forEach { e2 -> + AppBgSumPssMonitor(e.key, e2.key, e2.value.toTypedArray()).start() + } + } + } + } + + private val checkTask = Runnable { check() } + private val runningHandler = MatrixHandlerThread.getDefaultHandler() + + private fun start() { + MatrixLog.i(TAG, configs.contentToString()) + if (configs.none { it.enable }) { + MatrixLog.i(TAG, "none enabled") + return + } + + bgStatefulOwner.addLifecycleCallback(object : IMatrixBackgroundCallback() { + override fun onEnterBackground() { + runningHandler.postDelayed(checkTask, checkInterval) + } + + override fun onExitBackground() { + runningHandler.removeCallbacks(checkTask) + } + }) + } + + private fun check() { + val amsMemInfos = MemInfo.getAllProcessPss() + val debugMemInfos = ProcessSupervisor.getAllProcessMemInfo() ?: emptyArray() + + val amsPssSum = amsMemInfos.onEach { info -> + MatrixLog.i( + TAG, + "${info.processInfo?.pid}-${info.processInfo?.name}: ${info.amsPssInfo!!.totalPssK} KB" + ) + }.sumBy { it.amsPssInfo!!.totalPssK }.also { MatrixLog.i(TAG, "sumPss = $it KB") } + + val debugPssSum = safeLet(tag = TAG, defVal = 0) { + debugMemInfos.filter { it.processInfo != null && it.debugPssInfo != null && it.amsPssInfo != null }.onEach { + MatrixLog.i( + TAG, + "${it.processInfo?.pid}-${it.processInfo?.name}: dbgPss = ${it.debugPssInfo!!.totalPssK} KB, amsPss = ${it.amsPssInfo!!.totalPssK} KB" + ) + }.sumBy { it.debugPssInfo!!.totalPssK }.also { MatrixLog.i(TAG, "ipc sumDbgPss = $it KB") } + } + + MatrixLog.i(TAG, "check with interval [$checkInterval] amsPssSum = $amsPssSum KB, ${amsMemInfos.contentToString()}") + MatrixLog.i(TAG, "check with interval [$checkInterval] debugPssSum = $debugPssSum KB, ${debugMemInfos.contentToString()}") + + configs.forEach { config -> + var shouldCallback = false + + val extraPssInfo = config.extraPssFactory() + val extraPssSum = extraPssInfo.onEach { + MatrixLog.i(TAG, + "${it.processInfo!!.pid}-${it.processInfo!!.name}: extra total pss = ${it.amsPssInfo!!.totalPssK} KB" + ) + }.sumBy { it.amsPssInfo!!.totalPssK }.also { if (extraPssInfo.isNotEmpty()) { MatrixLog.i(TAG, "extra sum pss = $it KB") } } + + val overThreshold = config.run { + // @formatter:off + arrayOf("amsPss" to amsPssThresholdKB.check((amsPssSum + extraPssSum).toLong()) { shouldCallback = true }, + "debugPss" to debugPssThresholdKB.check((debugPssSum + extraPssSum).toLong()) { shouldCallback = true } + ).onEach { MatrixLog.i(TAG, "is over threshold? $it") }.any { it.second } + // @formatter:on + } + + if (overThreshold && shouldCallback) { + MatrixLog.i(TAG, "report over threshold") + config.callback.invoke(amsPssSum + extraPssSum, debugPssSum + extraPssSum, amsMemInfos + extraPssInfo, debugMemInfos + extraPssInfo) + } + } + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/MemoryThreshold.kt b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/MemoryThreshold.kt new file mode 100644 index 000000000..62961274e --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/MemoryThreshold.kt @@ -0,0 +1,42 @@ +package com.tencent.matrix.memory.canary.monitor + +/** + * Created by Yves on 2021/12/9 + */ + +internal data class Threshold( + private val size: Long, + private val checkTimes: Int = 3, + private val checkInterval: Long = 0, +) { + private var check = 0 + private var lastCheckTime = 0L + + internal fun check(size: Long, cb: () -> Unit): Boolean { + val current = System.currentTimeMillis() + val lastCheckToNow = current - lastCheckTime + lastCheckTime = current + + val over = size > this.size + + if (over && lastCheckToNow > checkInterval && check < checkTimes && ++check == checkTimes) { + cb.invoke() + } else if (!over && lastCheckToNow > checkInterval && check < checkTimes) { + check = 0 // reset + } + + return over && lastCheckToNow > checkInterval + } + + override fun toString(): String { + return "{size = $size, checkTimes = ${checkTimes}}" + } +} + +internal fun Long.asThreshold(checkTimes: Int = 3): Threshold { + return Threshold(this, checkTimes) +} + +internal fun Long.asThreshold(checkTimes: Int = 3, checkInterval: Long = 0): Threshold { + return Threshold(this, checkTimes, checkInterval) +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/ProcessBgMemoryMonitor.kt b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/ProcessBgMemoryMonitor.kt new file mode 100644 index 000000000..dd76dcc20 --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/ProcessBgMemoryMonitor.kt @@ -0,0 +1,89 @@ +package com.tencent.matrix.memory.canary.monitor + +import com.tencent.matrix.lifecycle.IStateObserver +import com.tencent.matrix.lifecycle.IBackgroundStatefulOwner +import com.tencent.matrix.lifecycle.owners.ProcessStagedBackgroundOwner +import com.tencent.matrix.util.MemInfo +import com.tencent.matrix.util.MatrixHandlerThread +import com.tencent.matrix.util.MatrixLog +import java.util.concurrent.TimeUnit + +/** + * Created by Yves on 2021/11/3 + */ + +private const val TAG = "Matrix.monitor.BackgroundMemoryMonitor" + +class ProcessBgMemoryMonitorConfig( + val enable: Boolean = true, + val bgStatefulOwner: IBackgroundStatefulOwner = ProcessStagedBackgroundOwner, + val checkInterval: Long = TimeUnit.MINUTES.toMillis(3), + javaThresholdByte: Long = 250 * 1024 * 1024L, + nativeThresholdByte: Long = 500 * 1024 * 1024L, + amsPssThresholdK: Long = 1024 * 1024, + debugPssThresholdK: Long = 1024 * 1024, + checkTimes: Int = 3, + val callback: (memInfo: MemInfo) -> Unit = { _ -> + // do report + }, +) { + internal val javaThresholdByte: Threshold = javaThresholdByte.asThreshold(checkTimes) + internal val nativeThresholdByte: Threshold = nativeThresholdByte.asThreshold(checkTimes) + internal val debugPssThresholdK: Threshold = debugPssThresholdK.asThreshold(checkTimes) + internal val amsPssThresholdK: Threshold = + amsPssThresholdK.asThreshold(checkTimes, TimeUnit.MINUTES.toMillis(5)) + + override fun toString(): String { + return "ProcessBgMemoryMonitorConfig(enable=$enable, bgStatefulOwner=$bgStatefulOwner, checkInterval=$checkInterval, reportCallback=${callback.javaClass.name}, javaThresholdByte=$javaThresholdByte, nativeThresholdByte=$nativeThresholdByte, debugPssThresholdK=$debugPssThresholdK, amsPssThresholdK=$amsPssThresholdK)" + } +} + +internal class ProcessBgMemoryMonitor(private val config: ProcessBgMemoryMonitorConfig) { + private val runningHandler = MatrixHandlerThread.getDefaultHandler() + + private val delayCheckTask = Runnable { check() } + + fun init() { + MatrixLog.i(TAG, "$config") + if (!config.enable) { + return + } + config.bgStatefulOwner.observeForever(object : IStateObserver { + override fun on() { + runningHandler.postDelayed(delayCheckTask, config.checkInterval) + } + + override fun off() { + runningHandler.removeCallbacks(delayCheckTask) + } + }) + } + + private fun check() { + + val memInfo = MemInfo.getCurrentProcessFullMemInfo() + var shouldCallback = false + + val overThreshold = config.run { + // @formatter:off + arrayOf( + "java" to javaThresholdByte.check(memInfo.javaMemInfo!!.usedByte) { shouldCallback = true }, + "native" to nativeThresholdByte.check(memInfo.nativeMemInfo!!.usedByte) { shouldCallback = true }, + "debugPss" to debugPssThresholdK.check(memInfo.debugPssInfo!!.totalPssK.toLong()) { shouldCallback = true }, + "amsPss" to amsPssThresholdK.check(memInfo.amsPssInfo!!.totalPssK.toLong()) { shouldCallback = true } + ).onEach { MatrixLog.i(TAG, "is over threshold ? $it") }.any { it.second } + // @formatter:on + } + + MatrixLog.i( + TAG, + "check: overThreshold: $overThreshold, shouldCallback: $shouldCallback $memInfo" + ) + + if (overThreshold && shouldCallback) { + MatrixLog.i(TAG, "report over threshold") + config.callback.invoke(memInfo) + } + } + +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/TimerMonitor.kt b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/TimerMonitor.kt new file mode 100644 index 000000000..dd1a01e39 --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/monitor/TimerMonitor.kt @@ -0,0 +1,37 @@ +package com.tencent.matrix.memory.canary.monitor + +import com.tencent.matrix.util.MatrixHandlerThread +import com.tencent.matrix.util.MatrixLog +import java.util.concurrent.TimeUnit + +/** + * Created by Yves on 2021/10/12 + */ +abstract class TimerMonitor(private val cycle: Long = DEFAULT_CHECK_TIME) : Runnable { + + companion object { + private const val TAG = "Matrix.monitor.TimerMonitor" + private val DEFAULT_CHECK_TIME = TimeUnit.MINUTES.toMillis(5) + } + + private val handler = MatrixHandlerThread.getDefaultHandler()!! + + fun start() { + MatrixLog.i(TAG, "start ${javaClass.name}") + handler.removeCallbacksAndMessages(null) + handler.postDelayed(this, cycle) + } + + fun stop() { + MatrixLog.i(TAG, "stop ${javaClass.name}") + handler.removeCallbacksAndMessages(null) + } + + final override fun run() { + action() + + handler.postDelayed(this, cycle) + } + + abstract fun action() +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/trim/TrimMemoryNotifier.kt b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/trim/TrimMemoryNotifier.kt new file mode 100644 index 000000000..371048811 --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/src/main/java/com/tencent/matrix/memory/canary/trim/TrimMemoryNotifier.kt @@ -0,0 +1,262 @@ +package com.tencent.matrix.memory.canary.trim + +import android.content.ComponentCallbacks2 +import android.content.res.Configuration +import android.os.Handler +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent +import com.tencent.matrix.Matrix +import com.tencent.matrix.lifecycle.IBackgroundStatefulOwner +import com.tencent.matrix.lifecycle.IMatrixBackgroundCallback +import com.tencent.matrix.lifecycle.owners.ProcessDeepBackgroundOwner +import com.tencent.matrix.lifecycle.owners.ProcessExplicitBackgroundOwner +import com.tencent.matrix.lifecycle.owners.ProcessStagedBackgroundOwner +import com.tencent.matrix.lifecycle.supervisor.AppDeepBackgroundOwner +import com.tencent.matrix.lifecycle.supervisor.AppStagedBackgroundOwner +import com.tencent.matrix.util.MatrixHandlerThread +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.safeApply +import java.util.concurrent.TimeUnit + +interface TrimCallback { + fun backgroundTrim() + fun systemTrim(level: Int) +} + +data class TrimMemoryConfig( + val enable: Boolean = false, + val delayMillis: ArrayList = arrayListOf(TimeUnit.MINUTES.toMillis(1)) +) + +/** + * Trim memory when turned staged background or deep background for 1min or received system trim callback + */ +object TrimMemoryNotifier { + + private const val TAG = "Matrix.TrimMemoryNotifier" + + private val procTrimCallbacks = ArrayList() + private val appTrimCallbacks = ArrayList() + + private fun ArrayList.backgroundTrim() { + val copy = synchronized(this) { ArrayList(this) } + copy.forEach { + it.safeApply(TAG) { backgroundTrim() } + } + Runtime.getRuntime().gc() + } + + private fun ArrayList.systemTrim(level: Int) { + val copy = synchronized(this) { ArrayList(this) } + copy.forEach { + it.safeApply(TAG) { systemTrim(level) } + } + Runtime.getRuntime().gc() + } + + class TrimTask( + private val name: String, + private val backgroundOwner: IBackgroundStatefulOwner, + private val trimCallback: ArrayList, + private val config: TrimMemoryConfig, + private val immediate: Boolean + ) : Runnable { + + private val runningHandler = + Handler(MatrixHandlerThread.getDefaultHandlerThread().looper) + + @Volatile + private var delayIndex = 0 + + fun init() { + backgroundOwner.addLifecycleCallback(object : IMatrixBackgroundCallback() { + override fun onEnterBackground() { + delayIndex = 0 + val delay = config.delayMillis[delayIndex] + runningHandler.removeCallbacksAndMessages(null) + if (immediate) { + trimCallback.backgroundTrim() + MatrixLog.i(TAG, "[$name] trim immediately") + } + runningHandler.postDelayed(this@TrimTask, delay) + MatrixLog.i( + TAG, + "...[$name] trim delay[${delayIndex + 1}/${config.delayMillis.size}] $delay" + ) + } + + override fun onExitBackground() { + runningHandler.removeCallbacks(this@TrimTask) + delayIndex = 0 + } + }) + } + + override fun run() { + val currIndex = delayIndex + if (currIndex >= config.delayMillis.size) { + MatrixLog.e(TAG, "index[$currIndex] out of bounds[${config.delayMillis.size}]") + return + } + MatrixLog.i( + TAG, + "!!![$name] trim timeout [${currIndex + 1}/${config.delayMillis.size}] ${config.delayMillis[currIndex]}" + ) + trimCallback.backgroundTrim() + val nextIndex = currIndex + 1 + if (nextIndex < config.delayMillis.size) { + delayIndex = nextIndex + val delay = config.delayMillis[nextIndex] + runningHandler.postDelayed(this, delay) + MatrixLog.i( + TAG, + "...[$name] trim delay[${nextIndex + 1}/${config.delayMillis.size}] $delay" + ) + } + } + } + + fun init(config: TrimMemoryConfig) { + if (!config.enable) { + return + } + + if (config.delayMillis.isEmpty()) { + throw IllegalArgumentException("config.delayMillis is empty") + } + + if (!Matrix.isInstalled()) { + MatrixLog.e(TAG, "Matrix NOT installed yet") + return + } + + // system trim + Matrix.with().application.registerComponentCallbacks(object : ComponentCallbacks2 { + + private var lastTrimTimeMillis = 0L + + @Volatile + private var trimCounter = 0 + private val maxTrimCount = 10 + + init { + ProcessExplicitBackgroundOwner.addLifecycleCallback(object : + IMatrixBackgroundCallback() { + + override fun onEnterBackground() { + // reset trim + trimCounter = 0 + } + + override fun onExitBackground() {} + }) + } + + override fun onLowMemory() { + val current = System.currentTimeMillis() + if (current - lastTrimTimeMillis < TimeUnit.MINUTES.toMillis((trimCounter + 1).toLong()) || trimCounter >= maxTrimCount) { + MatrixLog.w(TAG, "onLowMemory skip for frequency") + return + } + lastTrimTimeMillis = current + trimCounter++ + MatrixLog.e(TAG, "onLowMemory post") + MatrixHandlerThread.getDefaultHandler().post { + MatrixLog.e(TAG, "onLowMemory") + procTrimCallbacks.systemTrim(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) + appTrimCallbacks.systemTrim(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) + } + } + + override fun onTrimMemory(level: Int) { + if (level <= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + val current = System.currentTimeMillis() + if (current - lastTrimTimeMillis < TimeUnit.MINUTES.toMillis((trimCounter + 1).toLong()) || trimCounter >= maxTrimCount) { + MatrixLog.w(TAG, "onLowMemory skip for frequency") + return + } + lastTrimTimeMillis = current + trimCounter++ + MatrixLog.e(TAG, "onTrimMemory post: $level") + MatrixHandlerThread.getDefaultHandler().post { + MatrixLog.e(TAG, "onTrimMemory: $level") + procTrimCallbacks.systemTrim(level) + appTrimCallbacks.systemTrim(level) + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) {} + }) + + // @formatter:off + // process staged bg trim + TrimTask("ProcessStagedBg", ProcessStagedBackgroundOwner, procTrimCallbacks, config, false).init() + + // process deep bg trim + TrimTask("ProcessDeepBg", ProcessDeepBackgroundOwner, procTrimCallbacks, config, true).init() + + // app staged bg trim + TrimTask("AppStagedBg", AppStagedBackgroundOwner, appTrimCallbacks, config, false).init() + + // app deep bg trim + TrimTask("AppDeepBg", AppDeepBackgroundOwner, appTrimCallbacks, config, true).init() + // @formatter:on + } + + fun addProcessBackgroundTrimCallback(callback: TrimCallback) { + synchronized(procTrimCallbacks) { + procTrimCallbacks.add(callback) + } + } + + fun addProcessBackgroundTrimCallback(lifecycleOwner: LifecycleOwner, callback: TrimCallback) { + synchronized(procTrimCallbacks) { + procTrimCallbacks.add(callback) + } + lifecycleOwner.lifecycle.addObserver(object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun release() { + synchronized(procTrimCallbacks) { + procTrimCallbacks.remove(callback) + } + } + }) + } + + fun addAppBackgroundTrimCallback(callback: TrimCallback) { + synchronized(appTrimCallbacks) { + appTrimCallbacks.add(callback) + } + } + + fun addAppBackgroundTrimCallback(lifecycleOwner: LifecycleOwner, callback: TrimCallback) { + synchronized(appTrimCallbacks) { + appTrimCallbacks.add(callback) + } + lifecycleOwner.lifecycle.addObserver(object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun release() { + synchronized(appTrimCallbacks) { + appTrimCallbacks.remove(callback) + } + } + }) + } + + fun removeTrimCallback(callback: TrimCallback) { + synchronized(procTrimCallbacks) { + procTrimCallbacks.remove(callback) + } + synchronized(appTrimCallbacks) { + appTrimCallbacks.remove(callback) + } + } + + fun triggerTrim() { + procTrimCallbacks.backgroundTrim() + appTrimCallbacks.backgroundTrim() + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-canary/src/test/java/com/tencent/matrix_memory_canary/ExampleUnitTest.kt b/matrix/matrix-android/matrix-memory-canary/src/test/java/com/tencent/matrix_memory_canary/ExampleUnitTest.kt new file mode 100644 index 000000000..b4ca0c558 --- /dev/null +++ b/matrix/matrix-android/matrix-memory-canary/src/test/java/com/tencent/matrix_memory_canary/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.tencent.matrix_memory_canary + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-dump/gradle.properties b/matrix/matrix-android/matrix-memory-dump/gradle.properties deleted file mode 100644 index 9ec625401..000000000 --- a/matrix/matrix-android/matrix-memory-dump/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -POM_ARTIFACT_ID=matrix-memory-dump \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/dlfcn/self_dlfcn.h b/matrix/matrix-android/matrix-memory-dump/src/main/cpp/dlfcn/self_dlfcn.h deleted file mode 100644 index ef079568a..000000000 --- a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/dlfcn/self_dlfcn.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// Created by M.D. on 2021/10/29. -// - -#ifndef MATRIX_ANDROID_SELF_DLFCN_H -#define MATRIX_ANDROID_SELF_DLFCN_H - -extern "C" { - -void self_dlfcn_mode(int api); - -void self_dlclose(void *handle); - -void self_clean(void* handle); - -void *self_dlopen(const char *filename); - -void *self_dlsym(void *handle, const char *name); - -} - -#endif //MATRIX_ANDROID_SELF_DLFCN_H diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/internal/log.h b/matrix/matrix-android/matrix-memory-dump/src/main/cpp/internal/log.h deleted file mode 100644 index fe7c33fca..000000000 --- a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/internal/log.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// Created by M.D. on 2021/10/26. -// - -#ifndef MATRIX_ANDROID_LOG_H -#define MATRIX_ANDROID_LOG_H - -#if defined(__aarch64__) || defined(__arm__) - -#include - -#define _info_log(tag, fmt, args...) LOGI(tag, FMT, ##args) -#define _debug_log(tag, fmt, args...) LOGD(tag, FMT, ##args) -#define _error_log(tag, fmt, args...) LOGE(tag, FMT, ##args) - -#else - -#include - -#define _info_log(tag, fmt, args...) __android_log_print(ANDROID_LOG_INFO, tag, fmt, ##args) -#define _debug_log(tag, fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, tag, fmt, ##args) -#define _error_log(tag, fmt, args...) __android_log_print(ANDROID_LOG_ERROR, tag, fmt, ##args) - -#endif - -#endif diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/mem_dump.cpp b/matrix/matrix-android/matrix-memory-dump/src/main/cpp/mem_dump.cpp deleted file mode 100644 index 64207d6d8..000000000 --- a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/mem_dump.cpp +++ /dev/null @@ -1,357 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "bionic/tls.h" -#include "internal/log.h" -#include "runtime/collector_type.h" -#include "runtime/gc_cause.h" -#include "dlfcn/self_dlfcn.h" - -#define LOG_TAG "Matrix.MemoryDump" - -#define READ 0 -#define WRITE 1 - -struct fork_pair { - int pid; - int fd; -}; - -using namespace art::gc; - -namespace mirror { - - static void *suspend_all_ptr_ = nullptr; - - static void *resume_all_ptr_ = nullptr; - - class Thread { - }; - - class ScopedSuspend { - }; - - static void (*sgc_constructor)(void *, Thread *, GcCause, CollectorType) = nullptr; - - static void (*sgc_destructor)(void *) = nullptr; - - class ScopedGCCriticalSection { - private: - uint64_t buf[8] = {0}; - public: - ScopedGCCriticalSection(Thread *thread, GcCause cause, CollectorType collectorType) { - if (sgc_constructor != nullptr) { - sgc_constructor(this, thread, cause, collectorType); - } - } - - ~ScopedGCCriticalSection() { - if (sgc_destructor != nullptr) { - sgc_destructor(this); - } - } - }; - - static void (*exclusive_lock)(void *, Thread *) = nullptr; - - static void (*exclusive_unlock)(void *, Thread *) = nullptr; - - class ReadWriteMutex { - public: - void ExclusiveLock(Thread *self) { - if (exclusive_lock != nullptr) { - reinterpret_cast(exclusive_lock)(this, self); - } - } - - void ExclusiveUnlock(Thread *self) { - if (exclusive_unlock != nullptr) { - reinterpret_cast(exclusive_unlock)(this, self); - } - } - }; -} - -static int android_version_; - -static mirror::ScopedSuspend suspend_; - -static mirror::ReadWriteMutex *mutator_lock_ = nullptr; - -static void (*dump_heap_)(const char *, int, bool) = nullptr; - -static void suspend_runtime(mirror::Thread *thread) { - if (android_version_ > __ANDROID_API_Q__) { - mirror::ScopedGCCriticalSection sgc(thread, kGcCauseHprof, kCollectorTypeHprof); - - if (mirror::suspend_all_ptr_ != nullptr) { - reinterpret_cast - (mirror::suspend_all_ptr_)(&suspend_, "matrix_dump_hprof", true); - } else { - _error_log(LOG_TAG, - "Cannot suspend runtime because suspend function symbol cannot be found."); - } - - mutator_lock_->ExclusiveUnlock(thread); - } else { - if (mirror::suspend_all_ptr_ != nullptr) { - reinterpret_cast(mirror::suspend_all_ptr_)(); - } else { - _error_log(LOG_TAG, - "Cannot suspend runtime because suspend function symbol cannot be found."); - } - } -} - -static void resume_runtime(mirror::Thread *thread) { - if (android_version_ > __ANDROID_API_Q__) { - mutator_lock_->ExclusiveLock(thread); - - if (mirror::resume_all_ptr_ != nullptr) { - reinterpret_cast(mirror::resume_all_ptr_)(&suspend_); - } else { - _error_log(LOG_TAG, - "Cannot suspend runtime because suspend function symbol cannot be found."); - } - } else { - if (mirror::resume_all_ptr_ != nullptr) { - reinterpret_cast(mirror::resume_all_ptr_)(); - } else { - _error_log(LOG_TAG, - "Cannot resume runtime because resume function symbol cannot be found."); - } - } -} - -static bool initialize_functions( - void *(*_dl_open)(const char *), - void *(*_dl_sym)(void *, const char *), - void (*_dl_close)(void *), - void (*_dl_clean)(void *), - const char *impl_tag -) { - auto *art_lib = _dl_open("libart.so"); - - if (art_lib == nullptr) { - _error_log(LOG_TAG, "Cannot dynamic open library libart.so with %s.", impl_tag); - return false; - } - - auto *lock_sym = reinterpret_cast( - _dl_sym(art_lib, "_ZN3art5Locks13mutator_lock_E")); - if (lock_sym == nullptr) { - _error_log(LOG_TAG, "Cannot find symbol art::Locks::mutator_lock_."); - goto on_error; - } else { - mutator_lock_ = *lock_sym; - } - -#define _load_symbol_unsafe(ptr, type, sym, err) \ - ptr = reinterpret_cast(_dl_sym(art_lib, sym)); \ - if (ptr == nullptr) { \ - _info_log(LOG_TAG, err); \ - } - -#define _load_symbol(ptr, type, sym, err) \ - ptr = reinterpret_cast(_dl_sym(art_lib, sym)); \ - if (ptr == nullptr) { \ - _error_log(LOG_TAG, err); \ - goto on_error; \ - } - - _load_symbol_unsafe(dump_heap_, - void(*)(const char *, int, bool ), - "_ZN3art5hprof8DumpHeapEPKcib", - "Cannot find symbol art::hprof::DumpHeap") - - if (android_version_ > __ANDROID_API_Q__) { - _load_symbol(mirror::suspend_all_ptr_, - void*, - "_ZN3art16ScopedSuspendAllC1EPKcb", - "Cannot find symbol art::ScopedSuspendAll().") - _load_symbol(mirror::resume_all_ptr_, - void*, - "_ZN3art16ScopedSuspendAllD1Ev", - "Cannot find symbol art::~ScopedSuspendAll().") - - _load_symbol(mirror::sgc_constructor, - void(*)(void * , mirror::Thread *, GcCause, CollectorType), - "_ZN3art2gc23ScopedGCCriticalSectionC1EPNS_6ThreadENS0_7GcCauseENS0_13CollectorTypeE", - "Cannot find symbol art::gc::ScopedGCCriticalSection().") - _load_symbol(mirror::sgc_destructor, - void(*)(void * ), - "_ZN3art2gc23ScopedGCCriticalSectionD1Ev", - "Cannot find symbol art::gc::~ScopedGCCriticalSection().") - - _load_symbol(mirror::exclusive_lock, - void(*)(void * , mirror::Thread *), - "_ZN3art17ReaderWriterMutex13ExclusiveLockEPNS_6ThreadE", - "Cannot find symbol art::ReaderWriterMutex::ExclusiveLock().") - _load_symbol(mirror::exclusive_unlock, - void(*)(void * , mirror::Thread *), - "_ZN3art17ReaderWriterMutex15ExclusiveUnlockEPNS_6ThreadE", - "Cannot find symbol art::ReaderWriterMutex::ExclusiveUnlock().") - } else { - _load_symbol(mirror::suspend_all_ptr_, - void*, - "_ZN3art3Dbg9SuspendVMEv", - "Cannot find symbol art::Dbg::SuspendVM.") - - _load_symbol(mirror::resume_all_ptr_, - void*, - "_ZN3art3Dbg8ResumeVMEv", - "Cannot find symbol art::Dbg::ResumeVM.") - } - - _dl_clean(art_lib); - return true; - - on_error: - _dl_close(art_lib); - return false; -} - -static bool initialize_functions_with_self() { - return initialize_functions(self_dlopen, self_dlsym, self_dlclose, self_clean, "self_dlfcn"); -} - -static bool initialize_functions_with_semi() { - return initialize_functions( - semi_dlopen, - reinterpret_cast(semi_dlsym), - semi_dlclose, - semi_dlclose, - "semi_dlfcn" - ); -} - -static void fork_process_crash_handler(int signal_num) { - // Do nothing, just tell parent process to handle the error. - exit(-3); -} - -extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_initializeNative(JNIEnv *, jclass) { - - { - char api_level[5]; - if (__system_property_get("ro.build.version.sdk", api_level) < 1) { - _error_log(LOG_TAG, "Unable to get system property ro.build.version.sdk."); - } - android_version_ = static_cast(strtol(api_level, nullptr, 10)); - } - - // Set Android API version first to decide whether to use native "dlfcn" implementation or not. - self_dlfcn_mode(android_version_); - bool ret = initialize_functions_with_self(); - if (!ret) { - ret = initialize_functions_with_semi(); - } - return ret; -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_dumpHprof(JNIEnv *, jclass, jint fd) { - if (dump_heap_ != nullptr) { - dump_heap_("[fd]", fd, false); - return 0; - } else { - _error_log(LOG_TAG, "Failed to load art::hprof::DumpHeap()."); - return -1; - } -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_fork(JNIEnv *, jclass, jlong child_timeout) { - auto *thread = reinterpret_cast(__get_tls()[TLS_SLOT_ART_THREAD_SELF]); - suspend_runtime(thread); - int pid = fork(); - if (pid == 0) { - signal(SIGSEGV, fork_process_crash_handler); - alarm(child_timeout); - prctl(PR_SET_NAME, "matrix_dump_process"); - } else { - resume_runtime(thread); - } - return pid; -} - -extern "C" JNIEXPORT jlong JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_forkPipe(JNIEnv *, jclass, jlong child_timeout) { - int fd[2]; - if (pipe(fd)) { - return -2; - } - - auto *ret = reinterpret_cast(malloc(sizeof(fork_pair))); - if (ret == nullptr) { - close(fd[READ]); - close(fd[WRITE]); - return -2; - } - - auto *thread = reinterpret_cast(__get_tls()[TLS_SLOT_ART_THREAD_SELF]); - suspend_runtime(thread); - - int pid = fork(); - ret->pid = pid; - if (pid == 0) { - close(fd[READ]); - ret->fd = fd[WRITE]; - signal(SIGSEGV, fork_process_crash_handler); - alarm(child_timeout); - prctl(PR_SET_NAME, "matrix_dump_process"); - return reinterpret_cast(ret); - } else { - resume_runtime(thread); - close(fd[WRITE]); - if (pid == -1) { - close(fd[READ]); - free(ret); - return -1; - } else { - ret->fd = fd[READ]; - return reinterpret_cast(ret); - } - } -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_wait(JNIEnv *, jclass, jint pid) { - int status; - if (waitpid(pid, &status, 0) == -1) { - _error_log(LOG_TAG, "Failed to invoke waitpid()."); - return -2; - } - - if (WIFEXITED(status)) { - return WEXITSTATUS(status); - } else { - _error_log(LOG_TAG, "Fork process exited unexpectedly."); - return -2; - } -} - -extern "C" JNIEXPORT void JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_exit(JNIEnv *, jclass, jint code) { - _exit(code); -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_pidFromForkPair(JNIEnv *, jclass, jlong pointer) { - return reinterpret_cast(pointer)->pid; -} - -extern "C" JNIEXPORT jint JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_fdFromForkPair(JNIEnv *, jclass, jlong pointer) { - return reinterpret_cast(pointer)->fd; -} - -extern "C" JNIEXPORT void JNICALL -Java_com_tencent_matrix_memorydump_MemoryDumpKt_free(JNIEnv *, jclass, jlong pointer) { - free(reinterpret_cast(pointer)); -} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/java/com/tencent/matrix/memorydump/MemoryDump.kt b/matrix/matrix-android/matrix-memory-dump/src/main/java/com/tencent/matrix/memorydump/MemoryDump.kt deleted file mode 100644 index 583032da9..000000000 --- a/matrix/matrix-android/matrix-memory-dump/src/main/java/com/tencent/matrix/memorydump/MemoryDump.kt +++ /dev/null @@ -1,337 +0,0 @@ -package com.tencent.matrix.memorydump - -import android.os.Build -import android.os.Debug -import android.os.Process -import java.util.concurrent.Future -import java.util.concurrent.FutureTask -import com.tencent.matrix.util.MatrixLog -import java.io.BufferedInputStream -import java.io.FileInputStream -import java.io.InputStream -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors - -private const val TAG = "Matrix.MemoryDump" - -/** - * Memory dump manager. - * - * The manager is used to dump heap memory into a HPROF file, like [Debug.dumpHprofData]. However, - * there are several API of manager can dump memory without suspend runtime. The manager will fork - * a new process for dumping (See [Copy-on-Write](https://en.wikipedia.org/wiki/Copy-on-write)). - * - * The idea is from [KOOM](https://github.com/KwaiAppTeam/KOOM). - * - * @author aurorani - * @since 2021/10/20 - */ -object MemoryDumpManager { - - private val initialized: Boolean = initialize() - - private const val DEFAULT_DUMP_TIMEOUT = 60L - - private val dumpExecutor = Executors.newSingleThreadExecutor { - Thread(it, "matrix_memory_dump_executor") - } - - /** - * Callback when dump process completes dumping memory. - * - * TODO: Convert to SAM interface after upgrading Kotlin version to 1.4. - * - * @author aurorani - * @since 2021/10/25 - */ - interface DumpCallback { - fun onDumpComplete(result: Boolean) - } - - /** - * Dump HPROF to specific file on [path]. It will suspend the whole runtime. - * - * The function may cause jank and garbage collection, and will not throw exceptions but return - * false and print stack trace if error happened. - */ - @JvmStatic - fun dumpSuspend(path: String): Boolean { - if (!initialized) { - MatrixLog.e(TAG, "Memory dump manager is not successfully initialized. Skip dump.") - return false - } - return try { - Debug.dumpHprofData(path) - true - } catch (exception: Exception) { - MatrixLog.printErrStackTrace(TAG, exception, "") - false - } - } - - /** - * Dump HPROF to specific file on [path]. Compared to [dumpSuspend], it will only suspend current - * thread. - * - * The function may cause jank and garbage collection, and will not throw exceptions but return - * false and print stack trace if error happened. - */ - @JvmStatic - @JvmOverloads - fun dumpBlock(path: String, timeout: Long = DEFAULT_DUMP_TIMEOUT): Boolean { - if (!initialized) { - MatrixLog.e(TAG, "Memory dump manager is not successfully initialized. Skip dump.") - return false - } - MatrixLog.i(TAG, "[dump block, pid: ${Process.myPid()}] Fork dump process.") - return when (val pid = fork(timeout)) { - -1 -> run { - MatrixLog.e(TAG, "Cannot fork child dump process.") - false - } - 0 -> run { - try { - if (dumpSuspend(path)) exit(0) else exit(-1) - } catch (exception: Exception) { - // The catch block may be unnecessary, because the runtime is broken and cannot - // create any exception to throw. But keep the code, whatever. - MatrixLog.printErrStackTrace(TAG, exception, "") - exit(-2) - } - true - } - else -> run { - MatrixLog.i( - TAG, - "[dump block, pid: ${Process.myPid()}] Wait dump complete (dump process pid: ${pid})." - ) - val result = wait(pid) - MatrixLog.i( - TAG, - "[dump block, pid: ${Process.myPid()}] Dump complete, status code: $result." - ) - result == 0 - } - } - } - - /** - * Dump HPROF to specific file on [path]. It will dump memory as an asynchronous computation. - * - * The function may cause jank and garbage collection, and will not throw exceptions but return - * false and print stack trace if error happened. - */ - @JvmStatic - fun dumpAsync(path: String): Future { - if (!initialized) { - MatrixLog.e(TAG, "Memory dump manager is not successfully initialized. Skip dump.") - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - CompletableFuture.completedFuture(false) - } else { - dumpExecutor.submit { false } - } - } - return dumpExecutor.submit { - dumpBlock(path) - } - } - - /** - * Dump HPROF to specific file on [path]. It will dump memory as an asynchronous computation. - * - * The function may cause jank and garbage collection, and will not throw exceptions but return - * false and print stack trace if error happened. - */ - @JvmStatic - fun dumpAsync(path: String, callback: DumpCallback) { - if (!initialized) { - MatrixLog.e(TAG, "Memory dump manager is not successfully initialized. Skip dump.") - callback.onDumpComplete(false) - } - dumpExecutor.execute { - callback.onDumpComplete(dumpBlock(path)) - } - } - - /** - * Dump HPROF as a buffered input stream. It will dump memory as an asynchronous computation. - * - * The function returns a dump handler including HPROF file input stream instance and a [Future] - * object for listening dump process status, see [DumpHandler]. - * - * The buffer size of buffered input stream will be set as default value of [BufferedInputStream] - * if [bufferSize] is not positive. - * - * TODO: Extract function for two [dumpStream] using inline. DO NOT ALLOCATE ANY OBJECT, INCLUDING LAMBDA. - * - * The function may cause jank and garbage collection, and will not throw exceptions but return - * false and print stack trace if error happened. - */ - @JvmStatic - @JvmOverloads - fun dumpStream( - bufferSize: Int = 0, - timeout: Long = DEFAULT_DUMP_TIMEOUT - ): DumpHandler? { - if (!initialized) { - MatrixLog.e(TAG, "Memory dump manager is not successfully initialized. Skip dump.") - return null - } - val pointer = forkPipe(timeout) - if (pointer == -1L) { - MatrixLog.e(TAG, "Cannot fork child dump process.") - return null - } - if (pointer == -2L) { - MatrixLog.e(TAG, "Failed to allocate native resource in parent process.") - return null - } - MatrixLog.i(TAG, "[dump stream, pid: ${Process.myPid()}] Fork dump process.") - val pid = pidFromForkPair(pointer) - val fd = fdFromForkPair(pointer) - return when (pid) { - 0 -> run { - try { - exit(dumpHprof(fd)) - } catch (exception: Exception) { - // The catch block may be unnecessary, because the runtime is broken and cannot - // create any exception to throw. But keep the code, whatever. - MatrixLog.printErrStackTrace(TAG, exception, "") - exit(-1) - } - throw RuntimeException("Unreachable code.") - } - else -> { - free(pointer) - val stream = FileInputStream("/proc/self/fd/${fd}").let { - if (bufferSize > 0) BufferedInputStream(it, bufferSize) - else BufferedInputStream(it) - } - val future = FutureTask { - MatrixLog.i( - TAG, - "[dump stream, pid: ${Process.myPid()}] Wait dump complete (dump process pid: ${pid})." - ) - val result = wait(pid) - MatrixLog.i( - TAG, - "[dump stream, pid: ${Process.myPid()}] Dump complete, status code: $result." - ) - result == 0 - } - DumpHandler(stream, future) - } - } - } -} - -/** - * Instance for handling remote memory dump. - * - * **Developer should invokes [InputStream.close] manually after dump process completes execution.** - * - * @property stream HPROF file input stream. - * @property result [Future] instance to check dump execution is done or not and dump result. - */ -class DumpHandler internal constructor( - val stream: InputStream, - val result: Future -) - -/* - Native Function Handlers. - */ - -private fun initialize(): Boolean { - System.loadLibrary("matrix-memorydump") - val success = initializeNative() - if (!success) { - MatrixLog.printErrStackTrace( - TAG, - RuntimeException("Failed to initialize native resources."), - "" - ) - } - return success -} - -/** - * Initialize native resources. - */ -private external fun initializeNative(): Boolean - -/** - * Dump HPROF file to specific file which file descriptor [fd] points to. The function returns 0 if - * dump successfully, or -1 if error happened. - * - * The function is implemented with art::hprof::DumpHeap() in libart.so. - */ -private external fun dumpHprof(fd: Int): Int - -/** - * Fork dump process. To solve the deadlock, this function will suspend the runtime and resume it in - * parent process. - * - * The function is implemented with native standard fork(). - * See [man fork](https://man7.org/linux/man-pages/man2/fork.2.html). - */ -private external fun fork(timeout: Long): Int - -/** - * Fork dump process with a pipe for transferring data. To solve the deadlock, this function will - * suspend the runtime and resume it in parent process. - * - * The code this function may returns: - * -1: Cannot fork child dump process. - * -2: Failed to allocate native resource in parent process. - * others: The return value is a pointer of integer pair in native. The integer pair is "forked dump - * process id - hprof-reading file descriptor". - * - * Reason for designing the return value: - * The runtime cannot allocate any object after invoking fork() in forked process. - * - * The function is implemented with native standard fork() and pipe(). - * See [man fork](https://man7.org/linux/man-pages/man2/fork.2.html) - * and [man pipe](https://man7.org/linux/man-pages/man2/pipe.2.html). - */ -private external fun forkPipe(timeout: Long): Long - -/** - * Wait dump process exits and return exit status of child process. - * - * The function is implemented with native standard waitpid(). - * See [man wait](https://man7.org/linux/man-pages/man2/wait.2.html). - */ -private external fun wait(pid: Int): Int - -/** - * Exit current process. - * - * The function is implemented with native standard _exit(). - * See [man _exit](https://man7.org/linux/man-pages/man2/exit.2.html). - */ -private external fun exit(code: Int) - -/** - * Get process id member value of native fork_pair instance by [pointer]. - * - * Reason for designing structure in native: - * The runtime cannot allocate any object after invoking fork() in forked process. - */ -private external fun pidFromForkPair(pointer: Long): Int - -/** - * Get file descriptor member value of native fork_pair instance by [pointer]. - * - * Reason for designing structure in native: - * The runtime cannot allocate any object after invoking fork() in forked process. - */ -private external fun fdFromForkPair(pointer: Long): Int - -/** - * Free the native memory space pointed to by [pointer]. - * - * The function is implemented with native standard free(). - * See [man free](https://man7.org/linux/man-pages/man3/free.3.html). - */ -private external fun free(pointer: Long) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-opengl-leak/CMakeLists.txt b/matrix/matrix-android/matrix-opengl-leak/CMakeLists.txt index 3c04ffc6a..28dac4690 100644 --- a/matrix/matrix-android/matrix-opengl-leak/CMakeLists.txt +++ b/matrix/matrix-android/matrix-opengl-leak/CMakeLists.txt @@ -23,7 +23,8 @@ set( SOURCE_FILES ${SOURCE_DIR}/com_tencent_matrix_openglleak_detector_FuncSeeker.cpp ${SOURCE_DIR}/com_tencent_matrix_openglleak_hook_OpenGLHook.cpp - ${SOURCE_DIR}/com_tencent_matrix_openglleak_statistics_resource_ResRecordManager.cpp + ${SOURCE_DIR}/BufferQueue.cpp + ${SOURCE_DIR}/StackMeta.cpp ${SOURCE_DIR}/type.cpp ${SOURCE_DIR}/get_tls.cpp ) @@ -74,6 +75,9 @@ target_link_libraries( # Specifies the target library. PRIVATE ${log-lib} GLESv2 GLESv3 + EGL + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libsemi_dlfcn.a PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libwechatbacktrace.so ) diff --git a/matrix/matrix-android/matrix-opengl-leak/build.gradle b/matrix/matrix-android/matrix-opengl-leak/build.gradle index e44c150f6..0ffb14c84 100644 --- a/matrix/matrix-android/matrix-opengl-leak/build.gradle +++ b/matrix/matrix-android/matrix-opengl-leak/build.gradle @@ -7,7 +7,7 @@ android { buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - minSdkVersion rootProject.ext.minSdkVersion + minSdkVersion MIN_SDK_VERSION_FOR_HOOK targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName rootProject.ext.VERSION_NAME diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/BufferQueue.cpp b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/BufferQueue.cpp new file mode 100644 index 000000000..38b2e96be --- /dev/null +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/BufferQueue.cpp @@ -0,0 +1,134 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "BufferQueue.h" + +namespace matrix { + + std::atomic BufferQueueContainer::g_locker_collision_counter = 0; + + message_queue_counter_t BufferQueue::g_message_queue_counter_{}; + + BufferManagement::BufferManagement() { + containers_.reserve(MAX_PTR_SLOT); + for (int i = 0; i < MAX_PTR_SLOT; ++i) { + auto container = new BufferQueueContainer(); + containers_.emplace_back(container); + } + } + + BufferManagement::~BufferManagement() { + + for (auto container : containers_) { + delete container; + } + + delete queue_swapped_; + } + + [[noreturn]] void BufferManagement::process_routine(BufferManagement *this_) { + while (true) { + if (!this_->queue_swapped_) this_->queue_swapped_ = new BufferQueue(SIZE_AUGMENT); + + size_t busy_queue = 0; + for (auto container : this_->containers_) { + BufferQueue *swapped = nullptr; + { + std::lock_guard lock(container->mutex_); + if (container->queue_ && !container->queue_->empty()) { + swapped = container->queue_; + container->queue_ = this_->queue_swapped_; + } + } + + if (swapped && swapped->size() >= 5) { + busy_queue++; + } + if (swapped) { + swapped->process([&](message_t *message) { + message->runnable(); + }); + swapped->reset(); + this_->queue_swapped_ = swapped; + } + } + this_->busy_ratio = ((float) busy_queue) / this_->containers_.size(); + + if (this_->busy_ratio > 0.9f) { // Super busy + continue; + } else if (this_->busy_ratio > 0.6f) { // Busy + usleep(PROCESS_BUSY_INTERVAL); + } else if (this_->busy_ratio > 0.3f) { + usleep(PROCESS_NORMAL_INTERVAL); + } else if (this_->busy_ratio > 0.1f) { + usleep(PROCESS_LESS_NORMAL_INTERVAL); + } else { + usleep(PROCESS_IDLE_INTERVAL); + } + } + } + + int BufferManagement::get_queue_size() { + + int queue_size = 0; + for (auto container : containers_) { + { + std::lock_guard lock(container->mutex_); + if (container->queue_ && !container->queue_->empty()) { + queue_size += container->queue_->size(); + } + } + } + return queue_size; + } + + void BufferManagement::start_process() { + if (processing_) return; + processing_ = true; + pthread_create(&thread_, nullptr, + reinterpret_cast(&BufferManagement::process_routine), + this); + pthread_setname_np(thread_, "matrix.gl_rou"); + pthread_detach(thread_); + } + + void BufferManagement::enqueue_message(uintptr_t key, std::function runnable) { + BufferQueueContainer *container = this->containers_[container_hash(key)]; + container->lock(); + container->queue_->enqueue_message(std::move(runnable)); + container->unlock(); + } + +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/BufferQueue.h b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/BufferQueue.h new file mode 100644 index 000000000..3fac1c81b --- /dev/null +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/BufferQueue.h @@ -0,0 +1,254 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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. + */ + +#ifndef LIBMATRIX_HOOK_MEMORY_BUFFER_QUEUE_H +#define LIBMATRIX_HOOK_MEMORY_BUFFER_QUEUE_H + +#include +#include +#include +#include "../../../../matrix-hooks/src/main/cpp/common/Macros.h" +#include "functional" + +namespace matrix { + + __attribute__((noinline)) void _hook_check(bool assertion); + + static const unsigned int MAX_PTR_SLOT = 1 << 8; + static const unsigned int PTR_MASK = MAX_PTR_SLOT - 1; + + struct message_t { + std::function runnable; + }; + + static inline size_t container_hash(uintptr_t _key) { + return static_cast((_key ^ (_key >> 16)) & PTR_MASK); + } + + typedef struct { + std::atomic realloc_memory_counter_; + std::atomic realloc_counter_; + std::atomic realloc_failure_counter_; + std::atomic realloc_reach_limit_counter_; + } message_queue_counter_t; + + class MessageAllocator { + public: + MessageAllocator( + const size_t size_augment, + const size_t memory_limit, + message_queue_counter_t *const counter) + : size_augment_(size_augment), memory_limit_(memory_limit), counter_(counter) { + HOOK_CHECK(size_augment); + HOOK_CHECK(memory_limit); + HOOK_CHECK(counter); + }; + + ~MessageAllocator() { + free(queue_); + }; + + message_queue_counter_t *const counter_; + + const size_t size_augment_; + const size_t memory_limit_; + + size_t size_ = 0; + size_t idx_ = 0; + message_t *queue_ = nullptr; + + inline void reset() { + idx_ = 0; + if (size_ > size_augment_) { + buffer_realloc(true); + } + } + + inline bool check_realloc() { + if (UNLIKELY(idx_ >= size_)) { + if (UNLIKELY(!buffer_realloc(false))) { + return false; + } + } + + return true; + } + + inline bool buffer_realloc(bool init) { + if (UNLIKELY(!init && + counter_->realloc_memory_counter_.load(std::memory_order_relaxed) >= + memory_limit_)) { + counter_->realloc_reach_limit_counter_.fetch_add(1, std::memory_order_relaxed); + return false; + } + + if (!init && size_ != 0) { + counter_->realloc_counter_.fetch_add(1, std::memory_order_relaxed); + } + + size_t resize = init ? size_augment_ : (size_ + size_augment_); + + void *buffer = calloc(resize, sizeof(message_t)); + + if (LIKELY(buffer)) { + if (!init) { + message_t *copy = static_cast(buffer); + for (int i = 0; i < size_; ++i) { + copy[i] = message_t(); + copy[i].runnable = queue_[i].runnable; + queue_[i].runnable = nullptr; + } + free(queue_); + } + + if (init && size_ != 0) { + for (int i = 0; i < size_; ++i) { + queue_[i].runnable = nullptr; + } + free(queue_); + } + + queue_ = static_cast(buffer); + + counter_->realloc_memory_counter_.fetch_sub(sizeof(message_t) * size_); + size_ = resize; + counter_->realloc_memory_counter_.fetch_add(sizeof(message_t) * size_); + return true; + } else { + counter_->realloc_failure_counter_.fetch_add(1, std::memory_order_relaxed); + return false; + } + } + }; + + class BufferQueue { + + public: + BufferQueue(size_t size_augment) { + const size_t limit_size = (MEMORY_OVER_LIMIT / (sizeof(message_t) * 2)); + + messages_ = new MessageAllocator( + size_augment * 2, + limit_size * 2 * sizeof(message_t), + &g_message_queue_counter_); + messages_->buffer_realloc(true); + }; + + ~BufferQueue() { + delete messages_; + }; + + void enqueue_message(std::function runnable) { + if (UNLIKELY(!messages_->check_realloc())) { + return; + } + + message_t *message = &messages_->queue_[messages_->idx_++]; + message->runnable = runnable; + } + + bool empty() const { + return messages_->idx_ == 0; + } + + void reset() { + messages_->reset(); + } + + size_t size() { + return messages_->idx_; + } + + void process(std::function callback) { + for (size_t i = 0; i < messages_->idx_; i++) { + message_t *message = reinterpret_cast(&messages_->queue_[i]); + callback(message); + } + } + + MessageAllocator *messages_; + + // Statistic + static std::atomic g_queue_extra_stack_meta_allocated; + static std::atomic g_queue_extra_stack_meta_kept; + static message_queue_counter_t g_message_queue_counter_; + static message_queue_counter_t g_allocation_queue_counter_; + + private: + + }; + + class BufferQueueContainer { + public: + BufferQueueContainer() : queue_(nullptr) {} + + ~BufferQueueContainer() { + delete queue_; + } + + BufferQueue *queue_; + + std::mutex mutex_; + + static std::atomic g_locker_collision_counter; + + inline void lock() { + if (UNLIKELY(!mutex_.try_lock())) { + g_locker_collision_counter.fetch_add(1, std::memory_order_relaxed); + mutex_.lock(); + } + + if (UNLIKELY(!queue_)) { + queue_ = new BufferQueue(SIZE_AUGMENT); + } + } + + inline void unlock() { + mutex_.unlock(); + } + }; + + class BufferManagement { + public: + + void enqueue_message(uintptr_t key, std::function runnable); + + BufferManagement(); + + ~BufferManagement(); + + void start_process(); + + int get_queue_size(); + + private: + + float busy_ratio; + + std::vector containers_; + + [[noreturn]] static void process_routine(BufferManagement *this_); + + BufferQueue *queue_swapped_ = nullptr; + + bool processing_ = false; + + pthread_t thread_{}; + }; + +} + +#endif //LIBMATRIX_HOOK_MEMORY_BUFFER_QUEUE_H diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/StackMeta.cpp b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/StackMeta.cpp new file mode 100644 index 000000000..5a11fe97a --- /dev/null +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/StackMeta.cpp @@ -0,0 +1,114 @@ +#include "StackMeta.h" + +inline uint64_t stack_meta_hash(wechat_backtrace::Backtrace *backtrace) { + uint64_t sum = 1; + if (backtrace == nullptr) { + return (uint64_t) sum; + } + for (size_t i = 0; i != backtrace->frame_size; i++) { + sum += (backtrace->frames.get())[i].pc; + } + return (uint64_t) sum; +} + +inline bool stacktrace_compare(wechat_backtrace::Backtrace *a, wechat_backtrace::Backtrace *b) { + if (a->frame_size != b->frame_size) { + return false; + } + + bool same = true; + for (size_t i = 0; i < a->frame_size; i++) { + if ((a->frames.get())[i].pc == (a->frames.get())[i].pc) { + continue; + } else { + same = false; + break; + } + } + return same; +} + +bool delete_backtrace(wechat_backtrace::Backtrace *backtrace) { + uint64_t hash = stack_meta_hash(backtrace); + + if (hash) { + if (backtrace_map.exist(hash)) { + stack_meta_t *target_ext = &backtrace_map.find(); + bool same = false; + + stack_meta_t *target; + while (target_ext) { + same = stacktrace_compare(backtrace, target_ext->backtrace); + target = target_ext; + if (same) break; + target_ext = static_cast(target->ext); + } + + if (same) { + target->ref_count--; + if (target->ref_count == 0) { + delete target->backtrace; + backtrace_map.remove(hash); + } + return true; + } else { + return false; + } + } + } else { + return false; + } + return false; +} + +wechat_backtrace::Backtrace *deduplicate_backtrace(wechat_backtrace::Backtrace *backtrace) { + if (backtrace == nullptr) { + return nullptr; + } + uint64_t hash = stack_meta_hash(backtrace); + + if (hash) { + stack_meta_t *stack_meta; + + if (backtrace_map.exist(hash)) { + stack_meta = &backtrace_map.find(); + bool same = false; + + stack_meta_t *target = stack_meta; + stack_meta_t *target_ext = stack_meta; + while (target_ext) { + same = stacktrace_compare(backtrace, target_ext->backtrace); + target = target_ext; + if (same) break; + target_ext = static_cast(target->ext); + } + + if (!same) { + if (stack_meta->ref_count == 0 && stack_meta->backtrace->frame_size == 0) { + target = stack_meta; + } else { + target->ext = calloc(1, sizeof(stack_meta_t)); + target = static_cast(target->ext); + } + } + + stack_meta = target; + if (stack_meta->backtrace == nullptr && backtrace->frame_size != 0) { + stack_meta->backtrace = backtrace; + } else if (stack_meta->backtrace->frame_size == 0 && backtrace->frame_size != 0) { + stack_meta->backtrace = backtrace; + } else { + delete backtrace; + } + stack_meta->ref_count++; + } else { + stack_meta = backtrace_map.insert(hash, {0}); + if (backtrace->frame_size != 0) { + stack_meta->backtrace = backtrace; + } + stack_meta->ref_count++; + } + return stack_meta->backtrace; + } + return nullptr; +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/StackMeta.h b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/StackMeta.h new file mode 100644 index 000000000..418b2c98f --- /dev/null +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/StackMeta.h @@ -0,0 +1,22 @@ +#ifndef MATRIX_ANDROID_STACKMETA_H +#define MATRIX_ANDROID_STACKMETA_H + +#define STACK_SPLAY_MAP_CAPACITY 1024 + +#include +#include "Backtrace.h" +#include "splay_map.h" + +struct __attribute__((__packed__)) stack_meta_t { + size_t ref_count; + wechat_backtrace::Backtrace* backtrace; + void *ext; +}; + +static splay_map backtrace_map = splay_map(STACK_SPLAY_MAP_CAPACITY); + +wechat_backtrace::Backtrace *deduplicate_backtrace(wechat_backtrace::Backtrace *backtrace); + +bool delete_backtrace(wechat_backtrace::Backtrace *backtrace); + +#endif //MATRIX_ANDROID_STACKMETA_H diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/buffer_source.h b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/buffer_source.h new file mode 100644 index 000000000..eb0c221dc --- /dev/null +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/buffer_source.h @@ -0,0 +1,75 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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. + */ + +#ifndef buffer_source_h +#define buffer_source_h + +#include +#include +#include +#include + +class buffer_source { +public: + buffer_source() { + _buffer = NULL; + _buffer_size = 0; + } + + virtual ~buffer_source() {} + + inline void *buffer() { return _buffer; } + + inline size_t buffer_size() { return _buffer_size; } + + virtual void *realloc(size_t new_size) = 0; + virtual void free() = 0; + virtual bool init_fail() = 0; + +protected: + void *_buffer; + size_t _buffer_size; +}; + +class buffer_source_memory : public buffer_source { +public: + ~buffer_source_memory() { free(); } + + virtual bool init_fail() { return false; } + + virtual void *realloc(size_t new_size) { + + void *ptr = ::realloc(_buffer, new_size); + if (ptr != NULL) { + + _buffer = ptr; + _buffer_size = new_size; + } + + return ptr; + } + + virtual void free() { + if (_buffer) { + + ::free(_buffer); + _buffer = NULL; + _buffer_size = 0; + } + } +}; + +#endif /* buffer_source_h */ diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_detector_FuncSeeker.cpp b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_detector_FuncSeeker.cpp index 2afafbfa1..e42802d8b 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_detector_FuncSeeker.cpp +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_detector_FuncSeeker.cpp @@ -61,7 +61,7 @@ Java_com_tencent_matrix_openglleak_detector_FuncSeeker_getTargetFuncIndex(JNIEnv *replaceMethod = (void *) _my_glNormal; // 验证是否已经拿到偏移值 - HOOK_O_FUNC(target_func, 0, NULL); + HOOK_O_FUNC(target_func, 0, 0); } if (i_glGenNormal == 500) { @@ -175,7 +175,7 @@ Java_com_tencent_matrix_openglleak_detector_FuncSeeker_getBindFuncIndex(JNIEnv * *replaceMethod = (void *) _my_glBind; // 验证是否已经拿到偏移值 - HOOK_O_FUNC(bind_func, 0, NULL); + HOOK_O_FUNC(bind_func, 0, 0); } if (i_glBind == 500) { diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_hook_OpenGLHook.cpp b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_hook_OpenGLHook.cpp index 58ce9e5dd..77a8a6435 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_hook_OpenGLHook.cpp +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_hook_OpenGLHook.cpp @@ -6,11 +6,15 @@ #include #include #include +#include #include "com_tencent_matrix_openglleak_hook_OpenGLHook.h" #include #include "get_tls.h" #include "type.h" #include "my_functions.h" +#include + +#define HOOK_REQUEST_GROUPID_EGL_HOOK 0x07 extern "C" JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_init (JNIEnv *env, jobject thiz) { @@ -20,43 +24,75 @@ extern "C" JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_Op class_OpenGLHook = (jclass) env->NewGlobalRef(class_OpenGLHook); method_onGlGenTextures = env->GetStaticMethodID(class_OpenGLHook, "onGlGenTextures", - "([ILjava/lang/String;Ljava/lang/String;J)V"); + "([ILjava/lang/String;IJJJJLjava/lang/String;)V"); method_onGlDeleteTextures = env->GetStaticMethodID(class_OpenGLHook, "onGlDeleteTextures", - "([ILjava/lang/String;)V"); - method_onGlGenBuffers = env->GetStaticMethodID(class_OpenGLHook, "onGlGenBuffers", "([ILjava/lang/String;Ljava/lang/String;J)V"); + "([ILjava/lang/String;J)V"); + method_onGlGenBuffers = env->GetStaticMethodID(class_OpenGLHook, "onGlGenBuffers", + "([ILjava/lang/String;IJJJJLjava/lang/String;)V"); method_onGlDeleteBuffers = env->GetStaticMethodID(class_OpenGLHook, "onGlDeleteBuffers", - "([ILjava/lang/String;)V"); + "([ILjava/lang/String;J)V"); method_onGlGenFramebuffers = env->GetStaticMethodID(class_OpenGLHook, "onGlGenFramebuffers", - "([ILjava/lang/String;Ljava/lang/String;J)V"); + "([ILjava/lang/String;IJJJJLjava/lang/String;)V"); method_onGlDeleteFramebuffers = env->GetStaticMethodID(class_OpenGLHook, - "onGlDeleteFramebuffers", "([ILjava/lang/String;)V"); + "onGlDeleteFramebuffers", + "([ILjava/lang/String;J)V"); method_onGlGenRenderbuffers = env->GetStaticMethodID(class_OpenGLHook, - "onGlGenRenderbuffers", "([ILjava/lang/String;Ljava/lang/String;J)V"); + "onGlGenRenderbuffers", + "([ILjava/lang/String;IJJJJLjava/lang/String;)V"); method_onGlDeleteRenderbuffers = env->GetStaticMethodID(class_OpenGLHook, - "onGlDeleteRenderbuffers", "([ILjava/lang/String;)V"); + "onGlDeleteRenderbuffers", + "([ILjava/lang/String;J)V"); method_onGetError = env->GetStaticMethodID(class_OpenGLHook, "onGetError", "(I)V"); - method_getStack = env->GetStaticMethodID(class_OpenGLHook, "getStack", - "()Ljava/lang/String;"); - - method_onGlBindTexture = env->GetStaticMethodID(class_OpenGLHook, "onGlBindTexture", "(II)V"); - method_onGlBindBuffer = env->GetStaticMethodID(class_OpenGLHook, "onGlBindBuffer", "(II)V"); - method_onGlBindFramebuffer = env->GetStaticMethodID(class_OpenGLHook, "onGlBindFramebuffer", "(II)V"); - method_onGlBindRenderbuffer = env->GetStaticMethodID(class_OpenGLHook, "onGlBindRenderbuffer", "(II)V"); - method_onGlTexImage2D = env->GetStaticMethodID(class_OpenGLHook, "onGlTexImage2D", "(IIIIIIIIJLjava/lang/String;J)V"); - method_onGlTexImage3D = env->GetStaticMethodID(class_OpenGLHook, "onGlTexImage3D", "(IIIIIIIIIJLjava/lang/String;J)V"); - method_onGlBufferData = env->GetStaticMethodID(class_OpenGLHook, "onGlBufferData", "(IIJLjava/lang/String;J)V"); - method_onGlRenderbufferStorage = env->GetStaticMethodID(class_OpenGLHook, "onGlRenderbufferStorage", "(IIIIJLjava/lang/String;J)V"); + + method_onGlBindTexture = env->GetStaticMethodID(class_OpenGLHook, "onGlBindTexture", + "(IIJ)V"); + method_onGlBindBuffer = env->GetStaticMethodID(class_OpenGLHook, "onGlBindBuffer", + "(IIJ)V"); + method_onGlBindFramebuffer = env->GetStaticMethodID(class_OpenGLHook, "onGlBindFramebuffer", + "(IIJ)V"); + method_onGlBindRenderbuffer = env->GetStaticMethodID(class_OpenGLHook, + "onGlBindRenderbuffer", "(IIJ)V"); + + method_onGlTexImage2D = env->GetStaticMethodID(class_OpenGLHook, "onGlTexImage2D", + "(IIIIIIIIJIJJ)V"); + method_onGlTexImage3D = env->GetStaticMethodID(class_OpenGLHook, "onGlTexImage3D", + "(IIIIIIIIIJIJJ)V"); + method_onGlBufferData = env->GetStaticMethodID(class_OpenGLHook, "onGlBufferData", + "(IIJIJJ)V"); + method_onGlRenderbufferStorage = env->GetStaticMethodID(class_OpenGLHook, + "onGlRenderbufferStorage", + "(IIIIJIJJ)V"); + + method_getThrowable = env->GetStaticMethodID(class_OpenGLHook, "getThrowable", "()I"); + + method_onEglContextCreate = env->GetStaticMethodID(class_OpenGLHook, "onEglContextCreate", + "(Ljava/lang/String;IJJJLjava/lang/String;)V"); + method_onEglContextDestroy = env->GetStaticMethodID(class_OpenGLHook, "onEglContextDestroy", + "(Ljava/lang/String;JI)V"); + + messages_containers = new BufferManagement(); + messages_containers->start_process(); + pthread_key_create(&g_thread_name_key, [](void *thread_name) { + if (thread_name != nullptr) { + free(thread_name); + } + }); + return true; } return false; } -extern "C" JNIEXPORT void JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_setNativeStackDump (JNIEnv *, jobject thiz, jboolean open) { +extern "C" JNIEXPORT void JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_setNativeStackDump(JNIEnv *, jobject thiz, + jboolean open) { enable_stacktrace(open); } -extern "C" JNIEXPORT void JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_setJavaStackDump (JNIEnv *, jobject thiz, jboolean open) { +extern "C" JNIEXPORT void JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_setJavaStackDump(JNIEnv *, jobject thiz, + jboolean open) { enable_javastack(open); } @@ -65,13 +101,35 @@ JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_VERSION_1_6; } +extern "C" +JNIEXPORT jboolean JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookEgl(JNIEnv *env, jclass clazz) { + system_eglCreateContext = eglCreateContext; + system_eglDestroyContext = eglDestroyContext; + // TODO hook eglCreateXxxSurface() / eglDestroySurface() + + int ret = xhook_grouped_register(HOOK_REQUEST_GROUPID_EGL_HOOK, ".*\\.so$", "eglCreateContext", + (void *) my_egl_context_create, nullptr); + + ret = xhook_grouped_register(HOOK_REQUEST_GROUPID_EGL_HOOK, ".*\\.so$", "eglDestroyContext", + (void *) my_egl_context_destroy, + nullptr); + + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_EGL_HOOK, ".*libmatrix-opengl-leak\\.so$", nullptr); + + xhook_refresh(false); + xhook_export_symtable_hook("libEGL.so", "eglCreateContext", (void *) my_egl_context_create, nullptr); + xhook_export_symtable_hook("libEGL.so", "eglDestroyContext", (void *) my_egl_context_destroy, nullptr); + return ret == 0; +} /* * Class: com_tencent_matrix_openglleak_hook_OpenGLHook * Method: hookGlGenTextures * Signature: (I)Z */ -extern "C" JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlGenTextures +extern "C" JNIEXPORT jboolean JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlGenTextures (JNIEnv *, jclass, jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { @@ -114,7 +172,8 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlDeleteTextures * Method: hookGlGenBuffers * Signature: (I)Z */ -extern "C" JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlGenBuffers +extern "C" JNIEXPORT jboolean JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlGenBuffers (JNIEnv *, jclass, jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { @@ -245,7 +304,8 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlDeleteRenderbuffers * Method: hookGlGetError * Signature: (I)Z */ -extern "C" JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlGetError +extern "C" JNIEXPORT jboolean JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlGetError (JNIEnv *, jclass, jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { @@ -263,7 +323,8 @@ extern "C" JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_Op extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlTexImage2D(JNIEnv *env, jclass clazz, jint index) { +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlTexImage2D(JNIEnv *env, jclass clazz, + jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { return false; @@ -280,7 +341,8 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlTexImage2D(JNIEnv *env, extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlTexImage3D(JNIEnv *env, jclass clazz, jint index) { +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlTexImage3D(JNIEnv *env, jclass clazz, + jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { return false; @@ -297,7 +359,8 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlTexImage3D(JNIEnv *env, extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindTexture(JNIEnv *env, jclass clazz, jint index) { +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindTexture(JNIEnv *env, jclass clazz, + jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { return false; @@ -314,7 +377,8 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindTexture(JNIEnv *env extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindBuffer(JNIEnv *env, jclass clazz, jint index) { +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindBuffer(JNIEnv *env, jclass clazz, + jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { return false; @@ -331,7 +395,8 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindBuffer(JNIEnv *env, extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindFramebuffer(JNIEnv *env, jclass clazz, jint index) { +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindFramebuffer(JNIEnv *env, jclass clazz, + jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { return false; @@ -348,7 +413,8 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindFramebuffer(JNIEnv extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindRenderbuffer(JNIEnv *env, jclass clazz, jint index) { +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindRenderbuffer(JNIEnv *env, jclass clazz, + jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { return false; @@ -365,7 +431,8 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBindRenderbuffer(JNIEnv extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBufferData(JNIEnv *env, jclass clazz, jint index) { +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBufferData(JNIEnv *env, jclass clazz, + jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { return false; @@ -382,7 +449,9 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlBufferData(JNIEnv *env, extern "C" JNIEXPORT jboolean JNICALL -Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlRenderbufferStorage(JNIEnv *env, jclass clazz, jint index) { +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlRenderbufferStorage(JNIEnv *env, + jclass clazz, + jint index) { gl_hooks_t *hooks = get_gl_hooks(); if (NULL == hooks) { return false; @@ -397,5 +466,177 @@ Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlRenderbufferStorage(JNI return true; } +void get_native_stack(wechat_backtrace::Backtrace *backtrace, char *&stack, bool brief = false) { + std::stringstream full_stack_builder; + std::stringstream brief_stack_builder; + std::string last_so_name; + + auto _callback = [&](wechat_backtrace::FrameDetail it) { + std::string so_name = it.map_name; + + char *demangled_name; + int status = 0; + demangled_name = abi::__cxa_demangle(it.function_name, nullptr, 0, &status); + + full_stack_builder << "#pc " << std::hex << it.rel_pc << " " + << (demangled_name ? demangled_name : "(null)") + << " (" + << it.map_name + << ")" + << std::endl; + + if (last_so_name != it.map_name) { + last_so_name = it.map_name; + brief_stack_builder << it.map_name << ";"; + } + + brief_stack_builder << std::hex << it.rel_pc << ";"; + + if (demangled_name) { + free(demangled_name); + } + }; + + wechat_backtrace::restore_frame_detail(backtrace->frames.get(), backtrace->frame_size, + _callback); + + if (brief) { + auto brief_stack = brief_stack_builder.str(); + stack = new char[brief_stack.size() + 1]; + strcpy(stack, brief_stack.c_str()); + } else { + auto full_stack = full_stack_builder.str(); + stack = new char[full_stack.size() + 1]; + strcpy(stack, full_stack.c_str()); + } +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_dumpNativeStack(JNIEnv *env, jclass clazz, + jlong native_stack_ptr) { + int64_t addr = native_stack_ptr; + auto *ptr = (wechat_backtrace::Backtrace *) addr; + + char *native_stack = nullptr; + get_native_stack(ptr, native_stack); + + jstring ret = env->NewStringUTF(native_stack); + if (native_stack != nullptr) { + delete[] native_stack; + } + return ret; +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_dumpBriefNativeStack(JNIEnv *env, jclass clazz, + jlong native_stack_ptr) { + int64_t addr = native_stack_ptr; + auto *ptr = (wechat_backtrace::Backtrace *) addr; + char *native_stack = nullptr; + get_native_stack(ptr, native_stack, true); + jstring ret = env->NewStringUTF(native_stack); + + if (native_stack != nullptr) { + delete[] native_stack; + } + return ret; +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_releaseNative(JNIEnv *env, jclass clazz, + jlong native_stack_ptr) { + const uintptr_t releaseNativeKey = 0x001; + messages_containers->enqueue_message(releaseNativeKey, [native_stack_ptr] { + int64_t addr = native_stack_ptr; + auto *ptr = (wechat_backtrace::Backtrace *) addr; + delete_backtrace(ptr); + }); +} + +extern "C" +JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_isEglSurfaceAlive( + JNIEnv *env, jclass clazz, jlong egl_surface) { + EGLDisplay display = eglGetCurrentDisplay(); + if (display == nullptr) { + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + } + + EGLSurface eglSurface = reinterpret_cast(egl_surface); + + EGLint width; + eglQuerySurface(display, eglSurface, EGL_WIDTH, &width); + bool result = true; + if (eglGetError() == EGL_BAD_SURFACE) { + result = false; + } + + return result; +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_isEglContextAlive( + JNIEnv *env, jclass clazz, jlong egl_context) { + EGLDisplay display = eglGetCurrentDisplay(); + if (display == nullptr) { + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + } + + const EGLint attrib_config_list[] = { + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_NONE + }; + EGLint num_config = 0; + EGLConfig eglConfig; + if (!eglChooseConfig(display, attrib_config_list, &eglConfig, num_config, &num_config)) { + return false; + } + const EGLint attrib_ctx_list[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + auto origin_context = (EGLContext) egl_context; + EGLContext test_context = eglCreateContext(display, eglConfig, origin_context, attrib_ctx_list); + if (eglGetError() == EGL_BAD_CONTEXT) { + return false; + } + eglDestroyContext(display, test_context); + + return true; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_getResidualQueueSize(JNIEnv *env, + jobject clazz) { + return messages_containers->get_queue_size(); +} + + +extern "C" +JNIEXPORT void JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_updateCurrActivity(JNIEnv *env, jobject thiz, + jstring j_activity_info) { + const char *activity_info = env->GetStringUTFChars(j_activity_info, nullptr); + if (activity_info) { + const size_t info_len = strlen(activity_info); + if (curr_activity_info != nullptr) { + free(curr_activity_info); + } + curr_activity_info = static_cast(malloc(BUF_SIZE)); + const size_t cpy_len = std::min(info_len, BUF_SIZE - 1); + memcpy(curr_activity_info, activity_info, cpy_len); + curr_activity_info[cpy_len] = '\0'; + } else { + strncpy(curr_activity_info, "\tget activity info failed", BUF_SIZE); + } + env->ReleaseStringUTFChars(j_activity_info, activity_info); + env->DeleteLocalRef(j_activity_info); +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_hook_OpenGLHook.h b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_hook_OpenGLHook.h index 882001273..f636d7de2 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_hook_OpenGLHook.h +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_hook_OpenGLHook.h @@ -8,6 +8,27 @@ extern "C" { #endif +JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_isEglContextAlive( + JNIEnv *env, jclass clazz, jlong egl_context); + +extern "C" +JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_isEglSurfaceAlive( + JNIEnv *env, jclass clazz, jlong egl_surface); + +extern "C" +JNIEXPORT void JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_updateCurrActivity(JNIEnv *env, jobject thiz, + jstring activityInfo); + +JNIEXPORT void JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_releaseNative + (JNIEnv *, jclass thiz, jlong); + +JNIEXPORT jstring JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_dumpNativeStack + (JNIEnv *, jclass thiz, jlong); + +JNIEXPORT jstring JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_dumpBriefNativeStack + (JNIEnv *, jclass thiz, jlong); + /* * Class: com_tencent_matrix_openglleak_hook_OpenGLHook * Method: init @@ -137,6 +158,14 @@ extern "C" JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookGlRenderbufferStorage(JNIEnv *env, jclass clazz, jint index); +extern "C" +JNIEXPORT jint JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_getResidualQueueSize(JNIEnv *env, jobject clazz); + +extern "C" +JNIEXPORT jboolean JNICALL +Java_com_tencent_matrix_openglleak_hook_OpenGLHook_hookEgl(JNIEnv *env, jclass clazz); + #ifdef __cplusplus } #endif diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_statistics_resource_ResRecordManager.cpp b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_statistics_resource_ResRecordManager.cpp deleted file mode 100644 index 0204ef7f7..000000000 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_statistics_resource_ResRecordManager.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// -// Created by 邓沛堆 on 2021/1/19. -// - -#include -#include "com_tencent_matrix_openglleak_statistics_resource_ResRecordManager.h" -#include -#include -#include "BacktraceDefine.h" -#include "Backtrace.h" - -void get_native_stack(wechat_backtrace::Backtrace* backtrace, char *&stack) { - std::string caller_so_name; - std::stringstream full_stack_builder; - std::stringstream brief_stack_builder; - std::string last_so_name; - - auto _callback = [&](wechat_backtrace::FrameDetail it) { - std::string so_name = it.map_name; - - char *demangled_name = nullptr; - int status = 0; - demangled_name = abi::__cxa_demangle(it.function_name, nullptr, 0, &status); - - full_stack_builder << " | " - << "#pc " << std::hex << it.rel_pc << " " - << (demangled_name ? demangled_name : "(null)") - << " (" - << it.map_name - << ")" - << std::endl; - - if (last_so_name != it.map_name) { - last_so_name = it.map_name; - brief_stack_builder << it.map_name << ";"; - } - - brief_stack_builder << std::hex << it.rel_pc << ";"; - - if (demangled_name) { - free(demangled_name); - } - - if (caller_so_name.empty()) { // fallback - if (/*so_name.find("com.tencent.mm") == std::string::npos ||*/ - so_name.find("libwechatbacktrace.so") != std::string::npos || - so_name.find("libmatrix-hooks.so") != std::string::npos) { - return; - } - caller_so_name = so_name; - } - }; - - wechat_backtrace::restore_frame_detail(backtrace->frames.get(), backtrace->frame_size, - _callback); - - stack = new char[full_stack_builder.str().size() + 1]; - strcpy(stack, full_stack_builder.str().c_str()); -} - - -extern "C" JNIEXPORT void JNICALL Java_com_tencent_matrix_openglleak_statistics_resource_ResRecordManager_releaseNative - (JNIEnv *env, jclass thiz, jlong jl) { - int64_t addr = jl; - - wechat_backtrace::Backtrace* ptr = (wechat_backtrace::Backtrace*) addr; - delete ptr; -} - -extern "C" JNIEXPORT jstring JNICALL Java_com_tencent_matrix_openglleak_statistics_resource_ResRecordManager_dumpNativeStack - (JNIEnv *env, jclass thiz, jlong jl) { - int64_t addr = jl; - wechat_backtrace::Backtrace* ptr = (wechat_backtrace::Backtrace*)addr; - - char *native_stack = nullptr; - get_native_stack(ptr, native_stack); - - jstring ret = env->NewStringUTF(native_stack); - if(native_stack != nullptr) { - free(native_stack); - } - return ret; -} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_statistics_resource_ResRecordManager.h b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_statistics_resource_ResRecordManager.h deleted file mode 100644 index 837a132af..000000000 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/com_tencent_matrix_openglleak_statistics_resource_ResRecordManager.h +++ /dev/null @@ -1,25 +0,0 @@ -/* DO NOT EDIT THIS FILE - it is machine generated */ -#include -/* Header for class com_tencent_matrix_openglleak_statistics_OpenGLInfo */ - -#ifndef _Included_com_tencent_matrix_openglleak_statistics_resource_ResRecordManager -#define _Included_com_tencent_matrix_openglleak_statistics_resource_ResRecordManager -#ifdef __cplusplus -extern "C" { -#endif - -/* -* Class: com_tencent_mm_openglapihook_OpenGLHook -* Method: init -* Signature: ()Z -*/ -JNIEXPORT void JNICALL Java_com_tencent_matrix_openglleak_statistics_resource_ResRecordManager_releaseNative - (JNIEnv *, jclass thiz, jlong); - -JNIEXPORT jstring JNICALL Java_com_tencent_matrix_openglleak_statistics_resource_ResRecordManager_dumpNativeStack - (JNIEnv *, jclass thiz, jlong); - -#ifdef __cplusplus -} -#endif -#endif diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/my_functions.h b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/my_functions.h index a52d8e65c..ee9742e16 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/my_functions.h +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/my_functions.h @@ -1,6 +1,7 @@ // // Created by 邓沛堆 on 2020-05-28. // +// TODO: refactor #include "type.h" #include @@ -10,13 +11,18 @@ #include #include "BacktraceDefine.h" #include "Backtrace.h" -#include #include +#include "BufferQueue.h" +#include +#include +#include "StackMeta.h" +#include #ifndef OPENGL_API_HOOK_MY_FUNCTIONS_H #define OPENGL_API_HOOK_MY_FUNCTIONS_H using namespace std; +using namespace matrix; #define MEMHOOK_BACKTRACE_MAX_FRAMES MAX_FRAME_SHORT #define RENDER_THREAD_NAME "RenderThread" @@ -38,6 +44,8 @@ static System_GlBind_TYPE system_glBindFramebuffer = NULL; static System_GlBind_TYPE system_glBindRenderbuffer = NULL; static System_GlBufferData system_glBufferData = NULL; static System_GlRenderbufferStorage system_glRenderbufferStorage = NULL; +static System_eglCreateContext system_eglCreateContext = NULL; +static System_eglDestroyContext system_eglDestroyContext = NULL; static JavaVM *m_java_vm; @@ -50,7 +58,6 @@ static jmethodID method_onGlGenFramebuffers; static jmethodID method_onGlDeleteFramebuffers; static jmethodID method_onGlGenRenderbuffers; static jmethodID method_onGlDeleteRenderbuffers; -static jmethodID method_getStack; static jmethodID method_onGetError; static jmethodID method_onGlBindTexture; static jmethodID method_onGlBindBuffer; @@ -60,45 +67,53 @@ static jmethodID method_onGlTexImage2D; static jmethodID method_onGlTexImage3D; static jmethodID method_onGlBufferData; static jmethodID method_onGlRenderbufferStorage; - +static jmethodID method_getThrowable; +static jmethodID method_onEglContextCreate; +static jmethodID method_onEglContextDestroy; const size_t BUF_SIZE = 1024; -const int sample_num = 5; static pthread_once_t g_onceInitTls = PTHREAD_ONCE_INIT; static pthread_key_t g_tlsJavaEnv; +static pthread_key_t g_thread_name_key; static bool is_stacktrace_enabled = true; +static bool is_javastack_enabled = true; + +static matrix::BufferManagement *messages_containers; +static char *curr_activity_info = nullptr; void enable_stacktrace(bool enable) { is_stacktrace_enabled = enable; } -static bool is_javastack_enabled = true; - void enable_javastack(bool enable) { is_javastack_enabled = enable; } -void thread_id_to_string(thread::id thread_id, char *&result) { +void thread_id_to_string(pid_t thread_id, char *&result) { stringstream stream; stream << thread_id; result = new char[stream.str().size() + 1]; strcpy(result, stream.str().c_str()); } -inline void get_thread_name(char *thread_name) { - prctl(PR_GET_NAME, (char *) (thread_name)); +inline void get_thread_name(char *&thread_name) { + char *local_name = static_cast(pthread_getspecific(g_thread_name_key)); + if (local_name == nullptr) { + thread_name = static_cast(malloc(BUF_SIZE)); + prctl(PR_GET_NAME, (char *) (thread_name)); + pthread_setspecific(g_thread_name_key, thread_name); + } else { + thread_name = local_name; + } } -inline bool is_render_thread() { +bool is_render_thread() { bool result = false; - char *thread_name = static_cast(malloc(BUF_SIZE)); + char *thread_name; get_thread_name(thread_name); if (strcmp(RENDER_THREAD_NAME, thread_name) == 0) { result = true; } - if (thread_name != nullptr) { - free(thread_name); - } return result; } @@ -113,61 +128,147 @@ JNIEnv *GET_ENV() { }); }); - if (m_java_vm->AttachCurrentThread(&env, nullptr) == JNI_OK) { + char *thread_name = static_cast(malloc(BUF_SIZE)); + get_thread_name(thread_name); + + JavaVMAttachArgs args{ + .version = JNI_VERSION_1_6, + .name = thread_name, + .group = nullptr + }; + + if (m_java_vm->AttachCurrentThread(&env, &args) == JNI_OK) { pthread_setspecific(g_tlsJavaEnv, reinterpret_cast(1)); } else { env = nullptr; } + + if (thread_name != nullptr) { + free(thread_name); + } } return env; } -bool do_sample() { - int rand_num = rand() % 100; - if(rand_num <= sample_num) { - return true; - } else { - return false; - } +bool is_need_get_java_stack() { + JNIEnv *env; + int ret = m_java_vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + return ret == JNI_OK; } -bool get_java_stacktrace(char *__stack, size_t __size) { +wechat_backtrace::Backtrace *get_native_backtrace() { + wechat_backtrace::Backtrace *backtracePrt = nullptr; + if (is_stacktrace_enabled) { + wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( + MEMHOOK_BACKTRACE_MAX_FRAMES); - if (!__stack) { - return false; + backtracePrt = new wechat_backtrace::Backtrace; + backtracePrt->max_frames = backtrace_zero.max_frames; + backtracePrt->frame_size = backtrace_zero.frame_size; + backtracePrt->frames = backtrace_zero.frames; + + wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, + backtracePrt->frame_size); } + return backtracePrt; +} +int get_java_throwable() { + int throwable = -1; + if (is_javastack_enabled && is_need_get_java_stack()) { + JNIEnv *env = GET_ENV(); + throwable = env->CallStaticIntMethod(class_OpenGLHook, method_getThrowable); + } + return throwable; +} + +void +gen_jni_callback(int alloc_count, GLuint *copy_resource, int throwable, const pid_t tid, + wechat_backtrace::Backtrace *backtracePtr, EGLContext egl_context, + EGLSurface egl_draw_surface, EGLSurface egl_read_surface, + char *activity_info, jmethodID jmethodId) { JNIEnv *env = GET_ENV(); - jstring j_stacktrace = (jstring) env->CallStaticObjectMethod(class_OpenGLHook, method_getStack); + int *result = new int[alloc_count]; + for (int i = 0; i < alloc_count; i++) { + result[i] = *(copy_resource + i); + } + jintArray newArr = env->NewIntArray(alloc_count); - const char *stack = env->GetStringUTFChars(j_stacktrace, NULL); - if (stack) { - const size_t cpy_len = std::min(strlen(stack) + 1, __size - 1); - memcpy(__stack, stack, cpy_len); - __stack[cpy_len] = '\0'; - } else { - strncpy(__stack, "\tget java stacktrace failed", __size); + env->SetIntArrayRegion(newArr, 0, alloc_count, result); + + char *thread_id_c_str; + thread_id_to_string(tid, thread_id_c_str); + jstring j_thread_id = env->NewStringUTF(thread_id_c_str); + + jstring j_activity_info = env->NewStringUTF(activity_info); + + wechat_backtrace::Backtrace *backtrace = deduplicate_backtrace(backtracePtr); + + env->CallStaticVoidMethod( + class_OpenGLHook, + jmethodId, + newArr, + j_thread_id, + (jint) throwable, + (jlong) backtrace, + (jlong) egl_context, + (jlong) egl_draw_surface, + (jlong) egl_read_surface, + j_activity_info); + + delete[] result; + env->DeleteLocalRef(newArr); + env->DeleteLocalRef(j_thread_id); + env->DeleteLocalRef(j_activity_info); + + if (copy_resource != nullptr) { + free(copy_resource); } - env->ReleaseStringUTFChars(j_stacktrace, stack); - env->DeleteLocalRef(j_stacktrace); - return true; + if (thread_id_c_str != nullptr) { + free(thread_id_c_str); + } + if (activity_info != nullptr) { + free(activity_info); + } } -char *get_java_stack() { - char *buf = static_cast(malloc(BUF_SIZE)); - if (buf) { - get_java_stacktrace(buf, BUF_SIZE); +void delete_jni_callback(int delete_count, GLuint *copy_resource, const pid_t tid, + EGLContext egl_context, jmethodID jmethodId) { + JNIEnv *env = GET_ENV(); + + int *result = new int[delete_count]; + for (int i = 0; i < delete_count; i++) { + result[i] = *(copy_resource + i); } - return buf; -} + jintArray newArr = env->NewIntArray(delete_count); + env->SetIntArrayRegion(newArr, 0, delete_count, result); + + char *thread_id_c_str; + thread_id_to_string(tid, thread_id_c_str); + jstring j_thread_id = env->NewStringUTF(thread_id_c_str); -void get_thread_id_string(char *&result) { - thread::id thread_id = this_thread::get_id(); - thread_id_to_string(thread_id, result); + env->CallStaticVoidMethod(class_OpenGLHook, + jmethodId, + newArr, + j_thread_id, + (jlong) egl_context); + + delete[] result; + env->DeleteLocalRef(newArr); + env->DeleteLocalRef(j_thread_id); + + if (copy_resource != nullptr) { + free(copy_resource); + } + + if (thread_id_c_str != nullptr) { + free(thread_id_c_str); + } } + GL_APICALL void GL_APIENTRY my_glGenTextures(GLsizei n, GLuint *textures) { if (NULL != system_glGenTextures) { system_glGenTextures(n, textures); @@ -176,58 +277,37 @@ GL_APICALL void GL_APIENTRY my_glGenTextures(GLsizei n, GLuint *textures) { return; } - JNIEnv *env = GET_ENV(); + GLuint *copy_textures = new GLuint[n]; + memcpy(copy_textures, textures, n * sizeof(GLuint)); - int *result = new int[n]; - for (int i = 0; i < n; i++) { - result[i] = *(textures + i); - } - jintArray newArr = env->NewIntArray(n); - env->SetIntArrayRegion(newArr, 0, n, result); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); - char *thread_id = nullptr; - get_thread_id_string(thread_id); - jstring j_thread_id = env->NewStringUTF(thread_id); + int throwable = get_java_throwable(); - wechat_backtrace::Backtrace *backtracePrt = 0; - if (is_stacktrace_enabled) { - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - MEMHOOK_BACKTRACE_MAX_FRAMES); + pid_t tid = pthread_gettid_np(pthread_self()); - backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + EGLContext egl_context = eglGetCurrentContext(); - wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, - backtracePrt->frame_size); - } + EGLSurface egl_draw_surface = eglGetCurrentSurface(EGL_DRAW); + EGLSurface egl_read_surface = eglGetCurrentSurface(EGL_READ); - jstring java_stack; - char *javaStack = nullptr; - if (is_javastack_enabled) { - javaStack = get_java_stack(); - java_stack = env->NewStringUTF(javaStack); + char *activity_info = static_cast(malloc(BUF_SIZE)); + if (curr_activity_info != nullptr) { + strcpy(activity_info, curr_activity_info); } else { - java_stack = env->NewStringUTF(""); + strcpy(activity_info, "null"); } - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlGenTextures, newArr, j_thread_id, - java_stack, (int64_t) backtracePrt); - - delete[] result; - if (is_javastack_enabled && javaStack != nullptr) { - free(javaStack); - } + messages_containers-> + enqueue_message((uintptr_t) egl_context, + [n, copy_textures, throwable, tid, backtracePrt, egl_context, egl_read_surface, egl_draw_surface, activity_info]() { - env->DeleteLocalRef(newArr); - env->DeleteLocalRef(j_thread_id); - env->DeleteLocalRef(java_stack); + gen_jni_callback(n, copy_textures, throwable, tid, + backtracePrt, egl_context, egl_draw_surface, + egl_read_surface, + activity_info, method_onGlGenTextures); - if (thread_id != nullptr) { - delete[] thread_id; - thread_id = nullptr; - } + }); } } @@ -239,27 +319,21 @@ GL_APICALL void GL_APIENTRY my_glDeleteTextures(GLsizei n, GLuint *textures) { return; } - JNIEnv *env = GET_ENV(); + GLuint *copy_textures = new GLuint[n]; + memcpy(copy_textures, textures, n * sizeof(GLuint)); - int *result = new int[n]; - for (int i = 0; i < n; i++) { - result[i] = *(textures + i); - } - jintArray newArr = env->NewIntArray(n); - env->SetIntArrayRegion(newArr, 0, n, result); - - char *thread_id = nullptr; - get_thread_id_string(thread_id); - jstring j_thread_id = env->NewStringUTF(thread_id); - - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlDeleteTextures, newArr, j_thread_id); - delete[] result; - env->DeleteLocalRef(newArr); - env->DeleteLocalRef(j_thread_id); - if (thread_id != nullptr) { - delete[] thread_id; - thread_id = nullptr; - } + pid_t tid = pthread_gettid_np(pthread_self()); + + EGLContext egl_context = eglGetCurrentContext(); + + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [n, copy_textures, tid, egl_context] { + + delete_jni_callback(n, copy_textures, tid, egl_context, + method_onGlDeleteTextures); + + }); } } @@ -271,57 +345,37 @@ GL_APICALL void GL_APIENTRY my_glGenBuffers(GLsizei n, GLuint *buffers) { return; } - JNIEnv *env = GET_ENV(); + GLuint *copy_buffers = new GLuint[n]; + memcpy(copy_buffers, buffers, n * sizeof(GLuint)); - int *result = new int[n]; - for (int i = 0; i < n; i++) { - result[i] = *(buffers + i); - } - jintArray newArr = env->NewIntArray(n); - env->SetIntArrayRegion(newArr, 0, n, result); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); - char *thread_id = nullptr; - get_thread_id_string(thread_id); - jstring j_thread_id = env->NewStringUTF(thread_id); + int throwable = get_java_throwable(); - wechat_backtrace::Backtrace *backtracePrt = 0; - if (is_stacktrace_enabled) { - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - MEMHOOK_BACKTRACE_MAX_FRAMES); + pid_t tid = pthread_gettid_np(pthread_self()); - backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + EGLContext egl_context = eglGetCurrentContext(); - wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, - backtracePrt->frame_size); - } + EGLSurface egl_draw_surface = eglGetCurrentSurface(EGL_DRAW); + EGLSurface egl_read_surface = eglGetCurrentSurface(EGL_READ); - jstring java_stack; - char *javaStack = nullptr; - if (is_javastack_enabled) { - javaStack = get_java_stack(); - java_stack = env->NewStringUTF(javaStack); + char *activity_info = static_cast(malloc(BUF_SIZE)); + if (curr_activity_info != nullptr) { + strcpy(activity_info, curr_activity_info); } else { - java_stack = env->NewStringUTF(""); + strcpy(activity_info, "null"); } + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [n, copy_buffers, throwable, tid, backtracePrt, egl_context, egl_draw_surface, egl_read_surface, activity_info]() { - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlGenBuffers, newArr, j_thread_id, - java_stack, (int64_t) backtracePrt); + gen_jni_callback(n, copy_buffers, throwable, tid, + backtracePrt, egl_context, egl_draw_surface, + egl_read_surface, + activity_info, method_onGlGenBuffers); - delete[] result; - if (is_javastack_enabled && javaStack != nullptr) { - free(javaStack); - } + }); - env->DeleteLocalRef(newArr); - env->DeleteLocalRef(j_thread_id); - env->DeleteLocalRef(java_stack); - if (thread_id != nullptr) { - delete[] thread_id; - thread_id = nullptr; - } } } @@ -333,27 +387,21 @@ GL_APICALL void GL_APIENTRY my_glDeleteBuffers(GLsizei n, GLuint *buffers) { return; } - JNIEnv *env = GET_ENV(); + GLuint *copy_buffers = new GLuint[n]; + memcpy(copy_buffers, buffers, n * sizeof(GLuint)); - int *result = new int[n]; - for (int i = 0; i < n; i++) { - result[i] = *(buffers + i); - } - jintArray newArr = env->NewIntArray(n); - env->SetIntArrayRegion(newArr, 0, n, result); - - char *thread_id = nullptr; - get_thread_id_string(thread_id); - jstring j_thread_id = env->NewStringUTF(thread_id); - - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlDeleteBuffers, newArr, j_thread_id); - delete[] result; - env->DeleteLocalRef(newArr); - env->DeleteLocalRef(j_thread_id); - if (thread_id != nullptr) { - delete[] thread_id; - thread_id = nullptr; - } + pid_t tid = pthread_gettid_np(pthread_self()); + + EGLContext egl_context = eglGetCurrentContext(); + + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [n, copy_buffers, tid, egl_context]() { + + delete_jni_callback(n, copy_buffers, tid, egl_context, + method_onGlDeleteBuffers); + + }); } } @@ -364,58 +412,38 @@ GL_APICALL void GL_APIENTRY my_glGenFramebuffers(GLsizei n, GLuint *buffers) { if (is_render_thread()) { return; } + GLuint *copy_buffers = new GLuint[n]; + memcpy(copy_buffers, buffers, n * sizeof(GLuint)); - JNIEnv *env = GET_ENV(); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); - int *result = new int[n]; - for (int i = 0; i < n; i++) { - result[i] = *(buffers + i); - } - jintArray newArr = env->NewIntArray(n); - env->SetIntArrayRegion(newArr, 0, n, result); + int throwable = get_java_throwable(); - char *thread_id = nullptr; - get_thread_id_string(thread_id); - jstring j_thread_id = env->NewStringUTF(thread_id); + pid_t tid = pthread_gettid_np(pthread_self()); - wechat_backtrace::Backtrace *backtracePrt = 0; - if (is_stacktrace_enabled) { - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - MEMHOOK_BACKTRACE_MAX_FRAMES); + EGLContext egl_context = eglGetCurrentContext(); - backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + EGLSurface egl_draw_surface = eglGetCurrentSurface(EGL_DRAW); + EGLSurface egl_read_surface = eglGetCurrentSurface(EGL_READ); - wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, - backtracePrt->frame_size); - } - - jstring java_stack; - char *javaStack = nullptr; - if (is_javastack_enabled) { - javaStack = get_java_stack(); - java_stack = env->NewStringUTF(javaStack); + char *activity_info = static_cast(malloc(BUF_SIZE)); + if (curr_activity_info != nullptr) { + strcpy(activity_info, curr_activity_info); } else { - java_stack = env->NewStringUTF(""); + strcpy(activity_info, "null"); } - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlGenFramebuffers, newArr, - j_thread_id, java_stack, (int64_t) backtracePrt); + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [n, copy_buffers, throwable, tid, backtracePrt, egl_context, egl_draw_surface, egl_read_surface, activity_info]() { - delete[] result; - if (is_javastack_enabled && javaStack != nullptr) { - free(javaStack); - } + gen_jni_callback(n, copy_buffers, throwable, tid, + backtracePrt, egl_context, egl_draw_surface, + egl_read_surface, + activity_info, method_onGlGenFramebuffers); + + }); - env->DeleteLocalRef(newArr); - env->DeleteLocalRef(j_thread_id); - env->DeleteLocalRef(java_stack); - if (thread_id != nullptr) { - delete[] thread_id; - thread_id = nullptr; - } } } @@ -427,28 +455,20 @@ GL_APICALL void GL_APIENTRY my_glDeleteFramebuffers(GLsizei n, GLuint *buffers) return; } - JNIEnv *env = GET_ENV(); + GLuint *copy_buffers = new GLuint[n]; + memcpy(copy_buffers, buffers, n * sizeof(GLuint)); + + pid_t tid = pthread_gettid_np(pthread_self()); + + EGLContext egl_context = eglGetCurrentContext(); + + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [n, copy_buffers, tid, egl_context]() { + delete_jni_callback(n, copy_buffers, tid, egl_context, + method_onGlDeleteFramebuffers); + }); - int *result = new int[n]; - for (int i = 0; i < n; i++) { - result[i] = *(buffers + i); - } - jintArray newArr = env->NewIntArray(n); - env->SetIntArrayRegion(newArr, 0, n, result); - - char *thread_id = nullptr; - get_thread_id_string(thread_id); - jstring j_thread_id = env->NewStringUTF(thread_id); - - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlDeleteFramebuffers, newArr, - j_thread_id); - delete[] result; - env->DeleteLocalRef(newArr); - env->DeleteLocalRef(j_thread_id); - if (thread_id != nullptr) { - delete[] thread_id; - thread_id = nullptr; - } } } @@ -456,61 +476,41 @@ GL_APICALL void GL_APIENTRY my_glGenRenderbuffers(GLsizei n, GLuint *buffers) { if (NULL != system_glGenRenderbuffers) { system_glGenRenderbuffers(n, buffers); + GLuint *copy_buffers = new GLuint[n]; + memcpy(copy_buffers, buffers, n * sizeof(GLuint)); + if (is_render_thread()) { return; } - JNIEnv *env = GET_ENV(); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); - int *result = new int[n]; - for (int i = 0; i < n; i++) { - result[i] = *(buffers + i); - } - jintArray newArr = env->NewIntArray(n); - env->SetIntArrayRegion(newArr, 0, n, result); - - char *thread_id = nullptr; - get_thread_id_string(thread_id); - jstring j_thread_id = env->NewStringUTF(thread_id); + int throwable = get_java_throwable(); - wechat_backtrace::Backtrace *backtracePrt = 0; - if (is_stacktrace_enabled) { - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - MEMHOOK_BACKTRACE_MAX_FRAMES); + pid_t tid = pthread_gettid_np(pthread_self()); - backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + EGLContext egl_context = eglGetCurrentContext(); - wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, - backtracePrt->frame_size); - } + EGLSurface egl_draw_surface = eglGetCurrentSurface(EGL_DRAW); + EGLSurface egl_read_surface = eglGetCurrentSurface(EGL_READ); - jstring java_stack; - char *javaStack = nullptr; - if (is_javastack_enabled) { - javaStack = get_java_stack(); - java_stack = env->NewStringUTF(javaStack); + char *activity_info = static_cast(malloc(BUF_SIZE)); + if (curr_activity_info != nullptr) { + strcpy(activity_info, curr_activity_info); } else { - java_stack = env->NewStringUTF(""); + strcpy(activity_info, "null"); } - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlGenRenderbuffers, newArr, - j_thread_id, java_stack, (int64_t) backtracePrt); + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [n, copy_buffers, throwable, tid, backtracePrt, egl_context, egl_draw_surface, egl_read_surface, activity_info]() { - delete[] result; - if (is_javastack_enabled && javaStack != nullptr) { - free(javaStack); - } + gen_jni_callback(n, copy_buffers, throwable, tid, + backtracePrt, egl_context, egl_draw_surface, + egl_read_surface, + activity_info, method_onGlGenRenderbuffers); - env->DeleteLocalRef(newArr); - env->DeleteLocalRef(j_thread_id); - env->DeleteLocalRef(java_stack); - if (thread_id != nullptr) { - delete[] thread_id; - thread_id = nullptr; - } + }); } } @@ -522,35 +522,27 @@ GL_APICALL void GL_APIENTRY my_glDeleteRenderbuffers(GLsizei n, GLuint *buffers) return; } - JNIEnv *env = GET_ENV(); + GLuint *copy_buffers = new GLuint[n]; + memcpy(copy_buffers, buffers, n * sizeof(GLuint)); - int *result = new int[n]; - for (int i = 0; i < n; i++) { - result[i] = *(buffers + i); - } - jintArray newArr = env->NewIntArray(n); - env->SetIntArrayRegion(newArr, 0, n, result); - - char *thread_id = nullptr; - get_thread_id_string(thread_id); - jstring j_thread_id = env->NewStringUTF(thread_id); - - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlDeleteRenderbuffers, newArr, - j_thread_id); - delete[] result; - env->DeleteLocalRef(newArr); - env->DeleteLocalRef(j_thread_id); - if (thread_id != nullptr) { - delete[] thread_id; - thread_id = nullptr; - } + pid_t tid = pthread_gettid_np(pthread_self()); + + EGLContext egl_context = eglGetCurrentContext(); + + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [n, copy_buffers, tid, egl_context]() { + + delete_jni_callback(n, copy_buffers, tid, egl_context, + method_onGlDeleteRenderbuffers); + + }); } } GL_APICALL int GL_APIENTRY my_glGetError() { if (NULL != system_glGetError) { int result = system_glGetError(); - JNIEnv *env = GET_ENV(); jint jresult = result; @@ -572,41 +564,38 @@ my_glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, if (is_render_thread()) { return; } - int pixel = Utils::getSizeOfPerPixel(internalformat, format, type); - long size = width * height * pixel; - JNIEnv *env = GET_ENV(); - wechat_backtrace::Backtrace *backtracePrt = 0; - if (is_stacktrace_enabled) { - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - MEMHOOK_BACKTRACE_MAX_FRAMES); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); - backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + int throwable = get_java_throwable(); - wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, - backtracePrt->frame_size); - } + EGLContext egl_context = eglGetCurrentContext(); - jstring java_stack; - char *javaStack = nullptr; - if (is_javastack_enabled && do_sample()) { - javaStack = get_java_stack(); - java_stack = env->NewStringUTF(javaStack); - } else { - java_stack = env->NewStringUTF(""); - } + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [target, level, internalformat, width, height, border, format, type, throwable, backtracePrt, egl_context]() { + JNIEnv *env = GET_ENV(); - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlTexImage2D, target, level, - internalformat, width, height, border, format, type, (jlong) size, - java_stack, (jlong) backtracePrt); + int pixel = Utils::getSizeOfPerPixel(internalformat, format, + type); + long size = width * height * pixel; + + wechat_backtrace::Backtrace *backtrace = deduplicate_backtrace( + backtracePrt); + + env->CallStaticVoidMethod(class_OpenGLHook, + method_onGlTexImage2D, + target, + level, + internalformat, width, + height, border, format, + type, + (jlong) size, + (jint) throwable, + (jlong) backtrace, + (jlong) egl_context); + }); - if (is_javastack_enabled && javaStack != nullptr) { - free(javaStack); - } - env->DeleteLocalRef(java_stack); } } @@ -620,42 +609,42 @@ my_glTexImage3D(GLenum target, GLint level, GLint internalformat, GLsizei width, if (is_render_thread()) { return; } - int pixel = Utils::getSizeOfPerPixel(internalformat, format, type); - long size = width * height * depth * pixel; - JNIEnv *env = GET_ENV(); - wechat_backtrace::Backtrace *backtracePrt = 0; - if (is_stacktrace_enabled) { - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - MEMHOOK_BACKTRACE_MAX_FRAMES); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); - backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + int throwable = get_java_throwable(); - wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, - backtracePrt->frame_size); - } + EGLContext egl_context = eglGetCurrentContext(); - jstring java_stack; - char *javaStack = nullptr; - if (is_javastack_enabled && do_sample()) { - javaStack = get_java_stack(); - java_stack = env->NewStringUTF(javaStack); - } else { - java_stack = env->NewStringUTF(""); - } + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [target, level, internalformat, width, height, depth, border, format, type, backtracePrt, throwable, egl_context]() { - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlTexImage3D, target, level, - internalformat, width, height, depth, border, format, type, - (jlong) size, - java_stack, (jlong) backtracePrt); + JNIEnv *env = GET_ENV(); - if (is_javastack_enabled && javaStack != nullptr) { - free(javaStack); - } - env->DeleteGlobalRef(java_stack); + int pixel = Utils::getSizeOfPerPixel(internalformat, + format, + type); + + long size = width * height * depth * pixel; + + wechat_backtrace::Backtrace *backtrace = deduplicate_backtrace( + backtracePrt); + + env->CallStaticVoidMethod(class_OpenGLHook, + method_onGlTexImage3D, + target, + level, + internalformat, width, height, + depth, border, + format, + type, + (jlong) size, + (jint) throwable, + (jlong) backtrace, + (jlong) egl_context); + + }); } } @@ -667,8 +656,18 @@ GL_APICALL void GL_APIENTRY my_glBindTexture(GLenum target, GLuint resourceId) { if (is_render_thread()) { return; } - JNIEnv *env = GET_ENV(); - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBindTexture, target, resourceId); + + EGLContext egl_context = eglGetCurrentContext(); + + messages_containers + ->enqueue_message((uintptr_t) egl_context, [target, resourceId, egl_context]() { + JNIEnv *env = GET_ENV(); + env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBindTexture, target, + (jint) resourceId, + (jlong) egl_context); + + }); + } } @@ -679,9 +678,18 @@ GL_APICALL void GL_APIENTRY my_glBindBuffer(GLenum target, GLuint resourceId) { if (is_render_thread()) { return; } - JNIEnv *env = GET_ENV(); - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBindBuffer, target, resourceId); + EGLContext egl_context = eglGetCurrentContext(); + + messages_containers + ->enqueue_message((uintptr_t) egl_context, [target, resourceId, egl_context]() { + JNIEnv *env = GET_ENV(); + + env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBindBuffer, target, + (jint) resourceId, + (jlong) egl_context); + + }); } } @@ -692,9 +700,16 @@ GL_APICALL void GL_APIENTRY my_glBindFramebuffer(GLenum target, GLuint resourceI if (is_render_thread()) { return; } - JNIEnv *env = GET_ENV(); - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBindFramebuffer, target, resourceId); + EGLContext egl_context = eglGetCurrentContext(); + + messages_containers + ->enqueue_message((uintptr_t) egl_context, [target, resourceId, egl_context]() { + JNIEnv *env = GET_ENV(); + env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBindFramebuffer, + target, + resourceId, (jlong) egl_context); + }); } } @@ -705,9 +720,15 @@ GL_APICALL void GL_APIENTRY my_glBindRenderbuffer(GLenum target, GLuint resource if (is_render_thread()) { return; } - JNIEnv *env = GET_ENV(); - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBindRenderbuffer, target, - resourceId); + EGLContext egl_context = eglGetCurrentContext(); + + messages_containers + ->enqueue_message((uintptr_t) egl_context, [target, resourceId, egl_context]() { + JNIEnv *env = GET_ENV(); + env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBindRenderbuffer, + target, + (jint) resourceId, (jlong) egl_context); + }); } } @@ -719,39 +740,31 @@ my_glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage if (is_render_thread()) { return; } - JNIEnv *env = GET_ENV(); - wechat_backtrace::Backtrace *backtracePrt = 0; - if (is_stacktrace_enabled) { - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - MEMHOOK_BACKTRACE_MAX_FRAMES); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); - backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + int throwable = get_java_throwable(); - wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, - backtracePrt->frame_size); - } + EGLContext egl_context = eglGetCurrentContext(); - jstring java_stack; - char *javaStack = nullptr; - if (is_javastack_enabled && do_sample()) { - javaStack = get_java_stack(); - java_stack = env->NewStringUTF(javaStack); - } else { - java_stack = env->NewStringUTF(""); - } + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [target, size, usage, throwable, backtracePrt, egl_context]() { - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlBufferData, target, - usage, (jlong) size, - java_stack, (jlong) backtracePrt); + JNIEnv *env = GET_ENV(); - if (is_javastack_enabled && javaStack != nullptr) { - free(javaStack); - } - env->DeleteLocalRef(java_stack); + wechat_backtrace::Backtrace *backtrace = deduplicate_backtrace( + backtracePrt); + + env->CallStaticVoidMethod(class_OpenGLHook, + method_onGlBufferData, + target, + usage, (jlong) size, + (jint) throwable, + (jlong) backtrace, + (jlong) egl_context); + + }); } } @@ -763,42 +776,141 @@ my_glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GL if (is_render_thread()) { return; } - JNIEnv *env = GET_ENV(); - wechat_backtrace::Backtrace *backtracePrt = 0; - if (is_stacktrace_enabled) { - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - MEMHOOK_BACKTRACE_MAX_FRAMES); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); + + int throwable = get_java_throwable(); - backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + EGLContext egl_context = eglGetCurrentContext(); - wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, - backtracePrt->frame_size); + messages_containers + ->enqueue_message((uintptr_t) egl_context, + [target, internalformat, width, height, backtracePrt, throwable, egl_context]() { + + JNIEnv *env = GET_ENV(); + + long size = Utils::getRenderbufferSizeByFormula( + internalformat, width, height); + + wechat_backtrace::Backtrace *backtrace = deduplicate_backtrace( + backtracePrt); + + env->CallStaticVoidMethod(class_OpenGLHook, + method_onGlRenderbufferStorage, + target, + width, height, + internalformat, + (jlong) size, + (jint) throwable, + (jlong) backtrace, + (jlong) egl_context); + }); + + } +} + +EGLAPI EGLContext EGLAPIENTRY +my_egl_context_create(EGLDisplay dpy, EGLConfig config, EGLContext share_context, + const EGLint *attrib_list) { + EGLContext ret = NULL; + if (NULL != system_eglCreateContext) { + ret = system_eglCreateContext(dpy, config, share_context, attrib_list); + + if (is_render_thread()) { + return ret; } - jstring java_stack; - char *javaStack = nullptr; - if (is_javastack_enabled && do_sample()) { - javaStack = get_java_stack(); - java_stack = env->NewStringUTF(javaStack); + wechat_backtrace::Backtrace *backtracePrt = get_native_backtrace(); + + int throwable = get_java_throwable(); + + char *thread_id_c_str; + thread_id_to_string(pthread_gettid_np(pthread_self()), thread_id_c_str); + + char *activity_info = static_cast(malloc(BUF_SIZE)); + if (curr_activity_info != nullptr) { + strcpy(activity_info, curr_activity_info); } else { - java_stack = env->NewStringUTF(""); + strcpy(activity_info, "null"); } - long size = Utils::getRenderbufferSizeByFormula(internalformat, width, height); - env->CallStaticVoidMethod(class_OpenGLHook, method_onGlRenderbufferStorage, target, - width, height, internalformat, (jlong) size, java_stack, - (jlong) backtracePrt); + messages_containers + ->enqueue_message((uintptr_t) ret, + [backtracePrt, throwable, thread_id_c_str, ret, share_context, activity_info]() { - if (is_javastack_enabled && javaStack != nullptr) { - free(javaStack); - } - env->DeleteLocalRef(java_stack); + JNIEnv *env = GET_ENV(); + + wechat_backtrace::Backtrace *backtrace = deduplicate_backtrace( + backtracePrt); + + jstring j_activity_info = env->NewStringUTF(activity_info); + jstring j_thread_id = env->NewStringUTF(thread_id_c_str); + + env->CallStaticVoidMethod(class_OpenGLHook, + method_onEglContextCreate, + j_thread_id, + (jint) throwable, + (jlong) backtrace, + (jlong) ret, + (jlong) share_context, + j_activity_info); + + env->DeleteLocalRef(j_activity_info); + env->DeleteLocalRef(j_thread_id); + + if (activity_info != nullptr) { + free(activity_info); + } + + if (thread_id_c_str) { + free(thread_id_c_str); + } + }); } + return ret; } +/** + * EGL_BAD_DISPLAY is generated if display is not an EGL display connection. + * EGL_NOT_INITIALIZED is generated if display has not been initialized. + * EGL_BAD_CONTEXT is generated if context is not an EGL rendering context. + * + * @param dpy + * @param ctx + * @return EGL_FALSE is returned if destruction of the context fails, EGL_TRUE otherwise. + */ +EGLAPI EGLBoolean EGLAPIENTRY my_egl_context_destroy(EGLDisplay dpy, EGLContext ctx) { + EGLBoolean ret = EGL_FALSE; + if (NULL != system_eglDestroyContext) { + ret = system_eglDestroyContext(dpy, ctx); + + if (is_render_thread()) { + return ret; + } + + char *thread_id_c_str; + thread_id_to_string(pthread_gettid_np(pthread_self()), thread_id_c_str); + + messages_containers + ->enqueue_message((uintptr_t) ctx, + [thread_id_c_str, ctx, ret]() { + + JNIEnv *env = GET_ENV(); + jstring j_thread_id = env->NewStringUTF(thread_id_c_str); + + env->CallStaticVoidMethod(class_OpenGLHook, + method_onEglContextDestroy, + j_thread_id, + (jlong) ctx, + ret); + + env->DeleteLocalRef(j_thread_id); + if (thread_id_c_str) { + free(thread_id_c_str); + } + }); + } + return ret; +} #endif //OPENGL_API_HOOK_MY_FUNCTIONS_H \ No newline at end of file diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/splay_map.h b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/splay_map.h new file mode 100644 index 000000000..f84129621 --- /dev/null +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/splay_map.h @@ -0,0 +1,402 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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. + */ + +#ifndef splay_map_h +#define splay_map_h + +#include +#include +#include + +#include "buffer_source.h" + +#define SPLAY_CHECK(assertion) \ + if (__builtin_expect(!(assertion), false)) { \ + abort(); \ + } + + +template class splay_map { +private: + typedef uint32_t node_ptr; + + struct node { + TKey key; + node_ptr left, right; + }; + + struct tree_info { + node_ptr root_ptr; + node_ptr free_ptr; + uint32_t b_size; // buffer size + uint32_t t_size; // tree size + }; + + tree_info *t_info = NULL; + tree_info t_empty = { 0 }; + node *k_buff = NULL; // node buffer + node_ptr last_ptr = 0; // use for exist() and find() + TVal *v_buff = NULL; + uint32_t i_size = 0; // increment/init size + + buffer_source *key_buffer_source; + buffer_source *val_buffer_source; + + buffer_source *self_key_buffer_source_ = nullptr; + buffer_source *self_val_buffer_source_ = nullptr; + +#define get_val(ptr) v_buff[ptr] + +#define get_node(ptr) k_buff[ptr] + +#define get_node_lc(ptr) k_buff[ptr].left + +#define get_node_rc(ptr) k_buff[ptr].right + +#define get_node_key(ptr) k_buff[ptr].key + +#ifdef ENABLE_SPLAY_CHECK +#define check_overflow(ptr) SPLAY_CHECK(ptr < t_info->b_size) +#define check(assertion) SPLAY_CHECK(assertion) +#else +#define check_overflow(ptr) +#define check(assertion) +#endif + + bool splay(TKey key, node_ptr &root) { + /* Simple top down splay, not requiring i to be in the tree t. */ + /* What it does is described above. */ + if (root == 0) { + return false; + } + + node_ptr l = 0, r = 0, t = root; + bool ret = false; + + for (;;) { + check_overflow(t); + TKey tkey = get_node_key(t); + if (tkey > key) { + node_ptr tlc = get_node_lc(t); + if (tlc == 0) { + break; + } + check_overflow(tlc); + if (get_node_key(tlc) > key) { + get_node_lc(t) = get_node_rc(tlc); /* rotate right */ + get_node_rc(tlc) = t; + t = tlc; + if (get_node_lc(t) == 0) { + break; + } + } + check_overflow(r); + get_node_lc(r) = t; /* link right */ + r = t; + t = get_node_lc(t); + } else if (tkey < key) { + node_ptr trc = get_node_rc(t); + if (trc == 0) { + break; + } + check_overflow(trc); + if (get_node_key(trc) < key) { + get_node_rc(t) = get_node_lc(trc); /* rotate left */ + get_node_lc(trc) = t; + t = trc; + if (get_node_rc(t) == 0) { + break; + } + } + check_overflow(l); + get_node_rc(l) = t; /* link left */ + l = t; + t = get_node_rc(t); + } else { + ret = true; + break; + } + } + check_overflow(t); + check_overflow(r); + check_overflow(l); + get_node_rc(l) = get_node_lc(t); /* assemble */ + get_node_lc(r) = get_node_rc(t); + get_node_lc(t) = get_node_rc(0); + get_node_rc(t) = get_node_lc(0); + get_node_lc(0) = 0; + get_node_rc(0) = 0; + root = t; + return ret; + } + + node_ptr inter_find(TKey key) { + if (splay(key, t_info->root_ptr)) { + return t_info->root_ptr; + } else { + return 0; + } + } + + void inter_enumerate(node_ptr root, std::function callback) { + if (root == 0) { + return; + } + + std::stack s; + uint32_t count = 0; + while (root != 0 || !s.empty()) { + while (root != 0) { + s.push(root); + if (s.size() > t_info->t_size) { + return; + } + check_overflow(root); + root = get_node_lc(root); + } + + if (!s.empty()) { + root = s.top(); + check_overflow(root); + callback(get_node_key(root), get_val(root)); + if (++count > t_info->t_size) { + return; + } + s.pop(); + root = get_node_rc(root); + } + } + } + + void free_node(node_ptr ptr) { + if (ptr == 0) { + return; + } + check_overflow(ptr); + get_node(ptr).left = t_info->free_ptr; // ticky + t_info->free_ptr = ptr; + } + + node_ptr next_free_node() { + node_ptr next_ptr = t_info->free_ptr; + if (next_ptr == 0) { + next_ptr = t_info->t_size; + } else { + check_overflow(t_info->free_ptr); + t_info->free_ptr = get_node(t_info->free_ptr).left; // ticky + } + return next_ptr; + } + + bool reallocate_memory(bool is_init) { + if (is_init) { + // key buffer + uint32_t malloc_size = sizeof(tree_info) + i_size * sizeof(node); + void *new_buff = key_buffer_source->realloc(malloc_size); + if (new_buff) { + memset(new_buff, 0, malloc_size); + t_info = (tree_info *)new_buff; + k_buff = (node *)((char *)new_buff + sizeof(tree_info)); + } else { + check(false); + return false; + } + + // val buffer + malloc_size = i_size * sizeof(TVal); + new_buff = val_buffer_source->realloc(malloc_size); + if (new_buff) { + memset(new_buff, 0, malloc_size); + t_info->b_size = i_size; + v_buff = (TVal *)new_buff; + return true; + } else { + check(false); + return false; + } + } else { + t_empty = *t_info; // save t_info temporarily, t_info ptr will be invalid after reallocate new memory from file + // key buffer + uint32_t malloc_size = sizeof(tree_info) + (t_empty.b_size + i_size) * sizeof(node); + void *new_buff = key_buffer_source->realloc(malloc_size); + if (new_buff) { + memset((char *)new_buff + sizeof(tree_info) + t_empty.b_size * sizeof(node), 0, i_size * sizeof(node)); + t_info = (tree_info *)new_buff; + *t_info = t_empty; + k_buff = (node *)((char *)new_buff + sizeof(tree_info)); + } else { + check(false); + return false; + } + + // val buffer + malloc_size = (t_empty.b_size + i_size) * sizeof(TVal); + new_buff = val_buffer_source->realloc(malloc_size); + if (new_buff) { + memset((char *)new_buff + t_empty.b_size * sizeof(TVal), 0, i_size * sizeof(TVal)); + t_info->b_size = t_info->b_size + i_size; + v_buff = (TVal *)new_buff; + return true; + } else { + check(false); + return false; + } + } + } + + void _init(uint32_t _is, buffer_source *_kb, buffer_source *_vb) { + key_buffer_source = _kb; + val_buffer_source = _vb; + i_size = (_is == 0 ? 1024 : _is); + + void *data = key_buffer_source->buffer(); + size_t len = key_buffer_source->buffer_size(); + + if (data != NULL && len > sizeof(tree_info)) { + t_info = (tree_info *)data; + // check valid + if (t_info->b_size * sizeof(node) > len - sizeof(tree_info) || t_info->root_ptr >= t_info->b_size || t_info->free_ptr >= t_info->b_size + || t_info->t_size >= t_info->b_size || val_buffer_source->buffer() == NULL + || val_buffer_source->buffer_size() < t_info->b_size * sizeof(TVal)) { + reallocate_memory(true); + } else { + k_buff = (node *)((char *)data + sizeof(tree_info)); + v_buff = (TVal *)val_buffer_source->buffer(); + } + } else { + reallocate_memory(true); + } + } + +public: + splay_map(uint32_t _is) { + self_key_buffer_source_ = new buffer_source_memory; + self_val_buffer_source_ = new buffer_source_memory; + _init(_is, self_key_buffer_source_, self_val_buffer_source_); + } + +// splay_map(uint32_t _is, buffer_source *_kb, buffer_source *_vb) { +// _init(_is, _kb, _vb); +// } + + ~splay_map() { + if (self_key_buffer_source_) delete self_key_buffer_source_; + if (self_val_buffer_source_) delete self_val_buffer_source_; + } + + TVal *insert(TKey key, const TVal &val) { + if (t_info->t_size + 1 == t_info->b_size && !reallocate_memory(false)) { + return nullptr; // malloc fail + } + + if (t_info->root_ptr == 0) { + t_info->t_size = 1; + + node_ptr n = next_free_node(); + check_overflow(n); + get_node_key(n) = key; + get_node_lc(n) = 0; + get_node_rc(n) = 0; + get_val(n) = val; + t_info->root_ptr = n; + return &get_val(n); + } + + if (splay(key, t_info->root_ptr)) { + /* We get here if it's already in the tree */ + /* Don't add it again */ + check_overflow(t_info->root_ptr); + get_val(t_info->root_ptr) = val; + return &get_val(t_info->root_ptr); + } + + node_ptr root_ptr = t_info->root_ptr; + check_overflow(root_ptr); + if (get_node_key(root_ptr) > key) { + ++t_info->t_size; + + node_ptr n = next_free_node(); + check_overflow(n); + get_node_key(n) = key; + get_node_lc(n) = get_node_lc(root_ptr); + get_node_rc(n) = root_ptr; + get_val(n) = val; + + get_node_lc(root_ptr) = 0; + t_info->root_ptr = n; + return &get_val(n); + } else { + ++t_info->t_size; + + node_ptr n = next_free_node(); + check_overflow(n); + get_node_key(n) = key; + get_node_rc(n) = get_node_rc(root_ptr); + get_node_lc(n) = root_ptr; + get_val(n) = val; + + get_node_rc(root_ptr) = 0; + t_info->root_ptr = n; + return &get_val(n); + } + } + + TVal* remove(TKey key) { + /* Deletes i from the tree if it's there. */ + /* Return a pointer to the resulting tree. */ + if (splay(key, t_info->root_ptr)) { /* found it */ + node_ptr x; + check_overflow(t_info->root_ptr); + + if (get_node_lc(t_info->root_ptr) == 0) { + x = get_node_rc(t_info->root_ptr); + } else { + x = get_node_lc(t_info->root_ptr); + splay(key, x); + get_node_rc(x) = get_node_rc(t_info->root_ptr); + } + node_ptr r = t_info->root_ptr; + free_node(t_info->root_ptr); + --t_info->t_size; + t_info->root_ptr = x; + check_overflow(r); + return &get_val(r); + } + + return nullptr; + } + + bool exist(TKey key) { return (last_ptr = inter_find(key)) != 0; } + + // should check exist() first + TVal &find() { + check_overflow(last_ptr); + return get_val(last_ptr); } + + uint32_t root_ptr() { + return t_info->root_ptr; + } + + TVal &get(uint32_t ptr) { + return get_val(ptr); + } + + uint32_t size() { return t_info->t_size; } + + void enumerate(std::function callback) { inter_enumerate(t_info->root_ptr, callback); } +}; + +#endif /* splay_map_h */ diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/type.h b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/type.h index 3c34fa337..22c4d535c 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/type.h +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/cpp/type.h @@ -13,6 +13,7 @@ #include #include #include +#include #ifndef OPENGL_API_HOOK_TYPE_H #define OPENGL_API_HOOK_TYPE_H @@ -61,6 +62,11 @@ typedef void (*System_GlRenderbufferStorage)(GLenum target, GLsizei width, GLsizei height); +typedef EGLContext (*System_eglCreateContext)(EGLDisplay dpy, EGLConfig config, EGLContext share_context, + const EGLint *attrib_list); + +typedef EGLBoolean (*System_eglDestroyContext)(EGLDisplay dpy, EGLContext ctx); + System_GlNormal_TYPE get_target_func_ptr(const char *func_name); System_GlBind_TYPE get_bind_func_ptr(const char *func_name); diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/OpenglLeakPlugin.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/OpenglLeakPlugin.java index 9dcc52175..48f34af1f 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/OpenglLeakPlugin.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/OpenglLeakPlugin.java @@ -7,7 +7,6 @@ import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; - import com.tencent.matrix.openglleak.comm.FuncNameString; import com.tencent.matrix.openglleak.detector.IOpenglIndexDetector; import com.tencent.matrix.openglleak.detector.OpenglIndexDetectorService; @@ -100,6 +99,7 @@ private void executeHook(IBinder iBinder) { } // hook + OpenGLHook.hookEgl(); // hook eglCreateContext/eglDestroyContext first OpenGLHook.getInstance().hook(FuncNameString.GL_GEN_TEXTURES, map.get(FuncNameString.GL_GEN_TEXTURES)); OpenGLHook.getInstance().hook(FuncNameString.GL_DELETE_TEXTURES, map.get(FuncNameString.GL_DELETE_TEXTURES)); OpenGLHook.getInstance().hook(FuncNameString.GL_GEN_BUFFERS, map.get(FuncNameString.GL_GEN_BUFFERS)); @@ -108,17 +108,17 @@ private void executeHook(IBinder iBinder) { OpenGLHook.getInstance().hook(FuncNameString.GL_DELETE_FRAMEBUFFERS, map.get(FuncNameString.GL_DELETE_FRAMEBUFFERS)); OpenGLHook.getInstance().hook(FuncNameString.GL_GEN_RENDERBUFFERS, map.get(FuncNameString.GL_GEN_RENDERBUFFERS)); OpenGLHook.getInstance().hook(FuncNameString.GL_DELETE_RENDERBUFFERS, map.get(FuncNameString.GL_DELETE_RENDERBUFFERS)); -// OpenGLHook.getInstance().hook(FuncNameString.GL_TEX_IMAGE_2D, map.get(FuncNameString.GL_TEX_IMAGE_2D)); -// OpenGLHook.getInstance().hook(FuncNameString.GL_TEX_IMAGE_3D, map.get(FuncNameString.GL_TEX_IMAGE_3D)); -// OpenGLHook.getInstance().hook(FuncNameString.GL_BIND_TEXTURE, map.get(FuncNameString.GL_BIND_TEXTURE)); -// OpenGLHook.getInstance().hook(FuncNameString.GL_BIND_BUFFER, map.get(FuncNameString.GL_BIND_BUFFER)); + OpenGLHook.getInstance().hook(FuncNameString.GL_TEX_IMAGE_2D, map.get(FuncNameString.GL_TEX_IMAGE_2D)); + OpenGLHook.getInstance().hook(FuncNameString.GL_TEX_IMAGE_3D, map.get(FuncNameString.GL_TEX_IMAGE_3D)); + OpenGLHook.getInstance().hook(FuncNameString.GL_BIND_TEXTURE, map.get(FuncNameString.GL_BIND_TEXTURE)); + OpenGLHook.getInstance().hook(FuncNameString.GL_BIND_BUFFER, map.get(FuncNameString.GL_BIND_BUFFER)); // OpenGLHook.getInstance().hook(FuncNameString.GL_BIND_FRAMEBUFFER, map.get(FuncNameString.GL_BIND_FRAMEBUFFER)); -// OpenGLHook.getInstance().hook(FuncNameString.GL_BIND_RENDERBUFFER, map.get(FuncNameString.GL_BIND_RENDERBUFFER)); -// OpenGLHook.getInstance().hook(FuncNameString.GL_BUFFER_DATA, map.get(FuncNameString.GL_BUFFER_DATA)); -// OpenGLHook.getInstance().hook(FuncNameString.GL_RENDER_BUFFER_STORAGE, map.get(FuncNameString.GL_RENDER_BUFFER_STORAGE)); + OpenGLHook.getInstance().hook(FuncNameString.GL_BIND_RENDERBUFFER, map.get(FuncNameString.GL_BIND_RENDERBUFFER)); + OpenGLHook.getInstance().hook(FuncNameString.GL_BUFFER_DATA, map.get(FuncNameString.GL_BUFFER_DATA)); + OpenGLHook.getInstance().hook(FuncNameString.GL_RENDER_BUFFER_STORAGE, map.get(FuncNameString.GL_RENDER_BUFFER_STORAGE)); MatrixLog.e(TAG, "hook finish"); } catch (Throwable e) { - e.printStackTrace(); + MatrixLog.printErrStackTrace(TAG, e, ""); } finally { // 销毁实验进程 try { @@ -132,22 +132,27 @@ private void executeHook(IBinder iBinder) { private void startImpl() { Intent service = new Intent(context, OpenglIndexDetectorService.class); - boolean result = context.bindService(service, new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName componentName, final IBinder iBinder) { - new Thread(new Runnable() { - @Override - public void run() { - executeHook(iBinder); - } - }).start(); - } + boolean result = false; + try { + result = context.bindService(service, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, final IBinder iBinder) { + new Thread(new Runnable() { + @Override + public void run() { + executeHook(iBinder); + } + }).start(); + } - @Override - public void onServiceDisconnected(ComponentName componentName) { - context.unbindService(this); - } - }, context.BIND_AUTO_CREATE); + @Override + public void onServiceDisconnected(ComponentName componentName) { + context.unbindService(this); + } + }, Context.BIND_AUTO_CREATE); + } catch (Exception e) { + MatrixLog.d(TAG, "bindService error = " + e.getCause()); + } MatrixLog.d(TAG, "bindService result = " + result); if (result) { diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/detector/FuncSeeker.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/detector/FuncSeeker.java index 363a90ce8..596ca8a13 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/detector/FuncSeeker.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/detector/FuncSeeker.java @@ -1,7 +1,10 @@ package com.tencent.matrix.openglleak.detector; +import androidx.annotation.Keep; + import com.tencent.matrix.openglleak.comm.FuncNameString; +@Keep class FuncSeeker { public static int getFuncIndex(String targetFuncName) { diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/hook/OpenGLHook.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/hook/OpenGLHook.java index 505a5d416..4676e4777 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/hook/OpenGLHook.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/hook/OpenGLHook.java @@ -4,17 +4,20 @@ import android.opengl.EGL14; +import androidx.annotation.Keep; + import com.tencent.matrix.openglleak.comm.FuncNameString; import com.tencent.matrix.openglleak.statistics.BindCenter; import com.tencent.matrix.openglleak.statistics.resource.MemoryInfo; import com.tencent.matrix.openglleak.statistics.resource.OpenGLInfo; import com.tencent.matrix.openglleak.statistics.resource.ResRecordManager; import com.tencent.matrix.openglleak.utils.ActivityRecorder; -import com.tencent.matrix.openglleak.utils.ExecuteCenter; +import com.tencent.matrix.openglleak.utils.JavaStacktrace; import com.tencent.matrix.util.MatrixLog; import java.util.concurrent.atomic.AtomicInteger; +@Keep public class OpenGLHook { static { @@ -23,7 +26,7 @@ public class OpenGLHook { private static final String TAG = "MicroMsg.OpenGLHook"; - private static OpenGLHook mInstance = new OpenGLHook(); + private static final OpenGLHook mInstance = new OpenGLHook(); private ResourceListener mResourceListener; private ErrorListener mErrorListener; private BindListener mBindListener; @@ -133,18 +136,30 @@ public boolean hook(String targetFuncName, int index) { private static native boolean hookGlRenderbufferStorage(int index); - public static void onGlGenTextures(int[] ids, final String threadId, final String javaStack, final long nativeStackPtr) { + public static native boolean hookEgl(); + + public static native String dumpNativeStack(long nativeStackPtr); + + public static native String dumpBriefNativeStack(long nativeStackPtr); + + public static native void releaseNative(long nativeStackPtr); + + public static native boolean isEglContextAlive(long eglContext); + + public static native boolean isEglSurfaceAlive(long eglSurface); + + public native void updateCurrActivity(String activityInfo); + + public native int getResidualQueueSize(); + + public static void onGlGenTextures(int[] ids, final String threadId, final int throwable, final long nativeStackPtr, final long eglContext, final long eglDrawSurface, final long eglReadSurface, String activityInfo) { if (ids.length > 0) { final AtomicInteger counter = new AtomicInteger(ids.length); + JavaStacktrace.Trace trace = JavaStacktrace.getBacktraceValue(throwable); for (final int id : ids) { - final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.TEXTURE, id, threadId, EGL14.eglGetCurrentContext(), javaStack, nativeStackPtr, ActivityRecorder.getInstance().getCurrentActivityInfo(), counter); - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - ResRecordManager.getInstance().gen(openGLInfo); - } - }); + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.TEXTURE, id, threadId, eglContext, eglDrawSurface, eglReadSurface, trace, nativeStackPtr, ActivityRecorder.revertActivityInfo(activityInfo), counter); + ResRecordManager.getInstance().gen(openGLInfo); if (getInstance().mResourceListener != null) { getInstance().mResourceListener.onGlGenTextures(openGLInfo); @@ -153,16 +168,11 @@ public void run() { } } - public static void onGlDeleteTextures(int[] ids, String threadId) { + public static void onGlDeleteTextures(int[] ids, String threadId, final long eglContext) { if (ids.length > 0) { for (int id : ids) { - final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.TEXTURE, id, threadId, EGL14.eglGetCurrentContext()); - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - ResRecordManager.getInstance().delete(openGLInfo); - } - }); + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.TEXTURE, id, threadId, eglContext); + ResRecordManager.getInstance().delete(openGLInfo); if (getInstance().mResourceListener != null) { getInstance().mResourceListener.onGlDeleteTextures(openGLInfo); @@ -171,18 +181,14 @@ public void run() { } } - public static void onGlGenBuffers(int[] ids, String threadId, String javaStack, long nativeStackPtr) { + public static void onGlGenBuffers(int[] ids, String threadId, final int throwable, long nativeStackPtr, final long eglContext, final long eglDrawSurface, final long eglReadSurface, String activityInfo) { if (ids.length > 0) { AtomicInteger counter = new AtomicInteger(ids.length); + JavaStacktrace.Trace trace = JavaStacktrace.getBacktraceValue(throwable); for (int id : ids) { - final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.BUFFER, id, threadId, EGL14.eglGetCurrentContext(), javaStack, nativeStackPtr, ActivityRecorder.getInstance().getCurrentActivityInfo(), counter); - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - ResRecordManager.getInstance().gen(openGLInfo); - } - }); + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.BUFFER, id, threadId, eglContext, eglDrawSurface, eglReadSurface, trace, nativeStackPtr, ActivityRecorder.revertActivityInfo(activityInfo), counter); + ResRecordManager.getInstance().gen(openGLInfo); if (getInstance().mResourceListener != null) { getInstance().mResourceListener.onGlGenBuffers(openGLInfo); @@ -191,16 +197,11 @@ public void run() { } } - public static void onGlDeleteBuffers(int[] ids, String threadId) { + public static void onGlDeleteBuffers(int[] ids, String threadId, final long eglContext) { if (ids.length > 0) { for (int id : ids) { - final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.BUFFER, id, threadId, EGL14.eglGetCurrentContext()); - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - ResRecordManager.getInstance().delete(openGLInfo); - } - }); + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.BUFFER, id, threadId, eglContext); + ResRecordManager.getInstance().delete(openGLInfo); if (getInstance().mResourceListener != null) { getInstance().mResourceListener.onGlDeleteBuffers(openGLInfo); @@ -209,18 +210,14 @@ public void run() { } } - public static void onGlGenFramebuffers(int[] ids, String threadId, String javaStack, long nativeStackPtr) { + public static void onGlGenFramebuffers(int[] ids, String threadId, final int throwable, long nativeStackPtr, final long eglContext, final long eglDrawSurface, final long eglReadSurface, String activityInfo) { if (ids.length > 0) { AtomicInteger counter = new AtomicInteger(ids.length); + JavaStacktrace.Trace trace = JavaStacktrace.getBacktraceValue(throwable); for (int id : ids) { - final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.FRAME_BUFFERS, id, threadId, EGL14.eglGetCurrentContext(), javaStack, nativeStackPtr, ActivityRecorder.getInstance().getCurrentActivityInfo(), counter); - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - ResRecordManager.getInstance().gen(openGLInfo); - } - }); + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.FRAME_BUFFERS, id, threadId, eglContext, eglDrawSurface, eglReadSurface, trace, nativeStackPtr, ActivityRecorder.revertActivityInfo(activityInfo), counter); + ResRecordManager.getInstance().gen(openGLInfo); if (getInstance().mResourceListener != null) { getInstance().mResourceListener.onGlGenFramebuffers(openGLInfo); @@ -229,16 +226,11 @@ public void run() { } } - public static void onGlDeleteFramebuffers(int[] ids, String threadId) { + public static void onGlDeleteFramebuffers(int[] ids, String threadId, final long eglContext) { if (ids.length > 0) { for (int id : ids) { - final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.FRAME_BUFFERS, id, threadId, EGL14.eglGetCurrentContext()); - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - ResRecordManager.getInstance().delete(openGLInfo); - } - }); + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.FRAME_BUFFERS, id, threadId, eglContext); + ResRecordManager.getInstance().delete(openGLInfo); if (getInstance().mResourceListener != null) { getInstance().mResourceListener.onGlDeleteFramebuffers(openGLInfo); @@ -247,18 +239,14 @@ public void run() { } } - public static void onGlGenRenderbuffers(int[] ids, String threadId, String javaStack, long nativeStackPtr) { + public static void onGlGenRenderbuffers(int[] ids, String threadId, final int throwable, long nativeStackPtr, final long eglContext, final long eglDrawSurface, final long eglReadSurface, String activityInfo) { if (ids.length > 0) { AtomicInteger counter = new AtomicInteger(ids.length); + JavaStacktrace.Trace trace = JavaStacktrace.getBacktraceValue(throwable); for (int id : ids) { - final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.RENDER_BUFFERS, id, threadId, EGL14.eglGetCurrentContext(), javaStack, nativeStackPtr, ActivityRecorder.getInstance().getCurrentActivityInfo(), counter); - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - ResRecordManager.getInstance().gen(openGLInfo); - } - }); + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.RENDER_BUFFERS, id, threadId, eglContext, eglDrawSurface, eglReadSurface, trace, nativeStackPtr, ActivityRecorder.revertActivityInfo(activityInfo), counter); + ResRecordManager.getInstance().gen(openGLInfo); if (getInstance().mResourceListener != null) { getInstance().mResourceListener.onGlGenRenderbuffers(openGLInfo); @@ -267,16 +255,11 @@ public void run() { } } - public static void onGlDeleteRenderbuffers(int[] ids, String threadId) { + public static void onGlDeleteRenderbuffers(int[] ids, String threadId, final long eglContext) { if (ids.length > 0) { for (int id : ids) { - final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.RENDER_BUFFERS, id, threadId, EGL14.eglGetCurrentContext()); - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - ResRecordManager.getInstance().delete(openGLInfo); - } - }); + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.RENDER_BUFFERS, id, threadId, eglContext); + ResRecordManager.getInstance().delete(openGLInfo); if (getInstance().mResourceListener != null) { getInstance().mResourceListener.onGlDeleteRenderbuffers(openGLInfo); @@ -285,6 +268,36 @@ public void run() { } } + public static void onEglContextCreate(String threadId, final int throwable, long nativeStackPtr, final long eglContext, final long shareContext, String activityInfo) { + AtomicInteger counter = new AtomicInteger(1); + + JavaStacktrace.Trace trace = JavaStacktrace.getBacktraceValue(throwable); + + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.EGL_CONTEXT, -1, threadId, eglContext, 0, 0, trace, nativeStackPtr, ActivityRecorder.revertActivityInfo(activityInfo), counter); + ResRecordManager.getInstance().gen(openGLInfo); + ResRecordManager.getInstance().createContext(shareContext, eglContext); + + if (getInstance().mResourceListener != null) { + getInstance().mResourceListener.onEglContextCreate(openGLInfo); + } + } + + public static void onEglContextDestroy(String threadId, final long eglContext, final int ret) { + final OpenGLInfo openGLInfo = new OpenGLInfo(OpenGLInfo.TYPE.EGL_CONTEXT, -1, threadId, eglContext); + + if (ret == EGL14.EGL_FALSE) { + MatrixLog.e(TAG, "eglContextDestroy failed: thread=%s, context=%s, ret=%s, errno=%s", threadId, eglContext, ret, EGL14.eglGetError()); + return; + } + + ResRecordManager.getInstance().delete(openGLInfo); + ResRecordManager.getInstance().destroyContext(eglContext); + + if (getInstance().mResourceListener != null) { + getInstance().mResourceListener.onEglContextDestroy(openGLInfo); + } + } + public static void onGetError(int eid) { if (getInstance().mErrorListener != null) { getInstance().mErrorListener.onGlError(eid); @@ -292,226 +305,140 @@ public static void onGetError(int eid) { } - public static void onGlBindTexture(final int target, final int id) { - long eglContextId = 0L; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - eglContextId = EGL14.eglGetCurrentContext().getNativeHandle(); + public static void onGlBindTexture(final int target, final int id, final long eglContext) { + OpenGLInfo info = null; + if (id != 0) { + info = ResRecordManager.getInstance().findOpenGLInfo(OpenGLInfo.TYPE.TEXTURE, eglContext, id); } - - final long finalEglContextId = eglContextId; - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - OpenGLInfo info = null; - if (id != 0) { - info = ResRecordManager.getInstance().findOpenGLInfo(OpenGLInfo.TYPE.TEXTURE, finalEglContextId, id); - } - BindCenter.getInstance().glBindResource(OpenGLInfo.TYPE.TEXTURE, target, finalEglContextId, info); - } - }); + BindCenter.getInstance().glBindResource(OpenGLInfo.TYPE.TEXTURE, target, eglContext, info); if (getInstance().mBindListener != null) { - getInstance().mBindListener.onGlBindTexture(target, eglContextId, id); + getInstance().mBindListener.onGlBindTexture(target, eglContext, id); } } - public static void onGlBindBuffer(final int target, final int id) { - long eglContextId = 0L; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - eglContextId = EGL14.eglGetCurrentContext().getNativeHandle(); + public static void onGlBindBuffer(final int target, final int id, final long eglContext) { + OpenGLInfo info = null; + if (id != 0) { + info = ResRecordManager.getInstance().findOpenGLInfo(OpenGLInfo.TYPE.BUFFER, eglContext, id); } - - final long finalEglContextId = eglContextId; - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - OpenGLInfo info = null; - if (id != 0) { - info = ResRecordManager.getInstance().findOpenGLInfo(OpenGLInfo.TYPE.BUFFER, finalEglContextId, id); - } - BindCenter.getInstance().glBindResource(OpenGLInfo.TYPE.BUFFER, target, finalEglContextId, info); - } - }); + BindCenter.getInstance().glBindResource(OpenGLInfo.TYPE.BUFFER, target, eglContext, info); if (getInstance().mBindListener != null) { - getInstance().mBindListener.onGlBindBuffer(target, eglContextId, id); + getInstance().mBindListener.onGlBindBuffer(target, eglContext, id); } } - public static void onGlBindFramebuffer(final int target, final int id) { - long eglContextId = 0L; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - eglContextId = EGL14.eglGetCurrentContext().getNativeHandle(); + public static void onGlBindFramebuffer(final int target, final int id, final long eglContext) { + OpenGLInfo info = null; + if (id != 0) { + info = ResRecordManager.getInstance().findOpenGLInfo(OpenGLInfo.TYPE.FRAME_BUFFERS, eglContext, id); } - - final long finalEglContextId = eglContextId; - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - OpenGLInfo info = null; - if (id != 0) { - info = ResRecordManager.getInstance().findOpenGLInfo(OpenGLInfo.TYPE.FRAME_BUFFERS, finalEglContextId, id); - } - BindCenter.getInstance().glBindResource(OpenGLInfo.TYPE.FRAME_BUFFERS, target, finalEglContextId, info); - } - }); + BindCenter.getInstance().glBindResource(OpenGLInfo.TYPE.FRAME_BUFFERS, target, eglContext, info); if (getInstance().mBindListener != null) { - getInstance().mBindListener.onGlBindFramebuffer(target, eglContextId, id); + getInstance().mBindListener.onGlBindFramebuffer(target, eglContext, id); } } - public static void onGlBindRenderbuffer(final int target, final int id) { - long eglContextId = 0L; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - eglContextId = EGL14.eglGetCurrentContext().getNativeHandle(); + public static void onGlBindRenderbuffer(final int target, final int id, final long eglContext) { + OpenGLInfo info = null; + if (id != 0) { + info = ResRecordManager.getInstance().findOpenGLInfo(OpenGLInfo.TYPE.RENDER_BUFFERS, eglContext, id); } - - final long finalEglContextId = eglContextId; - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - OpenGLInfo info = null; - if (id != 0) { - info = ResRecordManager.getInstance().findOpenGLInfo(OpenGLInfo.TYPE.RENDER_BUFFERS, finalEglContextId, id); - } - BindCenter.getInstance().glBindResource(OpenGLInfo.TYPE.RENDER_BUFFERS, target, finalEglContextId, info); - } - }); + BindCenter.getInstance().glBindResource(OpenGLInfo.TYPE.RENDER_BUFFERS, target, eglContext, info); if (getInstance().mBindListener != null) { - getInstance().mBindListener.onGlBindRenderbuffer(target, eglContextId, id); + getInstance().mBindListener.onGlBindRenderbuffer(target, eglContext, id); } } - public static void onGlTexImage2D(final int target, final int level, final int internalFormat, final int width, final int height, final int border, final int format, final int type, final long size, final String javaStack, final long nativeStack) { - long eglContextId = 0L; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - eglContextId = EGL14.eglGetCurrentContext().getNativeHandle(); + public static void onGlTexImage2D(final int target, final int level, final int internalFormat, final int width, final int height, final int border, final int format, final int type, final long size, final int throwable, final long nativeStack, final long eglContext) { + final OpenGLInfo openGLInfo = BindCenter.getInstance().findCurrentResourceIdByTarget(OpenGLInfo.TYPE.TEXTURE, eglContext, target); + if (openGLInfo == null) { + MatrixLog.e(TAG, "onGlTexImage2D: getCurrentResourceIdByTarget openGLID == null, maybe didn't call glBindTextures()"); + return; } - final long finalEglContextId = eglContextId; - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - final OpenGLInfo openGLInfo = BindCenter.getInstance().findCurrentResourceIdByTarget(OpenGLInfo.TYPE.TEXTURE, finalEglContextId, target); - if (openGLInfo == null) { - MatrixLog.e(TAG, "onGlTexImage2D: getCurrentResourceIdByTarget openGLID == null, maybe undo glBindTextures()"); - return; - } - - MemoryInfo memoryInfo = openGLInfo.getMemoryInfo(); - if (memoryInfo == null) { - memoryInfo = new MemoryInfo(OpenGLInfo.TYPE.TEXTURE); - } - memoryInfo.setTexturesInfo(target, level, internalFormat, width, height, 0, border, format, type, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size, javaStack, nativeStack); - openGLInfo.setMemoryInfo(memoryInfo); - if (getInstance().mMemoryListener != null) { - getInstance().mMemoryListener.onGlTexImage2D(target, level, internalFormat, width, height, border, format, type, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size); - } - } - }); + JavaStacktrace.Trace javatrace = JavaStacktrace.getBacktraceValue(throwable); + MemoryInfo memoryInfo = openGLInfo.getMemoryInfo(); + if (memoryInfo == null) { + memoryInfo = new MemoryInfo(OpenGLInfo.TYPE.TEXTURE); + } + memoryInfo.setTexturesInfo(target, level, internalFormat, width, height, 0, border, format, type, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size, javatrace, nativeStack); + openGLInfo.setMemoryInfo(memoryInfo); + if (getInstance().mMemoryListener != null) { + getInstance().mMemoryListener.onGlTexImage2D(target, level, internalFormat, width, height, border, format, type, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size); + } } - public static void onGlTexImage3D(final int target, final int level, final int internalFormat, final int width, final int height, final int depth, final int border, final int format, final int type, final long size, final String javaStack, final long nativeStack) { - long eglContextId = 0L; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - eglContextId = EGL14.eglGetCurrentContext().getNativeHandle(); + public static void onGlTexImage3D(final int target, final int level, final int internalFormat, final int width, final int height, final int depth, final int border, final int format, final int type, final long size, final int throwable, final long nativeStack, final long eglContext) { + final OpenGLInfo openGLInfo = BindCenter.getInstance().findCurrentResourceIdByTarget(OpenGLInfo.TYPE.TEXTURE, eglContext, target); + if (openGLInfo == null) { + MatrixLog.e(TAG, "onGlTexImage3D: getCurrentResourceIdByTarget result == null, maybe didn't call glBindTextures()"); + return; } - final long finalEglContextId = eglContextId; - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - final OpenGLInfo openGLInfo = BindCenter.getInstance().findCurrentResourceIdByTarget(OpenGLInfo.TYPE.TEXTURE, finalEglContextId, target); - if (openGLInfo == null) { - MatrixLog.e(TAG, "onGlTexImage3D: getCurrentResourceIdByTarget result == null, maybe undo glBindTextures()"); - return; - } - - MemoryInfo memoryInfo = openGLInfo.getMemoryInfo(); - if (memoryInfo == null) { - memoryInfo = new MemoryInfo(OpenGLInfo.TYPE.TEXTURE); - } - memoryInfo.setTexturesInfo(target, level, internalFormat, width, height, depth, border, format, type, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size, javaStack, nativeStack); - openGLInfo.setMemoryInfo(memoryInfo); - if (getInstance().mMemoryListener != null) { - getInstance().mMemoryListener.onGlTexImage3D(target, level, internalFormat, width, height, depth, border, format, type, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size); - } - } - }); + JavaStacktrace.Trace javatrace = JavaStacktrace.getBacktraceValue(throwable); + MemoryInfo memoryInfo = openGLInfo.getMemoryInfo(); + if (memoryInfo == null) { + memoryInfo = new MemoryInfo(OpenGLInfo.TYPE.TEXTURE); + } + memoryInfo.setTexturesInfo(target, level, internalFormat, width, height, depth, border, format, type, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size, javatrace, nativeStack); + openGLInfo.setMemoryInfo(memoryInfo); + if (getInstance().mMemoryListener != null) { + getInstance().mMemoryListener.onGlTexImage3D(target, level, internalFormat, width, height, depth, border, format, type, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size); + } } - public static void onGlBufferData(final int target, final int usage, final long size, final String javaStack, final long nativeStack) { - if (Thread.currentThread().getName().equals("RenderThread")) { + public static void onGlBufferData(final int target, final int usage, final long size, final int throwable, final long nativeStack, final long eglContext) { + final OpenGLInfo openGLInfo = BindCenter.getInstance().findCurrentResourceIdByTarget(OpenGLInfo.TYPE.BUFFER, eglContext, target); + if (openGLInfo == null) { + MatrixLog.e(TAG, "onGlBufferData: getCurrentResourceIdByTarget result == null, maybe didn't call glBindBuffer()"); return; } - long eglContextId = 0L; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - eglContextId = EGL14.eglGetCurrentContext().getNativeHandle(); - } - final long finalEglContextId = eglContextId; - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - final OpenGLInfo openGLInfo = BindCenter.getInstance().findCurrentResourceIdByTarget(OpenGLInfo.TYPE.BUFFER, finalEglContextId, target); - if (openGLInfo == null) { - MatrixLog.e(TAG, "onGlBufferData: getCurrentResourceIdByTarget result == null, maybe undo glBindBuffer()"); - return; - } + long actualSize = -1; + if (target == GL_PIXEL_UNPACK_BUFFER) { + actualSize = size * 2; + } else { + actualSize = size; + } - long actualSize = -1; - if (target == GL_PIXEL_UNPACK_BUFFER) { - actualSize = size * 2; - } else { - actualSize = size; - } - MemoryInfo memoryInfo = openGLInfo.getMemoryInfo(); - if (memoryInfo == null) { - memoryInfo = new MemoryInfo(OpenGLInfo.TYPE.BUFFER); - } - memoryInfo.setBufferInfo(target, usage, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), actualSize, javaStack, nativeStack); - openGLInfo.setMemoryInfo(memoryInfo); + JavaStacktrace.Trace javatrace = JavaStacktrace.getBacktraceValue(throwable); + MemoryInfo memoryInfo = openGLInfo.getMemoryInfo(); + if (memoryInfo == null) { + memoryInfo = new MemoryInfo(OpenGLInfo.TYPE.BUFFER); + } + memoryInfo.setBufferInfo(target, usage, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), actualSize, javatrace, nativeStack); + openGLInfo.setMemoryInfo(memoryInfo); - if (getInstance().mMemoryListener != null) { - getInstance().mMemoryListener.onGlBufferData(target, usage, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size); - } - } - }); + if (getInstance().mMemoryListener != null) { + getInstance().mMemoryListener.onGlBufferData(target, usage, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size); + } } - public static void onGlRenderbufferStorage(final int target, final int internalformat, final int width, final int height, final long size, final String javaStack, final long nativeStack) { - long eglContextId = 0L; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - eglContextId = EGL14.eglGetCurrentContext().getNativeHandle(); + public static void onGlRenderbufferStorage(final int target, final int internalformat, final int width, final int height, final long size, final int key, final long nativeStack, final long eglContext) { + final OpenGLInfo openGLInfo = BindCenter.getInstance().findCurrentResourceIdByTarget(OpenGLInfo.TYPE.RENDER_BUFFERS, eglContext, target); + if (openGLInfo == null) { + MatrixLog.e(TAG, "onGlRenderbufferStorage: getCurrentResourceIdByTarget result == null, maybe didn't call glBindRenderbuffer()"); + return; } - final long finalEglContextId = eglContextId; - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - final OpenGLInfo openGLInfo = BindCenter.getInstance().findCurrentResourceIdByTarget(OpenGLInfo.TYPE.RENDER_BUFFERS, finalEglContextId, target); - if (openGLInfo == null) { - MatrixLog.e(TAG, "onGlRenderbufferStorage: getCurrentResourceIdByTarget result == null, maybe undo glBindRenderbuffer()"); - return; - } - - MemoryInfo memoryInfo = openGLInfo.getMemoryInfo(); - if (memoryInfo == null) { - memoryInfo = new MemoryInfo(OpenGLInfo.TYPE.RENDER_BUFFERS); - } - memoryInfo.setRenderbufferInfo(target, width, height, internalformat, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size, javaStack, nativeStack); - openGLInfo.setMemoryInfo(memoryInfo); + JavaStacktrace.Trace javatrace = JavaStacktrace.getBacktraceValue(key); + MemoryInfo memoryInfo = openGLInfo.getMemoryInfo(); + if (memoryInfo == null) { + memoryInfo = new MemoryInfo(OpenGLInfo.TYPE.RENDER_BUFFERS); + } + memoryInfo.setRenderbufferInfo(target, width, height, internalformat, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size, javatrace, nativeStack); + openGLInfo.setMemoryInfo(memoryInfo); - if (getInstance().mMemoryListener != null) { - getInstance().mMemoryListener.onGlRenderbufferStorage(target, width, height, internalformat, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size); - } - } - }); + if (getInstance().mMemoryListener != null) { + getInstance().mMemoryListener.onGlRenderbufferStorage(target, width, height, internalformat, openGLInfo.getId(), openGLInfo.getEglContextNativeHandle(), size); + } } public interface ErrorListener { @@ -546,6 +473,10 @@ public interface MemoryListener { public interface ResourceListener { + void onEglContextCreate(OpenGLInfo info); + + void onEglContextDestroy(OpenGLInfo info); + void onGlGenTextures(OpenGLInfo info); void onGlDeleteTextures(OpenGLInfo info); @@ -563,30 +494,8 @@ public interface ResourceListener { void onGlDeleteRenderbuffers(OpenGLInfo info); } - public static String getStack() { - return stackTraceToString(new Throwable().getStackTrace()); - } - - private static String stackTraceToString(final StackTraceElement[] arr) { - if (arr == null) { - return ""; - } - - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < arr.length; i++) { - if (i == 0) { - continue; - } - StackTraceElement element = arr[i]; - String className = element.getClassName(); - // remove unused stacks - if (className.contains("java.lang.Thread")) { - continue; - } - sb.append("\t").append(element).append('\n'); - } - return sb.toString(); + public static int getThrowable() { + return JavaStacktrace.getBacktraceKey(); } } diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/BindMap.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/BindMap.java index 8f869e404..7664963d1 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/BindMap.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/BindMap.java @@ -26,7 +26,6 @@ import static javax.microedition.khronos.opengles.GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_NEGATIVE_X; import com.tencent.matrix.openglleak.statistics.resource.OpenGLInfo; -import com.tencent.matrix.openglleak.utils.ExecuteCenter; import java.util.HashMap; import java.util.Map; @@ -136,22 +135,17 @@ public void putBindInfo(final OpenGLInfo.TYPE type, final int target, final long if (!isSupportTarget(type, target)) { return; } - ExecuteCenter.getInstance().post(new Runnable() { - @Override - public void run() { - switch (type) { - case BUFFER: - putInBindMap(bindBufferMap, target, eglContextId, info, OpenGLInfo.TYPE.BUFFER); - break; - case TEXTURE: - putInBindMap(bindTextureMap, target, eglContextId, info, OpenGLInfo.TYPE.TEXTURE); - break; - case RENDER_BUFFERS: - bindRenderbufferMap.put(eglContextId, info); - break; - } - } - }); + switch (type) { + case BUFFER: + putInBindMap(bindBufferMap, target, eglContextId, info, OpenGLInfo.TYPE.BUFFER); + break; + case TEXTURE: + putInBindMap(bindTextureMap, target, eglContextId, info, OpenGLInfo.TYPE.TEXTURE); + break; + case RENDER_BUFFERS: + bindRenderbufferMap.put(eglContextId, info); + break; + } } } diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/LeakMonitorForBackstage.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/LeakMonitorForBackstage.java index c6a2768be..461a5c7bf 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/LeakMonitorForBackstage.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/LeakMonitorForBackstage.java @@ -3,8 +3,8 @@ import android.app.Application; import android.os.Handler; -import com.tencent.matrix.AppActiveMatrixDelegate; -import com.tencent.matrix.listeners.IAppForeground; +import com.tencent.matrix.lifecycle.IStateObserver; +import com.tencent.matrix.lifecycle.owners.ProcessExplicitBackgroundOwner; import com.tencent.matrix.openglleak.statistics.resource.OpenGLInfo; import com.tencent.matrix.openglleak.statistics.resource.ResRecordManager; import com.tencent.matrix.openglleak.utils.GlLeakHandlerThread; @@ -14,12 +14,13 @@ import java.util.List; -public class LeakMonitorForBackstage extends LeakMonitorDefault implements IAppForeground { +public class LeakMonitorForBackstage extends LeakMonitorDefault implements IStateObserver { private final long mBackstageCheckTime; private Handler mH; private LeakMonitorForBackstage.LeakListener mLeakListener; + private LeakMonitorForBackstage.LeakListListener mLeakListListener; private List mLeaksList = new LinkedList<>(); @@ -39,29 +40,25 @@ public void setLeakListener(LeakListener l) { mLeakListener = l; } + public void setLeakListListener(LeakListListener l) { + mLeakListListener = l; + } + public void start(Application context) { - AppActiveMatrixDelegate.INSTANCE.addListener(this); + ProcessExplicitBackgroundOwner.INSTANCE.observeForever(this); super.start(context); } public void stop(Application context) { - AppActiveMatrixDelegate.INSTANCE.removeListener(this); + ProcessExplicitBackgroundOwner.INSTANCE.removeObserver(this); super.stop(context); } - @Override - public void onForeground(boolean isForeground) { - if (isForeground) { - foreground(); - } else { - background(); - } - } - private Runnable mRunnable = new Runnable() { @Override public void run() { synchronized (mLeaksList) { + List allInfos = new LinkedList<>(); Iterator it = mLeaksList.iterator(); while (it.hasNext()) { OpenGLInfo item = it.next(); @@ -69,29 +66,35 @@ public void run() { if (null != mLeakListener) { if (!ResRecordManager.getInstance().isGLInfoRelease(item)) { mLeakListener.onLeak(item); + allInfos.add(item); } } - it.remove(); } } - } - - if (null == mLeakListener) { - return; + if (mLeakListListener != null) { + mLeakListListener.onLeak(allInfos); + } } } }; - public void foreground() { - mH.removeCallbacks(mRunnable); + @Override + public void on() { + mH.postDelayed(mRunnable, mBackstageCheckTime); } - public void background() { - mH.postDelayed(mRunnable, mBackstageCheckTime); + @Override + public void off() { + mH.removeCallbacks(mRunnable); } public interface LeakListener { void onLeak(OpenGLInfo info); } + + public interface LeakListListener { + void onLeak(List infos); + } + } diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/MemoryInfo.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/MemoryInfo.java index a7a2190c1..3229e2c4e 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/MemoryInfo.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/MemoryInfo.java @@ -10,6 +10,8 @@ import static android.opengl.GLES30.GL_TEXTURE_2D_ARRAY; import static android.opengl.GLES30.GL_TEXTURE_3D; +import com.tencent.matrix.openglleak.hook.OpenGLHook; +import com.tencent.matrix.openglleak.utils.JavaStacktrace; import com.tencent.matrix.util.MatrixLog; import java.util.Arrays; @@ -30,9 +32,9 @@ public class MemoryInfo { private int usage; - private String javaStack = ""; + private JavaStacktrace.Trace javatrace; - private long nativeStackPtr; + private long nativeStackPtr = 0; private final OpenGLInfo.TYPE resType; @@ -76,7 +78,7 @@ public void releaseNativeStackPtr() { nativeStackPtr = 0; } - public void setTexturesInfo(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, int id, long eglContextId, long size, String javaStack, long nativeStackPtr) { + public void setTexturesInfo(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, int id, long eglContextId, long size, JavaStacktrace.Trace javatrace, long nativeStackPtr) { int faceId = getFaceId(target); if (faceId == -1) { MatrixLog.e("MicroMsg.OpenGLHook", "setTexturesInfo faceId = -1, target = " + target); @@ -84,10 +86,14 @@ public void setTexturesInfo(int target, int level, int internalFormat, int width } if (this.nativeStackPtr != 0) { - ResRecordManager.releaseNative(this.nativeStackPtr); + OpenGLHook.releaseNative(this.nativeStackPtr); } - this.javaStack = javaStack; + if (this.javatrace != null) { + javatrace.reduceReference(); + } + + this.javatrace = javatrace; this.nativeStackPtr = nativeStackPtr; FaceInfo faceInfo = faces[faceId]; @@ -116,11 +122,11 @@ public OpenGLInfo.TYPE getResType() { } public String getJavaStack() { - return javaStack; + return javatrace == null ? "" : javatrace.getContent(); } public String getNativeStack() { - return nativeStackPtr != 0 ? ResRecordManager.dumpNativeStack(nativeStackPtr) : ""; + return nativeStackPtr != 0 ? OpenGLHook.dumpNativeStack(nativeStackPtr) : ""; } public long getNativeStackPtr() { @@ -174,17 +180,26 @@ public int getUsage() { } - public void setBufferInfo(int target, int usage, int id, long eglContextId, long size, String javaStack, long nativeStackPtr) { + public void setBufferInfo(int target, int usage, int id, long eglContextId, long size, JavaStacktrace.Trace backtrace, long nativeStackPtr) { this.target = target; this.usage = usage; this.id = id; this.eglContextId = eglContextId; this.size = size; - this.javaStack = javaStack; + + if (this.javatrace != null) { + this.javatrace.reduceReference(); + } + + if (this.nativeStackPtr != 0) { + OpenGLHook.releaseNative(this.nativeStackPtr); + } + + this.javatrace = backtrace; this.nativeStackPtr = nativeStackPtr; } - public void setRenderbufferInfo(int target, int width, int height, int internalFormat, int id, long eglContextId, long size, String javaStack, long nativeStackPtr) { + public void setRenderbufferInfo(int target, int width, int height, int internalFormat, int id, long eglContextId, long size, JavaStacktrace.Trace backtrace, long nativeStackPtr) { this.target = target; this.width = width; this.height = height; @@ -192,10 +207,26 @@ public void setRenderbufferInfo(int target, int width, int height, int internalF this.id = id; this.eglContextId = eglContextId; this.size = size; - this.javaStack = javaStack; + + if (this.javatrace != null) { + this.javatrace.reduceReference(); + } + + if (this.nativeStackPtr != 0) { + OpenGLHook.releaseNative(this.nativeStackPtr); + } + + this.javatrace = backtrace; this.nativeStackPtr = nativeStackPtr; } + public void releaseJavaStacktrace() { + if (javatrace != null) { + this.javatrace.reduceReference(); + this.javatrace = null; + } + } + @Override public String toString() { return "MemoryInfo{" + @@ -206,11 +237,12 @@ public String toString() { ", id=" + id + ", eglContextId=" + eglContextId + ", usage=" + usage + - ", javaStack='" + javaStack + '\'' + + ", javaStack='" + getJavaStack() + '\'' + ", nativeStack='" + getNativeStack() + '\'' + ", resType=" + resType + ", size=" + getSize() + ", faces=" + Arrays.toString(faces) + '}'; } + } diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/OpenGLInfo.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/OpenGLInfo.java index 5708a42e1..fe72fe47c 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/OpenGLInfo.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/OpenGLInfo.java @@ -1,8 +1,8 @@ package com.tencent.matrix.openglleak.statistics.resource; -import android.opengl.EGLContext; -import android.os.Build; import com.tencent.matrix.openglleak.utils.ActivityRecorder; +import com.tencent.matrix.openglleak.utils.JavaStacktrace; + import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -10,68 +10,65 @@ public class OpenGLInfo { private String threadId = ""; private final int id; - private String javaStack = ""; private String nativeStack = ""; + private JavaStacktrace.Trace javatrace; private long nativeStackPtr = 0L; private final TYPE type; private MemoryInfo memoryInfo; - private EGLContext eglContext; + private final long eglContext; + private final long eglDrawSurface; + private final long eglReadSurface; private ActivityRecorder.ActivityInfo activityInfo; private AtomicInteger counter; public enum TYPE { - TEXTURE, BUFFER, FRAME_BUFFERS, RENDER_BUFFERS + TEXTURE, BUFFER, FRAME_BUFFERS, RENDER_BUFFERS, EGL_CONTEXT } public OpenGLInfo(OpenGLInfo clone) { this.threadId = clone.threadId; this.id = clone.id; this.eglContext = clone.eglContext; - this.javaStack = clone.javaStack; + this.javatrace = clone.javatrace; this.nativeStack = clone.nativeStack; this.nativeStackPtr = clone.nativeStackPtr; this.type = clone.type; this.activityInfo = clone.activityInfo; this.memoryInfo = clone.memoryInfo; + this.eglDrawSurface = clone.eglDrawSurface; + this.eglReadSurface = clone.eglReadSurface; } - public OpenGLInfo(TYPE type, int id, String threadId, EGLContext eglContext) { + public OpenGLInfo(TYPE type, int id, String threadId, long eglContext) { this.threadId = threadId; this.id = id; this.eglContext = eglContext; this.type = type; + this.eglDrawSurface = 0; + this.eglReadSurface = 0; } - public OpenGLInfo(TYPE type, int id, String threadId, EGLContext eglContext, String javaStack, long nativeStackPtr, ActivityRecorder.ActivityInfo activityInfo, AtomicInteger counter) { + public OpenGLInfo(TYPE type, int id, String threadId, long eglContext, long eglDrawSurface, long eglReadSurface, JavaStacktrace.Trace javatrace, long nativeStackPtr, ActivityRecorder.ActivityInfo activityInfo, AtomicInteger counter) { this.threadId = threadId; - this.javaStack = javaStack; + this.javatrace = javatrace; this.nativeStackPtr = nativeStackPtr; this.type = type; this.activityInfo = activityInfo; this.counter = counter; this.id = id; this.eglContext = eglContext; + this.eglDrawSurface = eglDrawSurface; + this.eglReadSurface = eglReadSurface; } public MemoryInfo getMemoryInfo() { return memoryInfo; } - public EGLContext getEglContext() { - return eglContext; - } - public long getEglContextNativeHandle() { - long eglContextNativeHandle = 0L; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (null != eglContext) { - eglContextNativeHandle = eglContext.getNativeHandle(); - } - } - - return eglContextNativeHandle; + return eglContext; } public void setMemoryInfo(MemoryInfo memoryInfo) { @@ -94,13 +91,20 @@ public TYPE getType() { } public String getJavaStack() { - return javaStack; + if (javatrace == null) { + return ""; + } + return javatrace.getContent(); } public String getNativeStack() { return ResRecordManager.getInstance().getNativeStack(this); } + public String getBriefNativeStack() { + return ResRecordManager.getInstance().getBriefNativeStack(this); + } + public AtomicInteger getCounter() { return counter; } @@ -117,6 +121,26 @@ public boolean isEglContextReleased() { return ResRecordManager.getInstance().isEglContextReleased(this); } + public void releaseJavaStacktrace() { + if (javatrace != null) { + this.javatrace.reduceReference(); + this.javatrace = null; + } + } + + public long getEglDrawSurface() { + return eglDrawSurface; + } + + public long getEglReadSurface() { + return eglReadSurface; + } + + + public boolean isEglSurfaceRelease() { + return ResRecordManager.getInstance().isEglSurfaceReleased(this); + } + @Override public String toString() { return "OpenGLInfo{" + @@ -126,7 +150,8 @@ public String toString() { ", threadId='" + threadId + '\'' + ", eglContextNativeHandle='" + getEglContextNativeHandle() + '\'' + ", eglContextReleased='" + isEglContextReleased() + '\'' + - ", javaStack='" + javaStack + '\'' + + ", eglSurfaceReleased='" + isEglSurfaceRelease() + '\'' + + ", javaStack='" + getJavaStack() + '\'' + ", nativeStack='" + getNativeStack() + '\'' + ", nativeStackPtr=" + nativeStackPtr + ", memoryInfo=" + memoryInfo + @@ -136,17 +161,17 @@ public String toString() { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || !(o instanceof OpenGLInfo)) return false; + if (!(o instanceof OpenGLInfo)) return false; OpenGLInfo that = (OpenGLInfo) o; return id == that.id && getEglContextNativeHandle() == that.getEglContextNativeHandle() && - threadId.equals(that.threadId) && + /*threadId.equals(that.threadId) &&*/ type == that.type; } @Override public int hashCode() { - return Objects.hash(id, getEglContextNativeHandle(), threadId, type); + return Objects.hash(id, getEglContextNativeHandle(), type); } } diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/ResRecordManager.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/ResRecordManager.java index f078479d8..9e3d6b0ef 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/ResRecordManager.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/statistics/resource/ResRecordManager.java @@ -2,8 +2,9 @@ import android.annotation.SuppressLint; +import com.tencent.matrix.openglleak.hook.OpenGLHook; import com.tencent.matrix.openglleak.utils.AutoWrapBuilder; -import com.tencent.matrix.openglleak.utils.EGLHelper; +import com.tencent.matrix.util.MatrixLog; import java.io.BufferedWriter; import java.io.File; @@ -14,18 +15,25 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +// TODO: 2022/12/22 should be deprecated and move to native public class ResRecordManager { + private static final String TAG = "Matrix.ResRecordManager"; private static final ResRecordManager mInstance = new ResRecordManager(); private final List mCallbackList = new LinkedList<>(); private final List mInfoList = new LinkedList<>(); - private final List mReleaseContext = new LinkedList<>(); + private final List mReleaseSurface = new LinkedList<>(); + + private final Map> mContextGroup = new HashMap<>(); + private final List mContextRecord = new ArrayList<>(); private ResRecordManager() { @@ -35,6 +43,38 @@ public static ResRecordManager getInstance() { return mInstance; } + public void createContext(Long shareContext, Long newContext) { + synchronized (mContextRecord) { + mContextRecord.add(newContext); + } + if (shareContext != 0) { + synchronized (mContextGroup) { + Set group = mContextGroup.get(shareContext); + if (group == null) { + group = new HashSet<>(); + mContextGroup.put(shareContext, group); + } + mContextGroup.put(newContext, group); + group.add(newContext); + group.add(shareContext); + } + } + } + + public void destroyContext(Long context) { + synchronized (mContextRecord) { + mContextRecord.remove(context); + } + synchronized (mContextGroup) { + if (mContextGroup.containsKey(context)) { + Set group = mContextGroup.remove(context); + if (group != null) { + group.remove(context); + } + } + } + } + public void gen(final OpenGLInfo gen) { if (gen == null) { return; @@ -53,15 +93,36 @@ public void gen(final OpenGLInfo gen) { } } - public void delete(final OpenGLInfo del) { + public void delete(OpenGLInfo del) { if (del == null) { return; } + OpenGLInfo infoDel; + + Set ctxGroup; + + synchronized (mContextGroup) { + ctxGroup = mContextGroup.get(del.getEglContextNativeHandle()); + } + synchronized (mInfoList) { // 之前可能释放过 int index = mInfoList.indexOf(del); + + if (-1 == index && ctxGroup != null) { // is shared context + for (Long ctx : ctxGroup) { + OpenGLInfo sharedInfo = new OpenGLInfo(del.getType(), del.getId(), del.getThreadId(), ctx); + index = mInfoList.indexOf(sharedInfo); + if (index != -1) { + MatrixLog.d(TAG, "del info found with shared context: %d, %s", index, ctx); + break; + } + } + } + if (-1 == index) { + MatrixLog.d(TAG, "del info not found"); return; } @@ -70,10 +131,13 @@ public void delete(final OpenGLInfo del) { return; } + infoDel = info; + AtomicInteger counter = info.getCounter(); counter.set(counter.get() - 1); if (counter.get() == 0) { - releaseNative(info.getNativeStackPtr()); + OpenGLHook.releaseNative(info.getNativeStackPtr()); + info.releaseJavaStacktrace(); } // 释放 memory info @@ -81,18 +145,19 @@ public void delete(final OpenGLInfo del) { if (null != memoryInfo) { long memNativePtr = memoryInfo.getNativeStackPtr(); if (memNativePtr != 0) { - releaseNative(memNativePtr); + OpenGLHook.releaseNative(memNativePtr); memoryInfo.releaseNativeStackPtr(); + memoryInfo.releaseJavaStacktrace(); } } - mInfoList.remove(del); + mInfoList.remove(info); } synchronized (mCallbackList) { for (Callback cb : mCallbackList) { if (null != cb) { - cb.delete(del); + cb.delete(infoDel); } } } @@ -129,16 +194,35 @@ public String getNativeStack(OpenGLInfo item) { } long nativeStackPtr = info.getNativeStackPtr(); if (nativeStackPtr != 0L) { - ret = dumpNativeStack(nativeStackPtr); + ret = OpenGLHook.dumpNativeStack(nativeStackPtr); } return ret; } } - public static native String dumpNativeStack(long nativeStackPtr); + public String getBriefNativeStack(OpenGLInfo item) { + synchronized (mInfoList) { + // 之前可能释放过 + int index = mInfoList.indexOf(item); + if (-1 == index) { + return "res already released, can not get native stack"; + } + + String ret = ""; + + OpenGLInfo info = mInfoList.get(index); + if (info == null) { + return ret; + } + long nativeStackPtr = info.getNativeStackPtr(); + if (nativeStackPtr != 0L) { + ret = OpenGLHook.dumpBriefNativeStack(nativeStackPtr); + } - public static native void releaseNative(long nativeStackPtr); + return ret; + } + } protected void registerCallback(Callback callback) { if (null == callback) { @@ -173,29 +257,73 @@ public void clear() { synchronized (mInfoList) { mInfoList.clear(); } - synchronized (mReleaseContext) { - mReleaseContext.clear(); - } } public boolean isEglContextReleased(OpenGLInfo info) { - synchronized (mReleaseContext) { - long eglContextNativeHandle = info.getEglContextNativeHandle(); - if (0L == eglContextNativeHandle) { + synchronized (mContextRecord) { + return !mContextRecord.contains(info.getEglContextNativeHandle()); + } +// synchronized (mReleaseContext) { +// long eglContextNativeHandle = info.getEglContextNativeHandle(); +// if (0L == eglContextNativeHandle) { +// return true; +// } +// +// for (long item : mReleaseContext) { +// if (item == eglContextNativeHandle) { +// return true; +// } +// } +// +// boolean alive = OpenGLHook.isEglContextAlive(info.getEglContextNativeHandle()); +// if (!alive) { +// mReleaseContext.add(info.getEglContextNativeHandle()); +// } +// return !alive; +// } + } + + public boolean isEglSurfaceReleased(OpenGLInfo info) { + synchronized (mReleaseSurface) { + long eglDrawSurface = info.getEglDrawSurface(); + long eglReadSurface = info.getEglReadSurface(); + + boolean drawRelease = false; + boolean readRelease = false; + + if (eglReadSurface == 0L || eglDrawSurface == 0L) { return true; } - for (long item : mReleaseContext) { - if (item == eglContextNativeHandle) { - return true; + for (long item : mReleaseSurface) { + if (item == eglReadSurface) { + readRelease = true; + } + + if (item == eglDrawSurface) { + drawRelease = true; } } - boolean alive = EGLHelper.isEglContextAlive(info.getEglContext()); - if (!alive) { - mReleaseContext.add(info.getEglContextNativeHandle()); + if (readRelease && drawRelease) { + return true; + } + + if (!readRelease) { + readRelease = !OpenGLHook.isEglSurfaceAlive(eglReadSurface); + } + + if (!drawRelease) { + drawRelease = !OpenGLHook.isEglSurfaceAlive(eglDrawSurface); } - return !alive; + + if (readRelease) { + mReleaseSurface.add(eglReadSurface); + } + if (drawRelease) { + mReleaseSurface.add(eglDrawSurface); + } + return readRelease && drawRelease; } } @@ -236,6 +364,8 @@ private String getResListString(List resList) { AutoWrapBuilder result = new AutoWrapBuilder(); for (OpenGLDumpInfo report : resList) { result.append(String.format(" alloc count = %d", report.getAllocCount())) + .append(String.format(" egl context is release = %s", report.innerInfo.isEglContextReleased())) + .append(String.format(" egl surface is release = %s", report.innerInfo.isEglSurfaceRelease())) .append(String.format(" total size = %s", report.getTotalSize())) .append(String.format(" id = %s", report.getAllocIdList())) .append(String.format(" activity = %s", report.innerInfo.getActivityInfo().name)) @@ -270,7 +400,10 @@ public String dumpGLToString() { int memoryJavaHash = memoryInfo == null ? 0 : memoryInfo.getJavaStack().hashCode(); int memoryNativeHash = memoryInfo == null ? 0 : memoryInfo.getNativeStack().hashCode(); - long infoHash = javaHash + nativeHash + memoryNativeHash + memoryJavaHash; + int isEGLRelease = info.isEglContextReleased() ? 1 : 0; + + long infoHash = javaHash + nativeHash + memoryNativeHash + memoryJavaHash + + info.getEglContextNativeHandle() + info.getActivityInfo().hashCode() + info.getThreadId().hashCode() + isEGLRelease; OpenGLDumpInfo oldInfo = infoMap.get(infoHash); if (oldInfo == null) { @@ -282,8 +415,9 @@ public String dumpGLToString() { boolean isSameThread = info.getThreadId().equals(oldInfo.innerInfo.getThreadId()); boolean isSameEglContext = info.getEglContextNativeHandle() == oldInfo.innerInfo.getEglContextNativeHandle(); boolean isSameActivity = info.getActivityInfo().equals(oldInfo.innerInfo.getActivityInfo()); + boolean isSameEGLStatus = info.isEglContextReleased() == oldInfo.innerInfo.isEglContextReleased(); - if (isSameType && isSameThread && isSameEglContext && isSameActivity) { + if (isSameType && isSameThread && isSameEglContext && isSameActivity && isSameEGLStatus) { oldInfo.incAllocRecord(info.getId()); if (oldInfo.innerInfo.getMemoryInfo() != null) { oldInfo.appendParamsInfos(info.getMemoryInfo()); @@ -298,6 +432,7 @@ public String dumpGLToString() { List bufferList = new ArrayList<>(); List framebufferList = new ArrayList<>(); List renderbufferList = new ArrayList<>(); + List eglContextList = new ArrayList<>(); for (OpenGLDumpInfo reportInfo : infoMap.values()) { if (reportInfo.innerInfo.getType() == OpenGLInfo.TYPE.TEXTURE) { @@ -312,6 +447,9 @@ public String dumpGLToString() { if (reportInfo.innerInfo.getType() == OpenGLInfo.TYPE.RENDER_BUFFERS) { renderbufferList.add(reportInfo); } + if (reportInfo.innerInfo.getType() == OpenGLInfo.TYPE.EGL_CONTEXT) { + eglContextList.add(reportInfo); + } } Comparator comparator = new Comparator() { @@ -331,6 +469,7 @@ public int compare(OpenGLDumpInfo o1, OpenGLDumpInfo o2) { Collections.sort(bufferList, comparator); Collections.sort(framebufferList, comparator); Collections.sort(renderbufferList, comparator); + Collections.sort(eglContextList, comparator); AutoWrapBuilder builder = new AutoWrapBuilder(); builder.appendDotted() @@ -338,6 +477,7 @@ public int compare(OpenGLDumpInfo o1, OpenGLDumpInfo o2) { .appendWithSpace(String.format("buffer Count = %d", bufferList.size()), 3) .appendWithSpace(String.format("framebuffer Count = %d", framebufferList.size()), 3) .appendWithSpace(String.format("renderbuffer Count = %d", renderbufferList.size()), 3) + .appendWithSpace(String.format("egl context Count = %d", eglContextList.size()), 3) .appendDotted() .appendWave() .appendWithSpace("texture part :", 3) @@ -351,6 +491,10 @@ public int compare(OpenGLDumpInfo o1, OpenGLDumpInfo o2) { .appendWithSpace("renderbuffer part :", 3) .appendWave() .append(getResListString(renderbufferList)) + .appendWave() + .appendWithSpace("egl context part :", 3) + .appendWave() + .append(getResListString(eglContextList)) .wrap(); return builder.toString(); diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/ActivityRecorder.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/ActivityRecorder.java index 8a5c9dcf4..afe312fd8 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/ActivityRecorder.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/ActivityRecorder.java @@ -7,16 +7,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.tencent.matrix.openglleak.hook.OpenGLHook; +import com.tencent.matrix.util.MatrixLog; + import java.lang.reflect.Field; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Objects; public class ActivityRecorder implements Application.ActivityLifecycleCallbacks { + private static final String TAG = "matrix.ActivityRecorder"; + private static final ActivityRecorder mInstance = new ActivityRecorder(); - private final List mList = new LinkedList<>(); private ActivityRecorder() { } @@ -29,6 +31,7 @@ public void start(Application context) { Activity activity = getActivity(); if (null != activity) { currentActivityInfo = new ActivityInfo(activity.hashCode(), activity.getLocalClassName()); + OpenGLHook.getInstance().updateCurrActivity(currentActivityInfo.toString()); } context.registerActivityLifecycleCallbacks(this); } @@ -46,7 +49,7 @@ public ActivityInfo getCurrentActivityInfo() { @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) { currentActivityInfo = new ActivityInfo(activity.hashCode(), activity.getLocalClassName()); - mList.add(currentActivityInfo); + OpenGLHook.getInstance().updateCurrActivity(currentActivityInfo.toString()); } @Override @@ -57,6 +60,7 @@ public void onActivityStarted(@NonNull Activity activity) { @Override public void onActivityResumed(@NonNull Activity activity) { currentActivityInfo = new ActivityInfo(activity.hashCode(), activity.getLocalClassName()); + OpenGLHook.getInstance().updateCurrActivity(currentActivityInfo.toString()); } @Override @@ -76,7 +80,6 @@ public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bun @Override public void onActivityDestroyed(@NonNull Activity activity) { - mList.remove(new ActivityInfo(activity.hashCode(), activity.getLocalClassName())); } public static Activity getActivity() { @@ -104,6 +107,23 @@ public static Activity getActivity() { return null; } + + public static ActivityInfo revertActivityInfo(String infoStr) { + if (infoStr == null || infoStr.isEmpty()) { + return new ActivityInfo(-1, "null"); + } + + try { + String[] result = infoStr.split(" : "); + int hash = Integer.parseInt(result[0]); + String name = result[1]; + return new ActivityInfo(hash, name); + } catch (Throwable t) { + MatrixLog.printErrStackTrace(TAG, t, ""); + } + return new ActivityInfo(-1, ""); + } + public static class ActivityInfo { public int activityHashcode; public String name; @@ -129,10 +149,7 @@ public int hashCode() { @Override public String toString() { - return "ActivityInfo{" + - "activityHashcode=" + activityHashcode + - ", name='" + name + '\'' + - '}'; + return activityHashcode + " : " + name; } } } diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/EGLHelper.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/EGLHelper.java index ec074efd0..96f8cdbf1 100644 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/EGLHelper.java +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/EGLHelper.java @@ -20,29 +20,28 @@ public class EGLHelper { @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) - public static void initOpenGL() { + public static EGLContext initOpenGL() { mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { - return; + return null; } int[] version = new int[2]; if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { - return; + return null; } int[] eglConfigAttribList = new int[]{ EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_NONE }; int[] numEglConfigs = new int[1]; EGLConfig[] eglConfigs = new EGLConfig[1]; if (!EGL14.eglChooseConfig(mEGLDisplay, eglConfigAttribList, 0, eglConfigs, 0, eglConfigs.length, numEglConfigs, 0)) { - return; + return null; } mEglConfig = eglConfigs[0]; @@ -54,7 +53,7 @@ public static void initOpenGL() { mEglContext = EGL14.eglCreateContext(mEGLDisplay, mEglConfig, EGL14.EGL_NO_CONTEXT, eglContextAttribList, 0); if (mEglContext == EGL14.EGL_NO_CONTEXT) { - return; + return null; } int[] surfaceAttribList = { @@ -66,30 +65,75 @@ public static void initOpenGL() { // Java 线程不进行实际绘制,因此创建 PbufferSurface 而非 WindowSurface mEglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEglConfig, surfaceAttribList, 0); if (mEglSurface == EGL14.EGL_NO_SURFACE) { - return; + return null; } if (!EGL14.eglMakeCurrent(mEGLDisplay, mEglSurface, mEglSurface, mEglContext)) { - return; + return null; } GLES20.glFlush(); + return mEglContext; } - public static boolean isEglContextAlive(EGLContext context) { + + public static EGLContext initOpenGLSharedContext(EGLContext shareContext) { + if (shareContext == null) { + return null; + } + + EGLDisplay eGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (eGLDisplay == EGL14.EGL_NO_DISPLAY) { + return null; + } + + int[] version = new int[2]; + if (!EGL14.eglInitialize(eGLDisplay, version, 0, version, 1)) { + return null; + } + + int[] eglConfigAttribList = new int[]{ + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_NONE + }; + int[] numEglConfigs = new int[1]; + EGLConfig[] eglConfigs = new EGLConfig[1]; + if (!EGL14.eglChooseConfig(eGLDisplay, eglConfigAttribList, 0, eglConfigs, 0, + eglConfigs.length, numEglConfigs, 0)) { + return null; + } + int[] eglContextAttribList = new int[]{ EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE }; - EGLContext testContext = EGL14.eglCreateContext(mEGLDisplay, mEglConfig, context, eglContextAttribList, 0); - if (testContext == EGL14.EGL_NO_CONTEXT) { - return false; + EGLContext eglContext = EGL14.eglCreateContext(eGLDisplay, eglConfigs[0], shareContext, eglContextAttribList, 0); + + if (eglContext == EGL14.EGL_NO_CONTEXT) { + return null; } - EGL14.eglDestroyContext(mEGLDisplay, testContext); + int[] surfaceAttribList = { + EGL14.EGL_WIDTH, 64, + EGL14.EGL_HEIGHT, 64, + EGL14.EGL_NONE + }; - return true; - } + // Java 线程不进行实际绘制,因此创建 PbufferSurface 而非 WindowSurface + mEglSurface = EGL14.eglCreatePbufferSurface(eGLDisplay, eglConfigs[0], surfaceAttribList, 0); + if (mEglSurface == EGL14.EGL_NO_SURFACE) { + return null; + } + + if (!EGL14.eglMakeCurrent(eGLDisplay, mEglSurface, mEglSurface, eglContext)) { + return null; + } + + GLES20.glFlush(); + return eglContext; + } } diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/ExecuteCenter.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/ExecuteCenter.java deleted file mode 100644 index 13aa03159..000000000 --- a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/ExecuteCenter.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.tencent.matrix.openglleak.utils; - -import android.os.Handler; - -public class ExecuteCenter { - - private static final ExecuteCenter mInstance = new ExecuteCenter(); - - private final Handler mHandler; - - public static ExecuteCenter getInstance() { - return mInstance; - } - - private ExecuteCenter() { - mHandler = new Handler(GlLeakHandlerThread.getInstance().getLooper()); - } - - public void post(Runnable runnable) { - mHandler.post(runnable); - } - - public void postDelay(Runnable runnable, long million) { - mHandler.postDelayed(runnable, million); - } - -} diff --git a/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/JavaStacktrace.java b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/JavaStacktrace.java new file mode 100644 index 000000000..6d29e3414 --- /dev/null +++ b/matrix/matrix-android/matrix-opengl-leak/src/main/java/com/tencent/matrix/openglleak/utils/JavaStacktrace.java @@ -0,0 +1,101 @@ +package com.tencent.matrix.openglleak.utils; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +// todo: deprecated and move to native +public class JavaStacktrace { + + private static final Map sThrowableMap = new ConcurrentHashMap<>(); + + private static final Map sString2Trace = new ConcurrentHashMap<>(); + + private static int sCollision = 0; + + private JavaStacktrace() { + } + + public static int getBacktraceKey() { + Throwable throwable = new Throwable(); + int key = throwable.hashCode(); + if (sThrowableMap.get(key) != null) { + sCollision++; + } + sThrowableMap.put(key, throwable); + return key; + } + + public static Trace getBacktraceValue(int key) { + Throwable throwable = sThrowableMap.get(key); + if (throwable == null) { + return new Trace(); + } + String traceKey = android.util.Log.getStackTraceString(throwable); //stackTraceToString(throwable.getStackTrace()); + Trace mapTrace = sString2Trace.get(traceKey); + if (mapTrace == null) { + Trace resultTrace = new Trace(traceKey); + resultTrace.addReference(); + sString2Trace.put(traceKey, resultTrace); + sThrowableMap.remove(key); + return resultTrace; + } else { + sThrowableMap.remove(key); + mapTrace.addReference(); + return mapTrace; + } + } + + public static int getCollision() { + return sCollision; + } + + private static String stackTraceToString(StackTraceElement[] arr) { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < arr.length; i++) { + if (i == 0) { + continue; + } + StackTraceElement element = arr[i]; + String className = element.getClassName(); + if (className.contains("java.lang.Thread")) { + continue; + } + sb.append("\t").append(element).append('\n'); + } + return sb.toString(); + } + + public static class Trace { + + private final String content; + + private int refCount = 0; + + public Trace() { + content = ""; + } + + public Trace(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + public void addReference() { + refCount++; + } + + public void reduceReference() { + refCount--; + if (refCount == 0) { + sString2Trace.remove(this.content); + } + } + + } + + +} diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/model/ActivityLeakResult.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/model/ActivityLeakResult.java index 51fc16420..90ecd2537 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/model/ActivityLeakResult.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/model/ActivityLeakResult.java @@ -105,7 +105,7 @@ public String toString() { StringBuilder sb = new StringBuilder("Leak Reference:"); if (referenceChain != null) { for (ReferenceTraceElement element : referenceChain.elements) { - sb.append(element.toString()).append(";"); + sb.append(element.toCollectableString()).append(";"); } } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/model/ReferenceTraceElement.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/model/ReferenceTraceElement.java index c949cc4e3..0e865ef7e 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/model/ReferenceTraceElement.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-analyzer/src/main/java/com/tencent/matrix/resource/analyzer/model/ReferenceTraceElement.java @@ -79,6 +79,14 @@ public ReferenceTraceElement(String referenceName, Type type, Holder holder, Str @Override public String toString() { + return toString(false); + } + + public String toCollectableString() { + return toString(true); + } + + private String toString(final boolean collectable) { String string = ""; if (type == STATIC_FIELD) { @@ -92,7 +100,11 @@ public String toString() { string += className; if (referenceName != null) { - string += " " + referenceName; + if (collectable && holder == ARRAY) { + string += " [*]"; + } else { + string += " " + referenceName; + } } else { string += " instance"; } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/build.gradle b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/build.gradle index ee46999a2..718046426 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/build.gradle +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/build.gradle @@ -1,4 +1,7 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +apply from: rootProject.file('gradle/WeChatNativeDepend.gradle') android { compileSdkVersion rootProject.ext.compileSdkVersion @@ -10,6 +13,7 @@ android { versionCode 1 versionName rootProject.ext.VERSION_NAME } + buildTypes { release { minifyEnabled false @@ -17,17 +21,26 @@ android { consumerProguardFiles 'proguard-rules.pro' } } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + } + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.12' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$gradle.KOTLIN_VERSION" implementation project(':matrix-resource-canary:matrix-resource-canary-common') implementation "com.tencent.tinker:tinker-ziputils:1.9.2" implementation project(':matrix-android-lib') + implementation project(':matrix-android-commons') + implementation project(':matrix-backtrace') implementation project(':matrix-resource-canary:matrix-resource-canary-analyzer') - implementation project(':matrix-memory-dump') + + testImplementation 'junit:junit:4.12' } version = rootProject.ext.VERSION_NAME @@ -38,7 +51,6 @@ if("External" == rootProject.ext.PUBLISH_CHANNEL) { } else { apply from: rootProject.file('gradle/WeChatPublish.gradle') - apply from: rootProject.file('gradle/WeChatNativeDepend.gradle') wechatPublish { artifactId = POM_ARTIFACT_ID } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/proguard-rules.pro b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/proguard-rules.pro index e48ad5895..af7b32e3e 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/proguard-rules.pro +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/proguard-rules.pro @@ -25,3 +25,5 @@ #-renamesourcefileattribute SourceFile -keep class com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo { *; } +-keep class com.tencent.matrix.resource.MemoryUtil$TaskResult { *; } +-keep class com.tencent.matrix.resource.MemoryUtil$*Exception { *; } \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/AndroidManifest.xml b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/AndroidManifest.xml index 06cb9f3d3..57035e05c 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/AndroidManifest.xml +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/CMakeLists.txt b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..a39be7f92 --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.10.2) +project(matrix-resource-canary-android) + +set(CMAKE_CXX_STANDARD 17) + +add_subdirectory(memory_util) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-dump/CMakeLists.txt b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/CMakeLists.txt similarity index 57% rename from matrix/matrix-android/matrix-memory-dump/CMakeLists.txt rename to matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/CMakeLists.txt index cda06177a..f30bbc8a4 100644 --- a/matrix/matrix-android/matrix-memory-dump/CMakeLists.txt +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/CMakeLists.txt @@ -1,31 +1,34 @@ cmake_minimum_required(VERSION 3.10.2) +project(matrix-resource-canary-android) -project("matrix-memorydump") +set(CMAKE_CXX_STANDARD 17) -add_library( - matrix-memorydump - SHARED - src/main/cpp/mem_dump.cpp - src/main/cpp/dlfcn/self_dlfcn.cpp) +add_subdirectory(../../../../../../matrix-hprof-analyzer matrix-hprof-analyzer.out) + +add_library(matrix_mem_util SHARED + excludes/excludes.cpp symbol/dlsym/dlsym.cpp symbol/symbol.cpp log.cpp memory_util.cpp) find_library( log-lib log) target_include_directories( - matrix-memorydump + matrix_mem_util PRIVATE ${EXT_DEP}/include PRIVATE ${EXT_DEP}/include/backtrace/common) target_link_libraries( - matrix-memorydump + matrix_mem_util + matrix_hprof_analyzer ${log-lib} ${EXT_DEP}/lib/${ANDROID_ABI}/libenhance_dlsym.a ${EXT_DEP}/lib/${ANDROID_ABI}/libsemi_dlfcn.a) if(${ANDROID_ABI} MATCHES "armeabi-v7a" OR ${ANDROID_ABI} MATCHES "arm64-v8a") target_link_libraries( - matrix-memorydump + matrix_mem_util ${EXT_DEP}/lib/${ANDROID_ABI}/libwechatbacktrace.so ) -endif() \ No newline at end of file +endif() + + diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/excludes/excludes.cpp b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/excludes/excludes.cpp new file mode 100644 index 000000000..a18c3da4c --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/excludes/excludes.cpp @@ -0,0 +1,963 @@ +#include "excludes.h" + +#include + +#ifndef __ANDROID_API_I_MR1__ +#define __ANDROID_API_I_MR1__ 15 +#endif + +#define MANUFACTURER_SAMSUNG "samsung" +#define MANUFACTURER_LENOVO "LENOVO" +#define MANUFACTURER_VIVO "vivo" +#define MANUFACTURER_NVIDIA "NVIDIA" +#define MANUFACTURER_LG "LGE" +#define MANUFACTURER_MEIZU "Meizu" +#define MANUFACTURER_MOTOROLA "motorola" +#define MANUFACTURER_HUAWEI "HUAWEI" +#define MANUFACTURER_SHARP "SHARP" +#define MANUFACTURER_ONEPLUS "OnePlus" +#define MANUFACTURER_RAZER "Razer" + +bool exclude_default_references(HprofAnalyzer &analyzer) { + + const int sdk_version = android_get_device_api_level(); + if (sdk_version < 0) return false; + + const std::string manufacturer = ({ + char value[PROP_VALUE_MAX]; + if (__system_property_get("ro.product.manufacturer", value) < -1) return false; + std::string result(value); + result; + }); + + /* + * reason: + * + * Android Q added a new android.app.IRequestFinishCallback$Stub class. android.app.Activity + * creates an implementation of that interface as an anonymous subclass. That anonymous subclass + * has a reference to the activity. Another process is keeping the + * android.app.IRequestFinishCallback$Stub reference alive long after Activity.onDestroyed() has + * been called, causing the activity to leak. + * + * Fix: You can "fix" this leak by overriding Activity.onBackPressed() and calling + * Activity.finishAfterTransition() instead of super if the activity is task root and the + * fragment stack is empty. + * + * Tracked here: https://issuetracker.google.com/issues/139738913 + */ + if (sdk_version == __ANDROID_API_Q__) { + analyzer.ExcludeInstanceFieldReference("android.app.Activity$1", "this$0"); + } + + /* + * reason: + * + * Android AOSP sometimes keeps a reference to a destroyed activity as a nextIdle client record + * in the android.app.ActivityThread.mActivities map. + * + * Not sure what's going on there, input welcome. + */ + if (sdk_version >= __ANDROID_API_K__ && sdk_version <= __ANDROID_API_O_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.app.ActivityThread$ActivityClientRecord", "nextIdle"); + } + + /* + * reason: + * + * Editor inserts a special span, which has a reference to the EditText. That span is a + * NoCopySpan, which makes sure it gets dropped when creating a new SpannableStringBuilder from + * a given CharSequence. TextView.onSaveInstanceState() does a copy of its mText before saving + * it in the bundle. Prior to KitKat, that copy was done using the SpannableString constructor, + * instead of SpannableStringBuilder. The SpannableString constructor does not drop NoCopySpan + * spans. So we end up with a saved state that holds a reference to the textview and therefore + * the entire view hierarchy & activity context. + * + * Fix: https://github.com/android/platform_frameworks_base/commit/af7dcdf35a37d7a7dbaad7d9869c1c91bce2272b + * + * To fix this, you could override TextView.onSaveInstanceState(), and then use reflection to + * access TextView.SavedState.mText and clear the NoCopySpan spans. + */ + if (sdk_version <= __ANDROID_API_K__) { + analyzer.ExcludeInstanceFieldReference( + "android.widget.Editor$EasyEditSpanController", "this$0"); + analyzer.ExcludeInstanceFieldReference( + "android.widget.Editor$SpanController", "this$0"); + } + + /* + * reason: + * + * MediaSessionLegacyHelper is a static singleton that is lazily instantiated and keeps a + * reference to the context it's given the first time MediaSessionLegacyHelper.getHelper() is + * called. This leak was introduced in android-5.0.1_r1 and fixed in Android 5.1.0_r1 by calling + * context.getApplicationContext(). + * + * Fix: https://github.com/android/platform_frameworks_base/commit/9b5257c9c99c4cb541d8e8e78fb04f008b1a9091 + * + * To fix this, you could call MediaSessionLegacyHelper.getHelper() early in + * Application.onCreate() and pass it the application context. + */ + if (sdk_version == __ANDROID_API_L__) { + analyzer.ExcludeStaticFieldReference( + "android.media.session.MediaSessionLegacyHelper", "sInstance"); + } + + /* + * reason: + * + * TextLine.sCached is a pool of 3 TextLine instances. TextLine.recycle() has had at least two + * bugs that created memory leaks by not correctly clearing the recycled TextLine instances. + * + * The first was fixed in android-5.1.0_r1: + * https://github.com/android/platform_frameworks_base/commit/893d6fe48d37f71e683f722457bea646994a10 + * + * The second was fixed, not released yet: + * https://github.com/android/platform_frameworks_base/commit/b3a9bc038d3a218b1dbdf7b5668e3d6c12be5e + * + * To fix this, you could access TextLine.sCached and clear the pool every now and then (e.g. on + * activity destroy). + */ + if (sdk_version <= __ANDROID_API_L_MR1__) { + analyzer.ExcludeStaticFieldReference("android.text.TextLine", "sCached"); + } + + /* + * reason: + * + * Prior to ART, a thread waiting on a blocking queue will leak the last dequeued object as a + * stack local reference. So when a HandlerThread becomes idle, it keeps a local reference to + * the last message it received. That message then gets recycled and can be used again. As long + * as all messages are recycled after being used, this won't be a problem, because these + * references are cleared when being recycled. However, dialogs create template Message + * instances to be copied when a message needs to be sent. These Message templates holds + * references to the dialog listeners, which most likely leads to holding a reference onto the + * activity in some way. Dialogs never recycle their template Message, assuming these Message + * instances will get GCed when the dialog is GCed. + * + * The combination of these two things creates a high potential for memory leaks as soon as you + * use dialogs. These memory leaks might be temporary, but some handler threads sleep for a long + * time. + * + * To fix this, you could post empty messages to the idle handler threads from time to time. + * This won't be easy because you cannot access all handler threads, but a library that is + * widely used should consider doing this for its own handler threads. + */ + analyzer.ExcludeInstanceFieldReference("android.os.Message", "obj"); + if (sdk_version <= __ANDROID_API_L__) { + analyzer.ExcludeInstanceFieldReference("android.os.Message", "next"); + analyzer.ExcludeInstanceFieldReference("android.os.Message", "target"); + } + + /* + * reason: + * + * When we detach a view that receives keyboard input, the InputMethodManager leaks a reference + * to it until a new view asks for keyboard input. + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=171190 + * + * Hack: https://gist.github.com/pyricau/4df64341cc978a7de414 + */ + if (sdk_version >= __ANDROID_API_I_MR1__ && sdk_version <= __ANDROID_API_O_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.view.inputmethod.InputMethodManager", "mNextServedView"); + analyzer.ExcludeInstanceFieldReference( + "android.view.inputmethod.InputMethodManager", "mServedView"); + analyzer.ExcludeInstanceFieldReference( + "android.view.inputmethod.InputMethodManager", "mServedInputConnection"); + } + + /* + * reason: + * + * The singleton InputMethodManager is holding a reference to mCurRootView long after the + * activity has been destroyed. + * + * Observed on ICS MR1: https://github.com/square/leakcanary/issues/1#issuecomment-100579429 + * + * Hack: https://gist.github.com/pyricau/4df64341cc978a7de414 + */ + if ((sdk_version >= __ANDROID_API_I_MR1__ && sdk_version <= __ANDROID_API_M__) + || + (manufacturer == MANUFACTURER_HUAWEI && + sdk_version >= __ANDROID_API_M__ && sdk_version <= __ANDROID_API_P__)) { + analyzer.ExcludeInstanceFieldReference( + "android.view.inputmethod.InputMethodManager", "mCurRootView"); + } + + /* + * reason: + * + * Android Q Beta has a leak where InputMethodManager.mImeInsetsConsumer isn't set to null when + * the activity is destroyed. + */ + if (sdk_version == __ANDROID_API_P__) { + analyzer.ExcludeInstanceFieldReference( + "android.view.inputmethod.InputMethodManager", "mImeInsetsConsumer"); + } + + /* + * reason: + * + * In Android Q Beta InputMethodManager keeps its EditableInputConnection after the activity has + * been destroyed. + */ + if (sdk_version >= __ANDROID_API_P__ && sdk_version <= __ANDROID_API_Q__) { + analyzer.ExcludeInstanceFieldReference( + "android.view.inputmethod.InputMethodManager", "mCurrentInputConnection"); + } + + /* + * reason: + * + * LayoutTransition leaks parent ViewGroup through ViewTreeObserver.OnPreDrawListener when + * triggered, this leaks stays until the window is destroyed. + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=171830 + */ + if (sdk_version >= __ANDROID_API_I__ && sdk_version <= __ANDROID_API_L_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.animation.LayoutTransition$1", "val$parent"); + } + + /* + * reason: + * + * SpellCheckerSessionListenerImpl.mHandler is leaking destroyed Activity when the + * SpellCheckerSession is closed before the service is connected. + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=172542 + */ + if (sdk_version >= __ANDROID_API_J__ && sdk_version <= __ANDROID_API_N__) { + analyzer.ExcludeInstanceFieldReference( + "android.view.textservice.SpellCheckerSession$1", "this$0"); + } + + /* + * reason: + * + * SpellChecker holds on to a detached view that points to a destroyed activity. mSpellRunnable + * is being enqueued, and that callback should be removed when closeSession() is called. Maybe + * closeSession() wasn't called, or maybe it was called after the view was detached. + */ + if (sdk_version == __ANDROID_API_L_MR1__) { + analyzer.ExcludeInstanceFieldReference("android.widget.SpellChecker$1", "this$0"); + } + + /* + * reason: + * + * ActivityChooserModel holds a static reference to the last set ActivityChooserModelPolicy + * which can be an activity context. + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=172659 + * + * Hack: https://gist.github.com/andaag/b05ab66ed0f06167d6e0 + */ + analyzer.ExcludeInstanceFieldReference( + "android.widget.ActivityChooserModel", + "mActivityChoserModelPolicy"); + if (sdk_version > __ANDROID_API_I__ && sdk_version <= __ANDROID_API_L_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.support.v7.internal.widget.ActivityChooserModel", + "mActivityChoserModelPolicy"); + } + + /* + * reason: + * + * MediaProjectionCallback is held by another process, and holds on to MediaProjection which has + * an activity as its context. + */ + if (sdk_version >= __ANDROID_API_L_MR1__ && sdk_version <= __ANDROID_API_P__) { + analyzer.ExcludeInstanceFieldReference( + "android.media.projection.MediaProjection$MediaProjectionCallback", + "this$0"); + } + + /* + * reason: + * + * Prior to Android 5, SpeechRecognizer.InternalListener was a non static inner class and leaked + * the SpeechRecognizer which leaked an activity context. + * + * Fixed in AOSP: https://github.com/android/platform_frameworks_base/commit/b37866db469e81aca534ff6186bdafd44352329b + */ + if (sdk_version < __ANDROID_API_L__) { + analyzer.ExcludeInstanceFieldReference( + "android.speech.SpeechRecognizer$InternalListener", "this$0"); + } + + /* + * reason: + * + * AccountManager$AmsTask$Response is a stub and is held in memory by native code, probably + * because the reference to the response in the other process hasn't been cleared. + * + * AccountManager$AmsTask is holding on to the activity reference to use for launching a new + * sub-Activity. + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=173689 + * + * Fix: Pass a null activity reference to the AccountManager methods and then deal with the + * returned future to to get the result and correctly start an activity when it's available. + */ + if (sdk_version <= __ANDROID_API_O_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.accounts.AccountManager$AmsTask$Response", "this$1"); + } + + /* + * reason: + * + * The static method MediaScannerConnection.scanFile() takes an activity context but the service + * might not disconnect after the activity has been destroyed. + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=173788 + * + * Fix: Create an instance of MediaScannerConnection yourself and pass in the application + * context. Call connect() and disconnect() manually. + */ + if (sdk_version <= __ANDROID_API_L_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.media.MediaScannerConnection", "mContext"); + } + + /* + * reason: + * + * UserManager has a static sInstance field that creates an instance and caches it the first + * time UserManager.get() is called. This instance is created with the outer context (which is + * an activity base context). + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=173789 + * + * Introduced by: https://github.com/android/platform_frameworks_base/commit/27db46850b708070452c0ce49daf5f79503fbde6 + * + * Fix: trigger a call to UserManager.get() in Application.onCreate(), so that the UserManager + * instance gets cached with a reference to the application context. + */ + if (sdk_version >= __ANDROID_API_J__ && sdk_version <= __ANDROID_API_N_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.os.UserManager", "mContext"); + } + + /* + * reason: + * + * android.appwidget.AppWidgetHost$Callbacks is a stub and is held in memory native code. The + * reference to the `mContext` was not being cleared, which caused the Callbacks instance to + * retain this reference + * + * Fixed in AOSP: https://github.com/android/platform_frameworks_base/commit/7a96f3c917e0001ee739b65da37b2fadec7d7765 + */ + if (sdk_version < __ANDROID_API_L_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.appwidget.AppWidgetHost$Callbacks", "this$0"); + } + + /* + * reason: + * + * Prior to Android M, VideoView required audio focus from AudioManager and never abandoned it, + * which leaks the Activity context through the AudioManager. The root of the problem is that + * AudioManager uses whichever context it receives, which in the case of the VideoView example + * is an Activity, even though it only needs the application's context. The issue is fixed in + * Android M, and the AudioManager now uses the application's context. + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=152173 + * + * Fix: https://gist.github.com/jankovd/891d96f476f7a9ce24e2 + */ + if (sdk_version <= __ANDROID_API_M__) { + analyzer.ExcludeInstanceFieldReference( + "android.media.AudioManager$1", "this$0"); + } + + /* + * reason: + * + * The EditText Blink of the Cursor is implemented using a callback and Messages, which trigger + * the display of the Cursor. If an AlertDialog or DialogFragment that contains a blinking + * cursor is detached, a message is posted with a delay after the dialog has been closed and as + * a result leaks the Activity. + * + * This can be fixed manually by calling TextView.setCursorVisible(false) in the dismiss() + * method of the dialog. + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=188551 + * + * Fixed in AOSP: https://android.googlesource.com/platform/frameworks/base/+/5b734f2430e9f26c769d6af8ea5645e390fcf5af%5E%21/ + */ + if (sdk_version <= __ANDROID_API_L_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.widget.Editor$Blink", "this$0"); + } + + /* + * reason: + * + * ConnectivityManager has a sInstance field that is set when the first ConnectivityManager + * instance is created. ConnectivityManager has a mContext field. When calling + * activity.getSystemService(Context.CONNECTIVITY_SERVICE), the first ConnectivityManager + * instance is created with the activity context and stored in sInstance. That activity context + * then leaks forever. + * + * Until this is fixed, app developers can prevent this leak by making sure the + * ConnectivityManager is first created with an App Context. E.g. in some static init do: + * context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE) + * + * Tracked here: https://code.google.com/p/android/issues/detail?id=198852 + * + * Introduced here: https://github.com/android/platform_frameworks_base/commit/e0bef71662d81caaaa0d7214fb0bef5d39996a69 + */ + if (sdk_version <= __ANDROID_API_M__) { + analyzer.ExcludeInstanceFieldReference( + "android.net.ConnectivityManager", "sInstance"); + } + + /* + * reason: + * + * AccessibilityNodeInfo has a static sPool of AccessibilityNodeInfo. When AccessibilityNodeInfo + * instances are released back in the pool, AccessibilityNodeInfo.clear() does not clear the + * mOriginalText field, which causes spans to leak which in turns causes TextView.ChangeWatcher + * to leak and the whole view hierarchy. + * + * Introduced here: https://android.googlesource.com/platform/frameworks/base/+/193520e3dff5248ddcf8435203bf99d2ba667219%5E%21/core/java/android/view/accessibility/AccessibilityNodeInfo.java + */ + if (sdk_version >= __ANDROID_API_O__ && sdk_version <= __ANDROID_API_O_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.view.accessibility.AccessibilityNodeInfo", "mOriginalText"); + } + + /* + * reason: + * + * AssistStructure (google assistant / autofill) holds on to text spannables on the screen. + * TextView.ChangeWatcher and android.widget.Editor end up in spans and typically hold on to the + * view hierarchy. + */ + if (sdk_version >= __ANDROID_API_N__ && sdk_version <= __ANDROID_API_Q__) { + analyzer.ExcludeInstanceFieldReference( + "android.app.assist.AssistStructure$ViewNodeText", "mText"); + } + + /* + * reason: + * + * AccessibilityIterators holds on to text layouts which can hold on to spans + * TextView.ChangeWatcher and android.widget.Editor end up in spans and typically hold on to the + * view hierarchy. + */ + if (sdk_version == __ANDROID_API_O_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.widget.AccessibilityIterators$LineTextSegmentIterator", "mLayout"); + } + + /* + * reason: + * + * BiometricPrompt holds on to a FingerprintManager which holds on to a destroyed activity. + */ + if (sdk_version == __ANDROID_API_P__) { + analyzer.ExcludeInstanceFieldReference( + "android.hardware.biometrics.BiometricPrompt", "mFingerprintManager"); + } + + /* + * reason: + * + * android.widget.Magnifier.InternalPopupWindow registers a frame callback + * on android.view.ThreadedRenderer.SimpleRenderer which holds it as a native reference. + * android.widget.Editor$InsertionHandleView registers an OnOperationCompleteCallback on + * Magnifier.InternalPopupWindow. These references are held after the activity has been + * destroyed. + */ + if (sdk_version == __ANDROID_API_P__) { + analyzer.ExcludeInstanceFieldReference( + "android.widget.Magnifier$InternalPopupWindow", "mCallback"); + } + + /* + * reason: + * + * When BackdropFrameRenderer.releaseRenderer() is called, there's an unknown case where + * mRenderer becomes null but mChoreographer doesn't and the thread doesn't stop and ends up + * leaking mDecorView which itself holds on to a destroyed activity. + */ + if (sdk_version >= __ANDROID_API_N__ && sdk_version <= __ANDROID_API_O__) { + analyzer.ExcludeInstanceFieldReference( + "com.android.internal.policy.BackdropFrameRenderer", "mDecorView"); + } + + /* + * reason: + * + * In Android P, ViewLocationHolder has an mRoot field that is not cleared in its clear() + * method. + * + * Introduced in https://github.com/aosp-mirror/platform_frameworks_base/commit/86b326012813f09d8f1de7d6d26c986a909d + * + * Bug report: https://issuetracker.google.com/issues/112792715 + */ + if (sdk_version == __ANDROID_API_P__) { + analyzer.ExcludeInstanceFieldReference( + "android.view.ViewGroup$ViewLocationHolder", "mRoot"); + } + + /* + * reason: + * + * Android Q Beta added AccessibilityNodeIdManager which stores all views from their + * onAttachedToWindow() call, until detached. Unfortunately it's possible to trigger the view + * framework to call detach before attach (by having a view removing itself from its parent in + * onAttach, which then causes AccessibilityNodeIdManager to keep children view forever. Future + * releases of Q will hold weak references. + */ + if (sdk_version >= __ANDROID_API_P__ && sdk_version <= __ANDROID_API_Q__) { + analyzer.ExcludeInstanceFieldReference( + "android.view.accessibility.AccessibilityNodeIdManager", "mIdsToViews"); + } + + /* + * reason: + * + * TextToSpeech.shutdown() does not release its references to context objects. Furthermore, + * TextToSpeech instances cannot be garbage collected due to other process keeping the + * references, resulting the context objects leaked. + * + * Developers might be able to mitigate the issue by passing application context to TextToSpeech + * constructor. + * + * Tracked at: https://github.com/square/leakcanary/issues/1210 and https://issuetracker.google.com/issues/129250419 + */ + if (sdk_version == __ANDROID_API_N__) { + analyzer.ExcludeInstanceFieldReference("android.speech.tts.TextToSpeech", "mContext"); + analyzer.ExcludeInstanceFieldReference("android.speech.tts.TtsEngines", "mContext"); + } + + /* + * reason: + * + * ControlledInputConnectionWrapper is held by a global variable in native code. + */ + analyzer.ExcludeNativeGlobalReference( + "android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper"); + + /* + * reason: + * + * Toast.TN is held by a global variable in native code due to an IPC call to show the toast. + */ + analyzer.ExcludeNativeGlobalReference("android.widget.Toast$TN"); + + /* + * reason: + * + * In Android 11 DP 2 ApplicationPackageManager.HasSystemFeatureQuery was an inner class. + * + * Introduced in https://cs.android.com/android/_/android/platform/frameworks/base/+/89608118192580ffca026b5dacafa637a556d578 + * + * Fixed in https://cs.android.com/android/_/android/platform/frameworks/base/+/1f771846c51148b7cb6283e6dc82a216ffaa5353 + * + * Related blog: https://dev.to/pyricau/beware-packagemanager-leaks-223g + */ + if (sdk_version == __ANDROID_API_Q__) { + analyzer.ExcludeInstanceFieldReference( + "android.app.ApplicationPackageManager$HasSystemFeatureQuery", "this$0"); + } + + /* + * reason: + * + * SpenGestureManager has a static mContext field that leaks a reference to the activity. Yes, + * a STATIC mContext field. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_K__) { + analyzer.ExcludeStaticFieldReference( + "com.samsung.android.smartclip.SpenGestureManager", "mContext"); + } + + /* + * reason: + * + * ClipboardUIManager is a static singleton that leaks an activity context. + * + * Fix: trigger a call to ClipboardUIManager.getInstance() in Application.onCreate(), so that + * the ClipboardUIManager instance gets cached with a reference to the application context. + * + * Example: https://gist.github.com/cypressious/91c4fb1455470d803a602838dfcd5774 + */ + if (manufacturer == MANUFACTURER_SAMSUNG && + sdk_version >= __ANDROID_API_K__ && sdk_version <= __ANDROID_API_L__) { + analyzer.ExcludeInstanceFieldReference( + "android.sec.clipboard.ClipboardUIManager", "mContext"); + } + + /* + * reason: + * + * SemClipboardManager is held in memory by an anonymous inner class implementation of + * android.os.Binder, thereby leaking an activity context. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && + sdk_version >= __ANDROID_API_K__ && sdk_version <= __ANDROID_API_N__) { + analyzer.ExcludeInstanceFieldReference( + "com.samsung.android.content.clipboard.SemClipboardManager", "mContext"); + } + + /* + * reason: + * + * SemClipboardManager inner classes are held by native references due to IPC calls. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && + sdk_version >= __ANDROID_API_K__ && sdk_version <= __ANDROID_API_P__) { + analyzer.ExcludeNativeGlobalReference( + "com.samsung.android.content.clipboard.SemClipboardManager$1"); + analyzer.ExcludeNativeGlobalReference( + "com.samsung.android.content.clipboard.SemClipboardManager$3"); + } + + /* + * reason: + * + * android.sec.clipboard.ClipboardExManager$IClipboardDataPasteEventImpl$1 is a native callback + * that holds IClipboardDataPasteEventImpl which holds ClipboardExManager which has a destroyed + * activity as mContext. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_M__) { + analyzer.ExcludeInstanceFieldReference( + "android.sec.clipboard.ClipboardExManager", "mContext"); + } + + /* + * reason: + * + * android.sec.clipboard.ClipboardExManager$IClipboardDataPasteEventImpl$1 is a native callback + * that holds IClipboardDataPasteEventImpl which holds ClipboardExManager which holds + * PersonaManager which has a destroyed activity as mContext. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_M__) { + analyzer.ExcludeInstanceFieldReference( + "android.sec.clipboard.ClipboardExManager", "mPersonaManager"); + } + + /* + * reason: + * + * TextView$IClipboardDataPasteEventImpl$1 is held by a native ref, and + * IClipboardDataPasteEventImpl ends up leaking a detached textview. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_L_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.widget.TextView$IClipboardDataPasteEventImpl", "this$0"); + } + + /* + * reason: + * + * SemEmergencyManager is a static singleton that leaks a DecorContext. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && + sdk_version >= __ANDROID_API_K__ && sdk_version <= __ANDROID_API_N__) { + analyzer.ExcludeInstanceFieldReference( + "com.samsung.android.emergencymode.SemEmergencyManager", "mContext"); + } + + /* + * reason: + * + * Unknown. From LeakCanary. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_N__) { + analyzer.ExcludeInstanceFieldReference( + "com.samsung.android.knox.SemPersonaManager", "mContext"); + } + + /* + * reason: + * + * Unknown. From LeakCanary. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && + sdk_version >= __ANDROID_API_P__ && sdk_version <= __ANDROID_API_Q__) { + analyzer.ExcludeInstanceFieldReference( + "android.app.SemAppIconSolution", "mContext"); + } + + /* + * reason: + * + * AwResource#setResources() is called with resources that hold a reference to the activity + * context (instead of the application context) and doesn't clear it. + * + * Not sure what's going on there, input welcome. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_K__) { + analyzer.ExcludeStaticFieldReference( + "com.android.org.chromium.android_webview.AwResource", "sResources"); + } + + /* + * reason: + * + * mLastHoveredView is a static field in TextView that leaks the last hovered view. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && + sdk_version >= __ANDROID_API_K__ && sdk_version <= __ANDROID_API_Q__) { + analyzer.ExcludeStaticFieldReference("android.widget.TextView", "mLastHoveredView"); + } + + /* + * reason: + * + * android.app.LoadedApk.mResources has a reference to + * android.content.res.Resources.mPersonaManager which has a reference to + * android.os.PersonaManager.mContext which is an activity. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_K__) { + analyzer.ExcludeInstanceFieldReference("android.os.PersonaManager", "mContext"); + } + + /* + * reason: + * + * In AOSP the Resources class does not have a context. Here we have ZygoteInit.mResources + * (static field) holding on to a Resources instance that has a context that is the activity. + * + * Observed here: https://github.com/square/leakcanary/issues/1#issue-74450184 + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_K__) { + analyzer.ExcludeInstanceFieldReference("android.content.res.Resources", "mContext"); + } + + /* + * reason: + * + * In AOSP the ViewConfiguration class does not have a context. Here we have + * ViewConfiguration.sConfigurations (static field) holding on to a ViewConfiguration instance + * that has a context that is the activity. + * + * Observed here: https://github.com/square/leakcanary/issues/1#issuecomment-100324683 + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_K__) { + analyzer.ExcludeInstanceFieldReference("android.view.ViewConfiguration", "mContext"); + } + + /* + * reason: + * + * Samsung added a static mContext_static field to AudioManager, holds a reference to the + * activity. + * + * Observed here: https://github.com/square/leakcanary/issues/32 + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_K__) { + analyzer.ExcludeStaticFieldReference("android.media.AudioManager", "mContext_static"); + } + + /* + * reason: + * + * Samsung added a static mContext field to ActivityManager, holds a reference to the activity. + * + * Observed here: https://github.com/square/leakcanary/issues/177 + * + * Fix in comment: https://github.com/square/leakcanary/issues/177#issuecomment-222724283 + */ + if (manufacturer == MANUFACTURER_SAMSUNG && + sdk_version >= __ANDROID_API_L_MR1__ && sdk_version <= __ANDROID_API_M__) { + analyzer.ExcludeStaticFieldReference("android.app.ActivityManager", "mContext"); + } + + /* + * reason: + * + * Samsung added a static mTargetView field to TextView which holds on to detached views. + */ + if (manufacturer == MANUFACTURER_SAMSUNG && sdk_version == __ANDROID_API_O_MR1__) { + analyzer.ExcludeStaticFieldReference("android.widget.TextView", "mTargetView"); + } + + /* + * reason: + * + * DecorView isn't leaking but its mDecorViewSupport field holds a MultiWindowDecorSupport which + * has a mWindow field which holds a leaking PhoneWindow. DecorView.mDecorViewSupport doesn't + * exist in AOSP. + * + * Filed here: https://github.com/square/leakcanary/issues/1819 + */ + if (manufacturer == MANUFACTURER_SAMSUNG && + sdk_version >= __ANDROID_API_O__ && sdk_version <= __ANDROID_API_Q__) { + analyzer.ExcludeInstanceFieldReference( + "com.android.internal.policy.MultiWindowDecorSupport", "mWindow"); + } + + /* + * reason: + * + * Lenovo specific leak. SystemSensorManager stores a reference to context in a static field in + * its constructor. Found on LENOVO 4.4.2. + * + * Fix: use application context to get SensorManager. + */ + if ((manufacturer == MANUFACTURER_LENOVO && sdk_version == __ANDROID_API_K__) || + (manufacturer == MANUFACTURER_VIVO && sdk_version == __ANDROID_API_L_MR1__)) { + analyzer.ExcludeStaticFieldReference( + "android.hardware.SystemSensorManager", "mAppContextImpl"); + } + + /* + * reason: + * + * Not sure exactly what ControllerMapper is about, but there is an anonymous HeapDumpHandler in + * ControllerMapper.MapperClient.ServiceClient, which leaks ControllerMapper.MapperClient which + * leaks the activity context. + */ + if (manufacturer == MANUFACTURER_NVIDIA && sdk_version == __ANDROID_API_K__) { + analyzer.ExcludeInstanceFieldReference( + "com.nvidia.ControllerMapper.MapperClient$ServiceClient", "this$0"); + } + + /* + * reason: + * + * A static helper for EditText bubble popups leaks a reference to the latest focused view. + */ + if (manufacturer == MANUFACTURER_LG && + sdk_version >= __ANDROID_API_K__ && sdk_version <= __ANDROID_API_L_MR1__) { + analyzer.ExcludeStaticFieldReference( + "android.widget.BubblePopupHelper", "sHelper"); + } + + /* + * reason: + * + * LGContext is a static singleton that leaks an activity context. + */ + if (manufacturer == MANUFACTURER_LG && sdk_version == __ANDROID_API_L__) { + analyzer.ExcludeInstanceFieldReference("com.lge.systemservice.core.LGContext", "mContext"); + } + + /* + * reason: + * + * SmartCoverManager$CallbackRegister is a callback held by a native ref, and SmartCoverManager + * ends up leaking an activity context. + */ + if (manufacturer == MANUFACTURER_LG && sdk_version == __ANDROID_API_O_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "com.lge.systemservice.core.SmartCoverManager", "mContext"); + } + + /* + * reason: + * + * Instrumentation would leak com.android.internal.app.RecommendActivity (in framework.jar) in + * Meizu FlymeOS 4.5 and above, which is based on Android 5.0 and above + */ + if (manufacturer == MANUFACTURER_MEIZU && + sdk_version >= __ANDROID_API_L__ && sdk_version <= __ANDROID_API_L_MR1__) { + analyzer.ExcludeStaticFieldReference( + "android.app.Instrumentation", "mRecommendActivity"); + } + + /* + * reason: + * + * DevicePolicyManager keeps a reference to the context it has been created with instead of + * extracting the application context. In this Motorola build, DevicePolicyManager has an inner + * SettingsObserver class that is a content observer, which is held into memory by a binder + * transport object. + */ + if (manufacturer == MANUFACTURER_MOTOROLA && + sdk_version >= __ANDROID_API_K__ && sdk_version <= __ANDROID_API_L_MR1__) { + analyzer.ExcludeInstanceFieldReference( + "android.app.admin.DevicePolicyManager$SettingsObserver", + "this$0"); + } + + /* + * reason: + * + * GestureBoostManager is a static singleton that leaks an activity context. + * + * Fix: https://github.com/square/leakcanary/issues/696#issuecomment-296420756 + */ + if (manufacturer == MANUFACTURER_HUAWEI && + sdk_version >= __ANDROID_API_N__ && sdk_version <= __ANDROID_API_N_MR1__) { + analyzer.ExcludeStaticFieldReference( + "android.gestureboost.GestureBoostManager", "mContext"); + } + + /* + * reason: + * + * ExtendedStatusBarManager is held in a static sInstance field and has a mContext field which + * references a decor context which references a destroyed activity. + */ + if (manufacturer == MANUFACTURER_SHARP && sdk_version == __ANDROID_API_Q__) { + analyzer.ExcludeStaticFieldReference("android.app.ExtendedStatusBarManager", "sInstance"); + } + + /* + * reason: + * + * OemSceneCallBlocker has a sContext static field which holds on to an activity instance. + */ + if (manufacturer == MANUFACTURER_ONEPLUS && sdk_version == __ANDROID_API_P__) { + analyzer.ExcludeStaticFieldReference("com.oneplus.util.OemSceneCallBlocker", "sContext"); + } + + /* + * reason: + * + * In AOSP, TextKeyListener instances are held in a TextKeyListener.sInstances static array. + * The Razer implementation added a mContext field, creating activity leaks. + */ + if (manufacturer == MANUFACTURER_RAZER && sdk_version == __ANDROID_API_P__) { + analyzer.ExcludeInstanceFieldReference("android.text.method.TextKeyListener", "mContext"); + } + + /* + General Exclude References + */ + + /* + * Non-strong references. + */ + analyzer.ExcludeInstanceFieldReference("java.lang.ref.WeakReference", "referent"); + analyzer.ExcludeInstanceFieldReference("java.lang.ref.SoftReference", "referent"); + analyzer.ExcludeInstanceFieldReference("java.lang.ref.PhantomReference", "referent"); + analyzer.ExcludeInstanceFieldReference("java.lang.ref.Finalizer", "prev"); + analyzer.ExcludeInstanceFieldReference("java.lang.ref.Finalizer", "element"); + analyzer.ExcludeInstanceFieldReference("java.lang.ref.Finalizer", "next"); + analyzer.ExcludeInstanceFieldReference("java.lang.ref.FinalizerReference", "prev"); + analyzer.ExcludeInstanceFieldReference("java.lang.ref.FinalizerReference", "element"); + analyzer.ExcludeInstanceFieldReference("java.lang.ref.FinalizerReference", "next"); + analyzer.ExcludeInstanceFieldReference("sun.misc.Cleaner", "prev"); + analyzer.ExcludeInstanceFieldReference("sun.misc.Cleaner", "next"); + + /* + * Finalizer watch dog daemon. + */ + analyzer.ExcludeThreadReference("FinalizerWatchdogDaemon"); + + /* + * Main thread. + */ + analyzer.ExcludeThreadReference("main"); + + /* + * Event receiver message queue. + */ + analyzer.ExcludeInstanceFieldReference( + "android.view.Choreographer$FrameDisplayEventReceiver", + "mMessageQueue"); + + return true; +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/excludes/excludes.h b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/excludes/excludes.h new file mode 100644 index 000000000..851491daa --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/excludes/excludes.h @@ -0,0 +1,10 @@ +#ifndef __matrix_resource_canary_memory_util_excludes_h__ +#define __matrix_resource_canary_memory_util_excludes_h__ + +#include "matrix_hprof_analyzer.h" + +using namespace matrix::hprof; + +bool exclude_default_references(HprofAnalyzer &analyzer); + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/log.cpp b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/log.cpp new file mode 100644 index 000000000..b5a46055b --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/log.cpp @@ -0,0 +1,28 @@ +#include +#include "log.h" + +#if defined(__arm__) || defined(__aarch64__) + +typedef int (*logger_t)(int log_level, const char *tag, const char *format, va_list varargs); + +extern "C" logger_t logger_func(); + +void print_log(int level, const char *tag, const char *format, ...) { + logger_t logger = logger_func(); + if (logger == nullptr) return; + va_list args; + va_start(args, format); + logger(level, tag, format, args); + va_end(args); +} + +#else + +void print_log(int level, const char *tag, const char *format, ...) { + va_list args; + va_start(args, format); + __android_log_vprint(level, tag, format, args); + va_end(args); +} + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/log.h b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/log.h new file mode 100644 index 000000000..5b47bf148 --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/log.h @@ -0,0 +1,15 @@ +// +// Created by M.D. on 2021/10/26. +// + +#ifndef MATRIX_ANDROID_LOG_H +#define MATRIX_ANDROID_LOG_H + +#include + +void print_log(int level, const char *tag, const char *format, ...); + +#define _info_log(tag, fmt, args...) print_log(ANDROID_LOG_INFO, tag, fmt, ##args) +#define _error_log(tag, fmt, args...) print_log(ANDROID_LOG_ERROR, tag, fmt, ##args) + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/memory_util.cpp b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/memory_util.cpp new file mode 100644 index 000000000..872f0a3cf --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/memory_util.cpp @@ -0,0 +1,531 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "excludes/excludes.h" +#include "log.h" +#include "symbol/symbol.h" + +#include "matrix_hprof_analyzer.h" + +using namespace matrix::hprof; + +#define TAG "Matrix.MemoryUtil" + +static bool task_process = false; + +static std::string extract_string(JNIEnv *env, jstring string) { + const char *value = env->GetStringUTFChars(string, nullptr); + std::string result(value, strlen(value)); + env->ReleaseStringUTFChars(string, value); + return result; +} + +static void log_and_throw_runtime_exception(JNIEnv *env, const char *message) { + _error_log(TAG, "exception: %s", message); + jclass runtime_exception_class = env->FindClass("java/lang/RuntimeException"); + if (runtime_exception_class != nullptr) { + env->ThrowNew(runtime_exception_class, message); + } +} + +// These values need to be updated synchronously with constants in object TaskState. *************** +#define TS_UNKNOWN (-1) +#define TS_DUMP 1 +#define TS_ANALYZER_CREATE 2 +#define TS_ANALYZER_INITIALIZE 3 +#define TS_ANALYZER_EXECUTE 4 +#define TS_CREATE_RESULT_FILE 5 +#define TS_SERIALIZE 6 +// ************************************************************************************************* + +static std::string task_state_dir; + +static void update_task_state(int8_t state) { + if (!task_process) return; + _info_log(TAG, "update_state: task %d state -> %d.", getpid(), state); + std::stringstream task_path; + task_path << task_state_dir << "/" << getpid(); + int task_fd = open(task_path.str().c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (task_fd == -1) { + _error_log(TAG, "update_state: invoke open() failed with errno %d", errno); + return; + } + if (write(task_fd, &state, sizeof(int8_t)) != sizeof(int8_t)) { + _error_log(TAG, "update_state: invoke write() failed with errno %d", errno); + } + if (close(task_fd) == -1) { + _error_log(TAG, "update_state: invoke close() failed with errno %d", errno); + } +} + +static int8_t get_task_state_and_cleanup(int pid) { + std::stringstream task_path; + task_path << task_state_dir << "/" << pid; + int task_fd = open(task_path.str().c_str(), O_RDONLY); + int8_t result; + if (task_fd == -1) { + _error_log(TAG, "get_state: invoke open() failed with errno %d", errno); + result = TS_UNKNOWN; + goto cleanup; + } + if (read(task_fd, &result, sizeof(int8_t)) != sizeof(int8_t)) { + _error_log(TAG, "get_state: invoke read() failed with errno %d", errno); + result = TS_UNKNOWN; + } + if (close(task_fd)) { + _error_log(TAG, "get_state: invoke close() failed with errno %d", errno); + } + cleanup: + if (access(task_path.str().c_str(), F_OK) == 0) { + if (remove(task_path.str().c_str())) { + _error_log(TAG, "get_state: invoke remove() failed with errno %d", errno); + } + } + return result; +} + +static std::string task_error_dir; + +static void update_task_error(const std::string &error) { + if (!task_process) return; + _info_log(TAG, "update_error: task %d error -> %s", getpid(), error.c_str()); + std::stringstream task_path; + task_path << task_error_dir << "/" << getpid(); + int task_fd = open(task_path.str().c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (task_fd == -1) { + _error_log(TAG, "update_error: invoke open() failed with errno %d", errno); + return; + } + if (write(task_fd, error.c_str(), error.length()) != error.length()) { + _error_log(TAG, "update_error: invoke write() failed with errno %d", errno); + } + if (close(task_fd) == -1) { + _error_log(TAG, "update_error: invoke close() failed with errno %d", errno); + } +} + +static std::string get_task_error_and_cleanup(int pid) { + std::stringstream task_path; + task_path << task_error_dir << "/" << pid; + std::string result; + { + std::ifstream file(task_path.str()); + getline(file, result); + if (file.fail()) result = ""; + } + if (access(task_path.str().c_str(), F_OK) == 0) { + if (remove(task_path.str().c_str())) { + _error_log(TAG, "get_task: invoke remove() failed with errno %d", errno); + } + } + return result; +} + +static jclass task_result_class = nullptr; +static jmethodID task_result_constructor = nullptr; + +extern "C" +JNIEXPORT void JNICALL +Java_com_tencent_matrix_resource_MemoryUtil_loadJniCache(JNIEnv *env, jobject) { + _info_log(TAG, "initialize: load JNI pointer cache"); + if (task_result_constructor == nullptr) { + if (task_result_class == nullptr) { + jclass local = env->FindClass("com/tencent/matrix/resource/MemoryUtil$TaskResult"); + if (local == nullptr) { + log_and_throw_runtime_exception(env, "Failed to find class TaskResult"); + return; + } + // Make sure the class will not be unloaded. + // See: https://developer.android.com/training/articles/perf-jni#jclass-jmethodid-and-jfieldid + task_result_class = reinterpret_cast(env->NewGlobalRef(local)); + if (task_result_class == nullptr) { + log_and_throw_runtime_exception(env, "Failed to create global reference of class TaskResult"); + return; + } + } + + task_result_constructor = + env->GetMethodID(task_result_class, "", "(IIBLjava/lang/String;)V"); + if (task_result_constructor == nullptr) { + log_and_throw_runtime_exception(env, "Failed to find constructor of class TaskResult"); + return; + } + } +} + +static jobject +create_task_result(JNIEnv *env, int32_t type, int32_t code, + int8_t task_state, const std::string &task_error) { + return env->NewObject(task_result_class, task_result_constructor, type, code, + task_state, env->NewStringUTF(task_error.c_str())); +} + +// ! JNI method +static void create_directory(JNIEnv *env, const char *path) { + if (mkdir(path, S_IRWXU) == 0) return; + if (errno != EEXIST) { + std::stringstream error_builder; + error_builder << "Failed to create directory " << path + << " with errno " << errno; + log_and_throw_runtime_exception(env, error_builder.str().c_str()); + return; + } + // Check the existing entity is a directory we can access. + struct stat s{}; + if (stat(path, &s)) { + std::stringstream error_builder; + error_builder << "Failed to check directory " << path + << " state with errno " << errno; + log_and_throw_runtime_exception(env, error_builder.str().c_str()); + return; + } + if (!S_ISDIR(s.st_mode)) { + std::stringstream error_builder; + error_builder << "Path " << path << " exists and it is not a directory"; + log_and_throw_runtime_exception(env, error_builder.str().c_str()); + return; + } + if (access(path, R_OK | W_OK)) { + std::stringstream error_builder; + error_builder << "Directory " << path << " accessibility check failed with errno " + << errno; + log_and_throw_runtime_exception(env, error_builder.str().c_str()); + return; + } +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_tencent_matrix_resource_MemoryUtil_syncTaskDir(JNIEnv *env, jobject, jstring path) { + _info_log(TAG, "initialize: sync and create task info directories path"); + const char *value = env->GetStringUTFChars(path, nullptr); + task_state_dir = ({ + std::stringstream builder; + builder << value << "/ts"; + builder.str(); + }); + task_error_dir = ({ + std::stringstream builder; + builder << value << "/err"; + builder.str(); + }); + env->ReleaseStringUTFChars(path, value); + create_directory(env, task_state_dir.c_str()); + create_directory(env, task_error_dir.c_str()); +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_tencent_matrix_resource_MemoryUtil_initializeSymbol(JNIEnv *env, jobject) { + _info_log(TAG, "initialize: initialize symbol"); + if (!initialize_symbols()) { + log_and_throw_runtime_exception(env, "Failed to initialize symbol"); + return; + } +} + +#define unwrap_optional(optional, nullopt_action) \ + ({ \ + const auto &result = optional; \ + if (!result.has_value()) { \ + nullopt_action; \ + } \ + result.value(); \ + }) + +static int32_t serialize_reference_type(LeakChain::Node::ReferenceType type) { + switch (type) { + case LeakChain::Node::ReferenceType::kStaticField: + return 1; + case LeakChain::Node::ReferenceType::kInstanceField: + return 2; + case LeakChain::Node::ReferenceType::kArrayElement: + return 3; + } +} + +static int32_t serialize_object_type(LeakChain::Node::ObjectType type) { + switch (type) { + case LeakChain::Node::ObjectType::kClass: + return 1; + case LeakChain::Node::ObjectType::kObjectArray: + return 2; + default: + return 3; + } +} + +static int fork_task(const char *task_name, unsigned int timeout) { + auto *thread = current_thread(); + suspend_runtime(thread); + int pid = fork(); + if (pid == 0) { + task_process = true; + if (timeout != 0) { + alarm(timeout); + } + prctl(PR_SET_NAME, task_name); + } else { + resume_runtime(thread); + } + return pid; +} + +static void on_error(const char *error) { + _error_log(TAG, "error happened: %s", error); + update_task_error(error); +} + +// ! execute on task process +static void execute_dump(const char *file_name) { + _info_log(TAG, "task_process %d: dump", getpid()); + update_task_state(TS_DUMP); + dump_heap(file_name); +} + +// ! execute on task process +static void analyzer_error_listener(const char *message) { + on_error(message); +} + +// ! execute on task process +static std::optional> +execute_analyze(const char *hprof_path, const char *reference_key) { + _info_log(TAG, "task_process %d: analyze", getpid()); + + update_task_state(TS_ANALYZER_CREATE); + const int hprof_fd = open(hprof_path, O_RDONLY); + if (hprof_fd == -1) { + std::stringstream error_builder; + error_builder << "invoke open() failed on HPROF with errno " << errno; + on_error(error_builder.str().c_str()); + return std::nullopt; + } + HprofAnalyzer::SetErrorListener(analyzer_error_listener); + HprofAnalyzer analyzer(hprof_fd); + + update_task_state(TS_ANALYZER_INITIALIZE); + if (!exclude_default_references(analyzer)) { + on_error("exclude default references rules failed"); + return std::nullopt; + } + + update_task_state(TS_ANALYZER_EXECUTE); + return analyzer.Analyze([reference_key](const HprofHeap &heap) { + const object_id_t leak_ref_class_id = unwrap_optional( + heap.FindClassByName( + "com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo"), + return std::vector()); + std::vector leaks; + for (const object_id_t leak_ref: heap.GetInstances(leak_ref_class_id)) { + const object_id_t key_string_id = unwrap_optional( + heap.GetFieldReference(leak_ref, "mKey"), continue); + const std::string &key_string = unwrap_optional( + heap.GetValueFromStringInstance(key_string_id), continue); + if (key_string != reference_key) + continue; + const object_id_t weak_ref = unwrap_optional( + heap.GetFieldReference(leak_ref, "mActivityRef"), continue); + const object_id_t leak = unwrap_optional( + heap.GetFieldReference(weak_ref, "referent"), continue); + leaks.emplace_back(leak); + } + return leaks; + }); +} + +// ! execute on task process +static bool execute_serialize(const char *result_path, const std::vector &leak_chains) { + _info_log(TAG, "task_process %d: serialize", getpid()); + + update_task_state(TS_CREATE_RESULT_FILE); + bool result = false; + int result_fd = open(result_path, O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR); + if (result_fd == -1) { + std::stringstream error_builder; + error_builder << "invoke open() failed on result file with errno " << errno; + on_error(error_builder.str().c_str()); + return false; + } + + update_task_state(TS_SERIALIZE); + // See comment documentation of MemoryUtil.deserialize for the file format of + // result file. +#define write_data(content, size) \ + if (write(result_fd, content, size) == -1) { \ + std::stringstream error_builder; \ + error_builder << "invoke write() failed on result file with errno " << errno; \ + on_error(error_builder.str().c_str()); \ + goto write_leak_chain_done; \ + } + + const uint32_t byte_order_magic = 0x1; + const uint32_t leak_chain_count = leak_chains.size(); + write_data(&byte_order_magic, sizeof(uint32_t)) + write_data(&leak_chain_count, sizeof(uint32_t)) + for (const auto &leak_chain : leak_chains) { + const uint32_t leak_chain_length = leak_chain.GetDepth() + 1; + write_data(&leak_chain_length, sizeof(uint32_t)) + + int32_t gc_root_type; + if (leak_chain_length == 1) { + gc_root_type = serialize_object_type(LeakChain::Node::ObjectType::kInstance); + } else { + const auto ref_type = leak_chain.GetNodes()[0].GetReferenceType(); + switch (ref_type) { + case LeakChain::Node::ReferenceType::kStaticField: + gc_root_type = serialize_object_type(LeakChain::Node::ObjectType::kClass); + break; + case LeakChain::Node::ReferenceType::kInstanceField: + gc_root_type = serialize_object_type( + LeakChain::Node::ObjectType::kInstance); + break; + case LeakChain::Node::ReferenceType::kArrayElement: + gc_root_type = serialize_object_type( + LeakChain::Node::ObjectType::kObjectArray); + break; + } + } + write_data(&gc_root_type, sizeof(int32_t)) + const uint32_t gc_root_name_length = leak_chain.GetGcRoot().GetName().length(); + write_data(&gc_root_name_length, sizeof(uint32_t)) + write_data(leak_chain.GetGcRoot().GetName().c_str(), gc_root_name_length) + + for (const auto &node : leak_chain.GetNodes()) { + const int32_t serialized_reference_type = + serialize_reference_type(node.GetReferenceType()); + write_data(&serialized_reference_type, sizeof(int32_t)) + const uint32_t reference_name_length = node.GetReference().length(); + write_data(&reference_name_length, sizeof(uint32_t)) + write_data(node.GetReference().c_str(), reference_name_length) + + const int32_t serialized_object_type = + serialize_object_type(node.GetObjectType()); + write_data(&serialized_object_type, sizeof(int32_t)) + const uint32_t object_name_length = node.GetObject().length(); + write_data(&object_name_length, sizeof(uint32_t)) + write_data(node.GetObject().c_str(), object_name_length) + } + + const uint32_t end_tag = 0; + write_data(&end_tag, sizeof(uint32_t)) + } + result = true; + write_leak_chain_done: + close(result_fd); + return result; +} + +#define TC_NO_ERROR 0 +#define TC_EXCEPTION 1 +#define TC_ANALYZE_ERROR 2 +#define TC_SERIALIZE_ERROR 3 + +extern "C" +JNIEXPORT jint JNICALL +Java_com_tencent_matrix_resource_MemoryUtil_forkDump(JNIEnv *env, jobject, + jstring java_hprof_path, + jlong timeout) { + const std::string hprof_path = extract_string(env, java_hprof_path); + + int task_pid = fork_task("matrix_mem_dump", timeout); + if (task_pid != 0) { + return task_pid; + } else { + /* dump */ + execute_dump(hprof_path.c_str()); + /* end */ + _exit(TC_NO_ERROR); + } +} + +extern "C" +JNIEXPORT jint JNICALL +Java_com_tencent_matrix_resource_MemoryUtil_forkAnalyze(JNIEnv *env, jobject, + jstring java_hprof_path, + jstring java_result_path, + jstring java_reference_key, + jlong timeout) { + const std::string hprof_path = extract_string(env, java_hprof_path); + const std::string result_path = extract_string(env, java_result_path); + const std::string reference_key = extract_string(env, java_reference_key); + + int task_pid = fork_task("matrix_mem_anlz", timeout); + if (task_pid != 0) { + return task_pid; + } else { + /* analyze */ + const std::optional> result = + execute_analyze(hprof_path.c_str(), reference_key.c_str()); + if (!result.has_value()) _exit(TC_ANALYZE_ERROR); + /* serialize result */ + const bool serialized = execute_serialize(result_path.c_str(), result.value()); + if (!serialized) _exit(TC_SERIALIZE_ERROR); + /* end */ + _exit(TC_NO_ERROR); + } +} + +extern "C" +JNIEXPORT jint JNICALL +Java_com_tencent_matrix_resource_MemoryUtil_forkDumpAndAnalyze(JNIEnv *env, jobject, + jstring java_hprof_path, + jstring java_result_path, + jstring java_reference_key, + jlong timeout) { + const std::string hprof_path = extract_string(env, java_hprof_path); + const std::string result_path = extract_string(env, java_result_path); + const std::string reference_key = extract_string(env, java_reference_key); + + int task_pid = fork_task("matrix_mem_d&a", timeout); + if (task_pid != 0) { + return task_pid; + } else { + /* dump */ + execute_dump(hprof_path.c_str()); + /* analyze */ + const std::optional> result = + execute_analyze(hprof_path.c_str(), reference_key.c_str()); + if (!result.has_value()) _exit(TC_ANALYZE_ERROR); + /* serialize result */ + const bool serialized = execute_serialize(result_path.c_str(), result.value()); + if (!serialized) _exit(TC_SERIALIZE_ERROR); + /* end */ + _exit(TC_NO_ERROR); + } +} + +// These values need to be updated synchronously with constants in class TaskResult. *************** +#define TR_TYPE_WAIT_FAILED (-1) +#define TR_TYPE_EXIT 0 +#define TR_TYPE_SIGNALED 1 +#define TR_TYPE_UNKNOWN 2 +// ************************************************************************************************* + +extern "C" JNIEXPORT jobject JNICALL +Java_com_tencent_matrix_resource_MemoryUtil_waitTask(JNIEnv *env, jobject, jint pid) { + int status; + if (waitpid(pid, &status, 0) == -1) { + _error_log(TAG, "invoke waitpid failed with errno %d", errno); + return create_task_result(env, TR_TYPE_WAIT_FAILED, errno, TS_UNKNOWN, "none"); + } + + const int8_t task_state = get_task_state_and_cleanup(pid); + const std::string task_error = get_task_error_and_cleanup(pid); + if (WIFEXITED(status)) { + return create_task_result(env, TR_TYPE_EXIT, WEXITSTATUS(status), task_state, task_error); + } else if (WIFSIGNALED(status)) { + return create_task_result(env, TR_TYPE_SIGNALED, WTERMSIG(status), task_state, task_error); + } else { + return create_task_result(env, TR_TYPE_UNKNOWN, 0, task_state, task_error); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/bionic/tls.h b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/bionic/tls.h similarity index 100% rename from matrix/matrix-android/matrix-memory-dump/src/main/cpp/bionic/tls.h rename to matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/bionic/tls.h diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/bionic/tls_defines.h b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/bionic/tls_defines.h similarity index 99% rename from matrix/matrix-android/matrix-memory-dump/src/main/cpp/bionic/tls_defines.h rename to matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/bionic/tls_defines.h index fbe0ed5cb..3e5802a06 100644 --- a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/bionic/tls_defines.h +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/bionic/tls_defines.h @@ -48,7 +48,7 @@ // - OpenGL and compiler-rt // - Accesses of x86 ELF TLS variables // -// - TLS_SLOT_OPENGL and TLS_SLOT_OPENGL_API: These two aren't used by bionic +// - TLS_SLOT_OPENGL and TLS_SLOT_OPENGL_API: These two aren't used by symbol.bionic // itself, but allow the graphics code to access TLS directly rather than // using the pthread API. // diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/dlfcn/self_dlfcn.cpp b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/dlsym/dlsym.cpp similarity index 70% rename from matrix/matrix-android/matrix-memory-dump/src/main/cpp/dlfcn/self_dlfcn.cpp rename to matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/dlsym/dlsym.cpp index c118c9164..52b9ba1a0 100644 --- a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/dlfcn/self_dlfcn.cpp +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/dlsym/dlsym.cpp @@ -2,33 +2,34 @@ // Created by M.D. on 2021/10/29. // -#include "self_dlfcn.h" -#include +#include "dlsym.h" + #include +#include extern "C" { static bool use_origin_ = false; -void self_dlfcn_mode(int api) { +void ds_mode(int api) { use_origin_ = api < __ANDROID_API_N__; } -void self_dlclose(void *handle) { +void ds_close(void *handle) { if (use_origin_) dlclose(handle); else enhance::dlclose(handle); } -void self_clean(void* handle) { +void ds_clean(void* handle) { if (!use_origin_) enhance::dlclose(handle); } -void *self_dlopen(const char *filename) { +void *ds_open(const char *filename) { if (use_origin_) return dlopen(filename, RTLD_NOW); else return enhance::dlopen(filename, RTLD_NOW); } -void *self_dlsym(void *handle, const char *name) { +void *ds_find(void *handle, const char *name) { if (use_origin_) return dlsym(handle, name); else return enhance::dlsym(handle, name); } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/dlsym/dlsym.h b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/dlsym/dlsym.h new file mode 100644 index 000000000..6b339e4ed --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/dlsym/dlsym.h @@ -0,0 +1,22 @@ +// +// Created by M.D. on 2021/10/29. +// + +#ifndef MATRIX_ANDROID_DLSYM_H +#define MATRIX_ANDROID_DLSYM_H + +extern "C" { + +void ds_mode(int api); + +void ds_close(void *handle); + +void ds_clean(void *handle); + +void *ds_open(const char *filename); + +void *ds_find(void *handle, const char *name); + +} + +#endif //MATRIX_ANDROID_DLSYM_H diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/runtime/collector_type.h b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/runtime/collector_type.h similarity index 100% rename from matrix/matrix-android/matrix-memory-dump/src/main/cpp/runtime/collector_type.h rename to matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/runtime/collector_type.h diff --git a/matrix/matrix-android/matrix-memory-dump/src/main/cpp/runtime/gc_cause.h b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/runtime/gc_cause.h similarity index 100% rename from matrix/matrix-android/matrix-memory-dump/src/main/cpp/runtime/gc_cause.h rename to matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/runtime/gc_cause.h diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/symbol.cpp b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/symbol.cpp new file mode 100644 index 000000000..53311f398 --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/symbol.cpp @@ -0,0 +1,206 @@ +#include "symbol.h" + +#include + +#include "bionic/tls.h" +#include "bionic/tls_defines.h" +#include "dlsym/dlsym.h" +#include "../log.h" +#include "runtime/collector_type.h" +#include "runtime/gc_cause.h" + +#define TAG "Matrix.MemoryUtil.Symbol" + +static int android_version_; + +/** + * Points to symbol `art::hprof::DumpHeap()`. + */ +static void (*dump_heap_)(const char *, int, bool) = nullptr; + +void dump_heap(const char *file_name) { + dump_heap_(file_name, -1, false); +} + +using namespace art::gc; + +namespace mirror { + /** + * Mirror type of `art::ScopedSuspendAll`. + */ + class ScopedSuspend { + }; + + /** + * Points to symbol `art::gc::ScopedGCCriticalSection()`. + */ + static void (*sgc_constructor)(void *, Thread *, GcCause, CollectorType) = nullptr; + + /** + * Points to symbol `art::gc::~ScopedGCCriticalSection()`. + */ + static void (*sgc_destructor)(void *) = nullptr; + + /** + * Mirror type of `art::gc::ScopedGCCriticalSection`. + */ + class ScopedGCCriticalSection { + private: + uint64_t buf[8] = {0}; + public: + ScopedGCCriticalSection(Thread *thread, GcCause cause, CollectorType collectorType) { + sgc_constructor(this, thread, cause, collectorType); + } + + ~ScopedGCCriticalSection() { + sgc_destructor(this); + } + }; + + /** + * Points to symbol `art::ReaderWriterMutex::ExclusiveLock()`. + */ + static void (*exclusive_lock)(void *, Thread *) = nullptr; + + /** + * Points to symbol `art::ReaderWriterMutex::ExclusiveUnlock()`. + */ + static void (*exclusive_unlock)(void *, Thread *) = nullptr; + + /** + * Mirror type of `art::ReaderWriterMutex`. + */ + class ReadWriteMutex { + public: + void ExclusiveLock(Thread *self) { + if (exclusive_lock != nullptr) { + reinterpret_cast(exclusive_lock)(this, self); + } + } + + void ExclusiveUnlock(Thread *self) { + if (exclusive_unlock != nullptr) { + reinterpret_cast(exclusive_unlock)(this, self); + } + } + }; +} + +mirror::Thread *current_thread() { + return reinterpret_cast(__get_tls()[TLS_SLOT_ART_THREAD_SELF]); +} + +static mirror::ScopedSuspend suspend_; + +static mirror::ReadWriteMutex *mutator_lock_ = nullptr; + +/** + * Points to symbol `art::Dbg::SuspendVM()`. + */ +static void *suspend_all_ptr_ = nullptr; + +void suspend_runtime(mirror::Thread *thread) { + if (android_version_ > __ANDROID_API_Q__) { + mirror::ScopedGCCriticalSection sgc(thread, kGcCauseHprof, kCollectorTypeHprof); + reinterpret_cast + (suspend_all_ptr_)(&suspend_, "matrix_dump_hprof", true); + mutator_lock_->ExclusiveUnlock(thread); + } else { + reinterpret_cast(suspend_all_ptr_)(); + } +} + +/** + * Points to symbol `art::Dbg::ResumeVM()`. + */ +static void *resume_all_ptr_ = nullptr; + +void resume_runtime(mirror::Thread *thread) { + if (android_version_ > __ANDROID_API_Q__) { + mutator_lock_->ExclusiveLock(thread); + reinterpret_cast(resume_all_ptr_)(&suspend_); + } else { + reinterpret_cast(resume_all_ptr_)(); + } +} + +bool initialize_symbols() { + android_version_ = android_get_device_api_level(); + if (android_version_ <= 0) return false; + ds_mode(android_version_); + + auto *art_lib = ds_open("libart.so"); + + if (art_lib == nullptr) { + _error_log(TAG, "open libart.so failed"); + return false; + } + +#define load_symbol(ptr, type, sym, err) \ + ptr = reinterpret_cast(ds_find(art_lib, sym)); \ + if ((ptr) == nullptr) { \ + _error_log(TAG, err); \ + goto on_error; \ + } + + load_symbol(dump_heap_, + void(*)(const char *, int, bool ), + "_ZN3art5hprof8DumpHeapEPKcib", + "cannot find symbol art::hprof::DumpHeap()") + + if (android_version_ > __ANDROID_API_Q__) { + load_symbol(mirror::sgc_constructor, + void(*)(void * , mirror::Thread *, art::gc::GcCause, art::gc::CollectorType), + "_ZN3art2gc23ScopedGCCriticalSectionC1EPNS_6ThreadENS0_7GcCauseENS0_13CollectorTypeE", + "cannot find symbol art::gc::ScopedGCCriticalSection()") + load_symbol(mirror::sgc_destructor, + void(*)(void * ), + "_ZN3art2gc23ScopedGCCriticalSectionD1Ev", + "cannot find symbol art::gc::~ScopedGCCriticalSection()") + } + + if (android_version_ > __ANDROID_API_Q__) { + mirror::ReadWriteMutex **lock_sym; + load_symbol(lock_sym, + mirror::ReadWriteMutex **, + "_ZN3art5Locks13mutator_lock_E", + "cannot find symbol art::Locks::mutator_lock_") + mutator_lock_ = *lock_sym; + + load_symbol(mirror::exclusive_lock, + void(*)(void * , mirror::Thread *), + "_ZN3art17ReaderWriterMutex13ExclusiveLockEPNS_6ThreadE", + "cannot find symbol art::ReaderWriterMutex::ExclusiveLock()") + load_symbol(mirror::exclusive_unlock, + void(*)(void * , mirror::Thread *), + "_ZN3art17ReaderWriterMutex15ExclusiveUnlockEPNS_6ThreadE", + "cannot find symbol art::ReaderWriterMutex::ExclusiveUnlock()") + } + + if (android_version_ > __ANDROID_API_Q__) { + load_symbol(suspend_all_ptr_, + void*, + "_ZN3art16ScopedSuspendAllC1EPKcb", + "cannot find symbol art::ScopedSuspendAll()") + load_symbol(resume_all_ptr_, + void*, + "_ZN3art16ScopedSuspendAllD1Ev", + "cannot find symbol art::~ScopedSuspendAll()") + } else { + load_symbol(suspend_all_ptr_, + void*, + "_ZN3art3Dbg9SuspendVMEv", + "cannot find symbol art::Dbg::SuspendVM()") + load_symbol(resume_all_ptr_, + void*, + "_ZN3art3Dbg8ResumeVMEv", + "cannot find symbol art::Dbg::ResumeVM()") + } + + ds_clean(art_lib); + return true; + + on_error: + ds_close(art_lib); + return false; +} diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/symbol.h b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/symbol.h new file mode 100644 index 000000000..09d7f6240 --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/cpp/memory_util/symbol/symbol.h @@ -0,0 +1,19 @@ +#ifndef __matrix_resource_canary_memory_util_symbol_h__ +#define __matrix_resource_canary_memory_util_symbol_h__ + +bool initialize_symbols(); + +void dump_heap(const char *file_name); + +namespace mirror { + class Thread { + }; +} + +mirror::Thread *current_thread(); + +void suspend_runtime(mirror::Thread *thread); + +void resume_runtime(mirror::Thread *thread); + +#endif \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/ActivityLeakFixer.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/ActivityLeakFixer.java index 46b93ce8e..4e8a0f703 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/ActivityLeakFixer.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/ActivityLeakFixer.java @@ -130,12 +130,17 @@ public static void fixInputMethodManagerLeak(Context destContext) { MatrixLog.i(TAG, "fixInputMethodManagerLeak done, cost: %s ms.", System.currentTimeMillis() - startTick); } + public static boolean sSupportSplit = false; + public static void unbindDrawables(Activity ui) { final long startTick = System.currentTimeMillis(); if (ui != null && ui.getWindow() != null && ui.getWindow().peekDecorView() != null) { - final View viewRoot = ui.getWindow().peekDecorView().getRootView(); + View viewRoot = ui.getWindow().peekDecorView().getRootView(); try { unbindDrawablesAndRecycle(viewRoot); + if (Build.VERSION.SDK_INT >= 31 && sSupportSplit) { + viewRoot = ui.getWindow().getDecorView().findViewById(android.R.id.content); + } if (viewRoot instanceof ViewGroup) { ((ViewGroup) viewRoot).removeAllViews(); } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/CanaryWorkerService.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/CanaryWorkerService.java index 6322137d1..e4943e591 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/CanaryWorkerService.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/CanaryWorkerService.java @@ -25,7 +25,6 @@ import com.tencent.matrix.Matrix; import com.tencent.matrix.resource.analyzer.model.HeapDump; -import com.tencent.matrix.resource.dumper.DumpStorageManager; import com.tencent.matrix.resource.hproflib.HprofBufferShrinker; import com.tencent.matrix.util.MatrixLog; @@ -126,9 +125,9 @@ private void doShrinkHprofAndReport(HeapDump heapDump) { private String getShrinkHprofName(File origHprof) { final String origHprofName = origHprof.getName(); - final int extPos = origHprofName.indexOf(DumpStorageManager.HPROF_EXT); + final int extPos = origHprofName.indexOf(".hprof"); final String namePrefix = origHprofName.substring(0, extPos); - return namePrefix + "_shrink" + DumpStorageManager.HPROF_EXT; + return namePrefix + "_shrink.hprof"; } private String getResultZipName(String prefix) { diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/MemoryUtil.kt b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/MemoryUtil.kt new file mode 100644 index 000000000..2fc6a86c4 --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/MemoryUtil.kt @@ -0,0 +1,464 @@ +package com.tencent.matrix.resource + +import android.os.Debug +import com.tencent.matrix.Matrix +import com.tencent.matrix.resource.analyzer.model.ActivityLeakResult +import com.tencent.matrix.resource.analyzer.model.ReferenceChain +import com.tencent.matrix.resource.analyzer.model.ReferenceTraceElement +import com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo +import com.tencent.matrix.util.MatrixLog +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.text.SimpleDateFormat +import java.util.* + +private const val TAG = "Matrix.MemoryUtil" + +private fun info(message: String) { + MatrixLog.i(TAG, message) +} + +private fun error(message: String, throwable: Throwable? = null) { + if (throwable != null) + MatrixLog.printErrStackTrace(TAG, throwable, message) + else + MatrixLog.e(TAG, message) +} + +private val currentTime: Long + get() = System.currentTimeMillis() + +private fun File.assureIsDirectory() { + if (!isDirectory) { + if (exists()) + throw IllegalStateException("Path $absolutePath is pointed to an existing element but it is not a directory.") + mkdirs() + } +} + +private class OrderedStreamWrapper( + private val order: ByteOrder, + private val stream: InputStream +) { + + fun readOrderedInt(): Int { + val buffer = ByteBuffer.allocate(4) + .apply { + order(order) + } + stream.read(buffer.array(), 0, 4) + return buffer.getInt(0) + } + + fun readString(length: Int): String { + val buffer = ByteArray(length) + stream.read(buffer) + return String(buffer, Charsets.UTF_8) + } + + fun close() { + stream.close() + } +} + +/** + * Memory non-suspending dump / analyze util. + * + * The util is used to dump / analyze heap memory into a HPROF file, like [Debug.dumpHprofData] and + * LeakCanary. However, all APIs of the util can dump memory without suspending runtime because the + * util will fork a new process for dumping (See + * [Copy-on-Write](https://en.wikipedia.org/wiki/Copy-on-write)). + * + * The idea is from [KOOM](https://github.com/KwaiAppTeam/KOOM). + * + * @author aurorani + * @since 2022/02/09 + */ +object MemoryUtil { + + private const val DEFAULT_TASK_TIMEOUT = 0L + + private val storageDir: File = + File(Matrix.with().application.cacheDir, "matrix_mem_util").apply { + assureIsDirectory() + } + + private external fun loadJniCache() + + private external fun syncTaskDir(storageDirPath: String) + + private external fun initializeSymbol() + + private val initialized: InitializeException? = run { + try { + System.loadLibrary("matrix_mem_util") + loadJniCache() + syncTaskDir(storageDir.absolutePath) + initializeSymbol() + return@run null + } catch (throwable: Throwable) { + return@run InitializeException(throwable) + } + } + + private inline fun initSafe(action: (exception: InitializeException?) -> T): T { + return action.invoke(initialized) + } + + /** + * Dumps HPROF to specific file on [hprofPath]. The function will suspend current thread. + * + * The task will be cancelled in [timeout] seconds. If [timeout] is zero, the task will be + * executed without time limitation. + * + * The function may cause jank and garbage collection, and will not throw exceptions but return + * false and print stack trace if error happened. + */ + @JvmStatic + @JvmOverloads + fun dump( + hprofPath: String, + timeout: Long = DEFAULT_TASK_TIMEOUT + ): Boolean = initSafe { exception -> + if (exception != null) { + error("", exception) + return@initSafe false + } + return when (val pid = forkDump(hprofPath, timeout)) { + -1 -> run { + error("Failed to fork task executing process.") + false + } + else -> run { // current process + info("Wait for task process [${pid}] complete executing.") + val result = waitTask(pid) + result.exception?.let { + info("Task process [${pid}] complete with error: ${it.message}.") + } ?: info("Task process [${pid}] complete without error.") + return result.exception == null + } + } + } + + private external fun forkDump(hprofPath: String, timeout: Long): Int + + private fun createTask( + hprofPath: String, + referenceKey: String, + timeout: Long, + forkTask: (String, String, String, Long) -> Int + ): ActivityLeakResult = initSafe { exception -> + if (exception != null) return@initSafe ActivityLeakResult.failure(exception, 0) + val analyzeStart = currentTime + val resultFile = createResultFile() + ?: return ActivityLeakResult.failure( + RuntimeException("Failed to create temporary analyze result file."), 0 + ) + val resultPath = resultFile.absolutePath + val leakResult = when (val pid = forkTask(hprofPath, resultPath, referenceKey, timeout)) { + -1 -> ActivityLeakResult.failure(ForkException(), 0) + else -> run { // current process + info("Wait for task process [${pid}] complete executing.") + val result = waitTask(pid) + if (result.exception != null) { + info("Task process [${pid}] complete with error: ${result.exception!!.message}.") + return@run ActivityLeakResult.failure( + result.exception, currentTime - analyzeStart + ) + } else { + info("Task process [${pid}] complete without error.") + } + return@run try { + val chains = deserialize(resultFile) + if (chains.isEmpty()) { + ActivityLeakResult.noLeak(currentTime - analyzeStart) + } else { + // TODO: support reporting multiple leak chain. + chains.first().run { + ActivityLeakResult.leakDetected( + false, + nodes.last().objectName, + convertToReferenceChain(), + currentTime - analyzeStart + ) + } + } + } catch (throwable: Throwable) { + ActivityLeakResult.failure(throwable, currentTime - analyzeStart) + } + } + } + if (resultFile.exists()) resultFile.delete() + return@initSafe leakResult + } + + /** + * Analyze existing HPROF file [hprofPath]. This function will suspend current thread. + * + * Object referring from [DestroyedActivityInfo] instance with key [referenceKey] will be marked + * as leak object while analyzing. + * + * The function may cause jank and garbage collection, and will not throw exceptions but return + * a failure result if error happened, see [ActivityLeakResult.failure]. + */ + @JvmStatic + @JvmOverloads + fun analyze( + hprofPath: String, + referenceKey: String, + timeout: Long = DEFAULT_TASK_TIMEOUT, + ): ActivityLeakResult = + createTask(hprofPath, referenceKey, timeout, ::forkAnalyze) + + private external fun forkAnalyze( + hprofPath: String, resultPath: String, referenceKey: String, + timeout: Long + ): Int + + /** + * Dumps HPROF to specific file on [hprofPath] and analyzes it immediately. The function will + * suspend current thread. + * + * Object referring from [DestroyedActivityInfo] instance with key [referenceKey] will be marked + * as leak object while analyzing. + * + * The task will be cancelled in [timeout] seconds. If [timeout] is zero, the task will be + * executed without time limitation. + * + * The function may cause jank and garbage collection, and will not throw exceptions but return + * a failure result if error happened, see [ActivityLeakResult.failure]. + */ + @JvmStatic + @JvmOverloads + fun dumpAndAnalyze( + hprofPath: String, + referenceKey: String, + timeout: Long = DEFAULT_TASK_TIMEOUT, + ): ActivityLeakResult = + createTask(hprofPath, referenceKey, timeout, ::forkDumpAndAnalyze) + + private external fun forkDumpAndAnalyze( + hprofPath: String, resultPath: String, referenceKey: String, + timeout: Long + ): Int + + private val resultDir: File + get() = File(storageDir, "result").apply { + assureIsDirectory() + } + + private val analyzeFileTimeFormat = + SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US) + + private fun createResultFile(): File? { + val time = analyzeFileTimeFormat.format(Calendar.getInstance().time) + val result = File(resultDir, "result-${time}.tmp") + return try { + result.apply { + createNewFile() + } + } catch (exception: IOException) { + error("Failed to create analyze result file on path ${result.absolutePath}.") + null + } + } + + private fun convertReferenceType(value: Int): ReferenceTraceElement.Type = + when (value) { + // It is an end-tag that can only be the last chain element reference type. + 0 -> ReferenceTraceElement.Type.INSTANCE_FIELD + 1 -> ReferenceTraceElement.Type.STATIC_FIELD + 2 -> ReferenceTraceElement.Type.INSTANCE_FIELD + 3 -> ReferenceTraceElement.Type.ARRAY_ENTRY + else -> throw IllegalArgumentException("Unsupported reference type $value") + } + + private fun convertObjectType(value: Int): ReferenceTraceElement.Holder = + when (value) { + 1 -> ReferenceTraceElement.Holder.CLASS + 2 -> ReferenceTraceElement.Holder.ARRAY + 3 -> ReferenceTraceElement.Holder.OBJECT + else -> throw IllegalArgumentException("Unsupported object type $value") + } + + private data class LeakChain(val nodes: List) { + data class Node( + val objectName: String, + val objectType: Int, + val referenceName: String, + val referenceType: Int + ) + + fun convertToReferenceChain(): ReferenceChain = + nodes.map { + ReferenceTraceElement( + it.referenceName, + convertReferenceType(it.referenceType), + convertObjectType(it.objectType), + it.objectName, + null, null, emptyList() + ) + }.let { + ReferenceChain(it) + } + } + + /** + * Deserialize the analyze result file. + * + * The basic fields of the binary output are number (unsigned 32-bit integer) and string + * (NOT null-terminated). + * + * The format of analyze result file look like: + * + * ``` + * number byte_order_magic + * number count_of_leak_chains + * leak_chain[count_of_leak_chains] leak_chains + * ``` + * + * Which with sub-records look like: + * + * ``` + * leak_chain: + * number chain_length + * chain_node[chain_length] chain_nodes + * + * chain_node: + * node node + * reference/end_tag reference + * + * node: + * number node_type + * number node_name_length + * string node_name + * + * reference: + * number reference_type + * number reference_name_length + * string reference_name + * + * end_tag: + * number always_zero_end_tag + * ``` + */ + private fun deserialize(file: File): List { + val stream = run { + val input = file.inputStream() + val magic = ByteArray(4) + input.read(magic, 0, 4) + if (magic.contentEquals(byteArrayOf(0x00, 0x00, 0x00, 0x01))) + OrderedStreamWrapper(ByteOrder.BIG_ENDIAN, input) + else + OrderedStreamWrapper(ByteOrder.LITTLE_ENDIAN, input) + } + try { + val chainCount = stream.readOrderedInt() + if (chainCount == 0) { + stream.close() + return emptyList() + } + val result = mutableListOf() + for (chainIndex in 0 until chainCount) { + val nodes = mutableListOf() + val chainLength = stream.readOrderedInt() + for (nodeIndex in 0 until chainLength) { + // node + val objectType = stream.readOrderedInt() + val objectName = stream.readString(stream.readOrderedInt()) + + // reference + val referenceType = stream.readOrderedInt() + val referenceName = + if (referenceType == 0) "" // reached end tag + else stream.readString(stream.readOrderedInt()) + + nodes += LeakChain.Node(objectName, objectType, referenceName, referenceType) + } + result += LeakChain(nodes) + } + return result + } catch (exception: IOException) { + throw exception + } finally { + stream.close() + } + } + + private object TaskState { + const val DUMP: Byte = 1 + const val ANALYZER_CREATE: Byte = 2 + const val ANALYZER_INITIALIZE: Byte = 3 + const val ANALYZER_EXECUTE: Byte = 4 + const val CREATE_RESULT_FILE: Byte = 5 + const val SERIALIZE: Byte = 6 + } + + private class TaskResult( + private val type: Int, + private val code: Int, + private val stateRaw: Byte, + val error: String + ) { + companion object { + private const val TYPE_WAIT_FAILED = -1 + private const val TYPE_EXIT = 0 + private const val TYPE_SIGNALED = 1 + } + + private val state: String + get() { + return when (stateRaw) { + TaskState.DUMP -> "dump" + TaskState.ANALYZER_CREATE -> "analyzer_create" + TaskState.ANALYZER_INITIALIZE -> "analyzer_initialize" + TaskState.ANALYZER_EXECUTE -> "analyzer_execute" + TaskState.CREATE_RESULT_FILE -> "create_result_file" + TaskState.SERIALIZE -> "serialize" + else -> "unknown" + } + } + + private val success: Boolean + get() = type == TYPE_EXIT && code == 0 + + val exception by lazy { + if (success) return@lazy null + return@lazy when (type) { + TYPE_WAIT_FAILED -> WaitException(code) + TYPE_EXIT -> UnexpectedExitException(code, state, error) + TYPE_SIGNALED -> TerminateException(code, state, error) + else -> UnknownAnalyzeException(type, code, state, error) + } + } + } + + /** + * Waits task process exits and returns exit status of child process. + * + * The function is implemented with native standard waitpid(). + * See [man wait](https://man7.org/linux/man-pages/man2/wait.2.html). + */ + private external fun waitTask(pid: Int): TaskResult + + private class InitializeException(throwable: Throwable) : + RuntimeException("Initialization failed due to: $throwable") + + private class ForkException : + RuntimeException("Failed to fork task process") + + private class WaitException(errno: Int) : + RuntimeException("Failed to wait task process with errno $errno") + + private class UnexpectedExitException(code: Int, state: String, error: String) : + RuntimeException("Task process exited with code $code unexpectedly (state: ${state}, error: ${error})") + + private class TerminateException(signal: Int, state: String, error: String) : + RuntimeException("Task process was terminated by signal $signal (state: ${state}, error: ${error})") + + private class UnknownAnalyzeException(type: Int, code: Int, state: String, error: String) : + RuntimeException("Unknown error with type $type returned from task process (code: ${code}, state: ${state}, error: ${error})") +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/ResourcePlugin.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/ResourcePlugin.java index 0220739a8..c9887fbd5 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/ResourcePlugin.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/ResourcePlugin.java @@ -20,13 +20,15 @@ import android.app.Application; import android.os.Build; +import com.tencent.matrix.lifecycle.EmptyActivityLifecycleCallbacks; import com.tencent.matrix.plugin.Plugin; import com.tencent.matrix.plugin.PluginListener; import com.tencent.matrix.resource.config.ResourceConfig; import com.tencent.matrix.resource.config.SharePluginInfo; +import com.tencent.matrix.resource.dumper.HprofFileManager; import com.tencent.matrix.resource.processor.BaseLeakProcessor; -import com.tencent.matrix.resource.watcher.ActivityLifeCycleCallbacksAdapter; import com.tencent.matrix.resource.watcher.ActivityRefWatcher; +import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; /** @@ -45,7 +47,7 @@ public ResourcePlugin(ResourceConfig config) { public static void activityLeakFixer(Application application) { // Auto break the path from Views in their holder to gc root when activity is destroyed. - application.registerActivityLifecycleCallbacks(new ActivityLifeCycleCallbacksAdapter() { + application.registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() { @Override public void onActivityDestroyed(Activity activity) { ActivityLeakFixer.fixInputMethodManagerLeak(activity); @@ -78,6 +80,12 @@ public void start() { return; } mWatcher.start(); + MatrixHandlerThread.getDefaultHandler().post(new Runnable() { + @Override + public void run() { + HprofFileManager.INSTANCE.checkExpiredFile(); + } + }); } @Override diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/config/ResourceConfig.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/config/ResourceConfig.java index 71e42aea1..aa67603c8 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/config/ResourceConfig.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/config/ResourceConfig.java @@ -49,13 +49,15 @@ public enum DumpMode { private final boolean mDetectDebugger; private final String mTargetActivity; private final String mManufacture; + private final boolean enableManualDumpNotification; - private ResourceConfig(IDynamicConfig dynamicConfig, DumpMode dumpHprofMode, boolean detectDebuger, String targetActivity, String manufacture) { + private ResourceConfig(IDynamicConfig dynamicConfig, DumpMode dumpHprofMode, boolean detectDebuger, String targetActivity, String manufacture, boolean enableManualDumpNotification) { this.mDynamicConfig = dynamicConfig; this.mDumpHprofMode = dumpHprofMode; this.mDetectDebugger = detectDebuger; this.mTargetActivity = targetActivity; this.mManufacture = manufacture; + this.enableManualDumpNotification = enableManualDumpNotification; } public long getScanIntervalMillis() { @@ -86,11 +88,16 @@ public String getManufacture() { return mManufacture; } + public boolean isManualDumpNotificationEnabled() { + return enableManualDumpNotification; + } + public static final class Builder { private DumpMode mDefaultDumpHprofMode = DEFAULT_DUMP_HPROF_MODE; private IDynamicConfig dynamicConfig; private String mTargetActivity; + private boolean enableManualDumpNotification = true; private boolean mDetectDebugger = false; private String mManufacture; @@ -109,6 +116,11 @@ public Builder setDetectDebuger(boolean enabled) { return this; } + public Builder enableManualDumpNotification(boolean enable) { + enableManualDumpNotification = enable; + return this; + } + public Builder setManualDumpTargetActivity(String targetActivity) { mTargetActivity = targetActivity; return this; @@ -120,7 +132,7 @@ public Builder setManufacture(String manufacture) { } public ResourceConfig build() { - return new ResourceConfig(dynamicConfig, mDefaultDumpHprofMode, mDetectDebugger, mTargetActivity, mManufacture); + return new ResourceConfig(dynamicConfig, mDefaultDumpHprofMode, mDetectDebugger, mTargetActivity, mManufacture, enableManualDumpNotification); } } } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/config/SharePluginInfo.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/config/SharePluginInfo.java index a5f7e6a67..4db4b158c 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/config/SharePluginInfo.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/config/SharePluginInfo.java @@ -30,8 +30,11 @@ public class SharePluginInfo { public static final String ISSUE_REF_KEY = "ref_key"; public static final String ISSUE_LEAK_DETAIL = "leak_detail"; public static final String ISSUE_COST_MILLIS = "cost_millis"; + public static final String ISSUE_RETRY_COUNT = "retry_count"; public static final String ISSUE_LEAK_PROCESS = "leak_process"; + @Deprecated public static final String ISSUE_DUMP_DATA = "dump_data"; + public static final String ISSUE_HPROF_PATH = "hprof_path"; public static final String ISSUE_NOTIFICATION_ID = "notification_id"; public static final class IssueType { @@ -39,5 +42,6 @@ public static final class IssueType { public static final int ERR_FILE_NOT_FOUND = 2; public static final int ERR_ANALYSE_OOM = 3; public static final int ERR_UNSUPPORTED_API = 4; + public static final int ERR_EXCEPTION = 5; } } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/AndroidHeapDumper.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/AndroidHeapDumper.java index 920bf4580..52e7f9eb5 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/AndroidHeapDumper.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/AndroidHeapDumper.java @@ -30,6 +30,7 @@ import com.tencent.matrix.util.MatrixLog; import java.io.File; +import java.io.FileNotFoundException; import java.util.concurrent.TimeUnit; /** @@ -42,25 +43,28 @@ public class AndroidHeapDumper { private static final String TAG = "Matrix.AndroidHeapDumper"; private final Context mContext; - private final DumpStorageManager mDumpStorageManager; private final Handler mMainHandler; public interface HeapDumpHandler { void process(HeapDump result); } - public AndroidHeapDumper(Context context, DumpStorageManager dumpStorageManager) { - this(context, dumpStorageManager, new Handler(Looper.getMainLooper())); + public AndroidHeapDumper(Context context) { + this(context, new Handler(Looper.getMainLooper())); } - public AndroidHeapDumper(Context context, DumpStorageManager dumpStorageManager, Handler mainHandler) { + public AndroidHeapDumper(Context context, Handler mainHandler) { mContext = context; - mDumpStorageManager = dumpStorageManager; mMainHandler = mainHandler; } public File dumpHeap(boolean isShowToast) { - final File hprofFile = mDumpStorageManager.newHprofFile(); + File hprofFile = null; + try { + hprofFile = HprofFileManager.INSTANCE.prepareHprofFile("", false); + } catch (FileNotFoundException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } if (null == hprofFile) { MatrixLog.w(TAG, "hprof file is null."); diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/DumpStorageManager.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/DumpStorageManager.java deleted file mode 100644 index 5d0db2cc7..000000000 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/DumpStorageManager.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2015 Square, Inc. - * - * 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 com.tencent.matrix.resource.dumper; - -import android.content.Context; -import android.os.Environment; -import android.os.Process; - -import com.tencent.matrix.util.MatrixLog; -import com.tencent.matrix.util.MatrixUtil; - -import java.io.File; -import java.io.FilenameFilter; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** - * Created by tangyinsheng on 2017/6/2. - *

- * This class is ported from LeakCanary. - */ - -public class DumpStorageManager { - private static final String TAG = "Matrix.DumpStorageManager"; - - public static final String HPROF_EXT = ".hprof"; - - public static final int DEFAULT_MAX_STORED_HPROF_FILECOUNT = 5; - - protected final Context mContext; - protected final int mMaxStoredHprofFileCount; - - public DumpStorageManager(Context context) { - this(context, DEFAULT_MAX_STORED_HPROF_FILECOUNT); - } - - public DumpStorageManager(Context context, int maxStoredHprofFileCount) { - if (maxStoredHprofFileCount <= 0) { - throw new IllegalArgumentException("illegal max stored hprof file count: " - + maxStoredHprofFileCount); - } - mContext = context; - mMaxStoredHprofFileCount = maxStoredHprofFileCount; - } - - public File newHprofFile() { - final File storageDir = prepareStorageDirectory(); - if (storageDir == null) { - return null; - } - final String hprofFileName = "dump" - + "_" + MatrixUtil.getProcessName(mContext).replace(".", "_").replace(":", "_") - + "_" + Process.myPid() - + "_" - + new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(new Date()) - + HPROF_EXT; - return new File(storageDir, hprofFileName); - } - - private File prepareStorageDirectory() { - final File storageDir = getStorageDirectory(); - if (!storageDir.exists() && (!storageDir.mkdirs() || !storageDir.canWrite())) { - MatrixLog.w(TAG, "failed to allocate new hprof file since path: %s is not writable.", - storageDir.getAbsolutePath()); - return null; - } - final File[] hprofFiles = storageDir.listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.endsWith(HPROF_EXT); - } - }); - if (hprofFiles != null && hprofFiles.length > mMaxStoredHprofFileCount) { - for (File file : hprofFiles) { - if (file.exists() && !file.delete()) { - MatrixLog.w(TAG, "faile to delete hprof file: " + file.getAbsolutePath()); - } - } - } - return storageDir; - } - - private File getStorageDirectory() { - final String sdcardState = Environment.getExternalStorageState(); - File root = null; - if (Environment.MEDIA_MOUNTED.equals(sdcardState)) { - root = mContext.getExternalCacheDir(); - } else { - root = mContext.getCacheDir(); - } - final File result = new File(root, "matrix_resource"); - - MatrixLog.i(TAG, "path to store hprof and result: %s", result.getAbsolutePath()); - - return result; - } -} diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/HprofFileManager.kt b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/HprofFileManager.kt new file mode 100644 index 000000000..cfa5f6e54 --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/dumper/HprofFileManager.kt @@ -0,0 +1,116 @@ +package com.tencent.matrix.resource.dumper + +import android.os.Process +import com.tencent.matrix.Matrix +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.MatrixUtil +import com.tencent.matrix.util.safeApply +import java.io.File +import java.io.FileNotFoundException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.TimeUnit + +/** + * manage hprof files with LRU + */ +object HprofFileManager { + + private const val TAG = "Matrix.HprofFileManager" + + private const val MAX_FILE_COUNT = 10 + private const val CLEAN_THRESHOLD = 12 * 1024 * 1024 * 1024L // 12G + private const val MIN_FREE_SPACE = 1024 * 1024 * 1024L // 1G + + private val EXPIRED_TIME_MILLIS = TimeUnit.DAYS.toMillis(7) + + private val hprofStorageDir by lazy { File(Matrix.with().application.cacheDir.absolutePath + "/hprofs") } + + private val format by lazy { SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US) } + + fun checkExpiredFile() { + safeApply(TAG) { + val current = System.currentTimeMillis() + if (hprofStorageDir.exists() && hprofStorageDir.isDirectory) { + hprofStorageDir.listFiles { it: File -> + current - it.lastModified() > EXPIRED_TIME_MILLIS + }?.forEach { + if (it.exists()) { + MatrixLog.i(TAG, "expired: ${it.absolutePath}") + it.delete() + } + } + } + } + } + + @Throws(FileNotFoundException::class) + fun prepareHprofFile(prefix: String = "", deleteSoon: Boolean = false): File { + hprofStorageDir.prepare(deleteSoon) + return File(hprofStorageDir, getHprofFileName(prefix)) + } + + fun clearAll() { + hprofStorageDir.deleteRecursively() + } + + private fun File.prepare(deleteSoon: Boolean) { + reserve() + makeSureEnoughSpace(deleteSoon) + } + + private fun File.reserve() { + if (!exists() && (!mkdirs() || !canWrite())) { + "fialed to create new hprof file since path: $absolutePath is not writable".let { + MatrixLog.e(TAG, it) + throw FileNotFoundException(it) + } + } + } + + private fun File.makeSureEnoughSpace(deleteSoon: Boolean) { + if (!isDirectory) { + return + } + lru() + if (freeSpace < CLEAN_THRESHOLD) { + listFiles()?.forEach { it.delete() } + } + if (freeSpace < if (deleteSoon) MIN_FREE_SPACE else CLEAN_THRESHOLD) { + throw FileNotFoundException("free space($freeSpace) less than $CLEAN_THRESHOLD, skip dump hprof") + } + } + + private fun File.lru() { + if (!isDirectory) { + return + } + val files = listFiles() ?: return + files.sortBy { it.lastModified() } + files.forEach { + MatrixLog.d(TAG, "==> list sorted: ${it.absolutePath}, last mod = ${it.lastModified()}") + } + if (files.size >= MAX_FILE_COUNT) { + files.take(files.size - MAX_FILE_COUNT + 1).forEach { + it.delete() + } + } + } + + private val processSuffix by lazy { + if (MatrixUtil.isInMainProcess(Matrix.with().application)) { + "Main" + } else { + val split = + MatrixUtil.getProcessName(Matrix.with().application).split(":").toTypedArray() + if (split.size > 1) { + split[1] + } else { + "unknown" + } + } + } + + private fun getHprofFileName(prefix: String) = + "$prefix-${processSuffix}-${Process.myPid()}-${format.format(Calendar.getInstance().time)}.hprof" +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/BaseLeakProcessor.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/BaseLeakProcessor.java index 0246edb46..0145fdf99 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/BaseLeakProcessor.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/BaseLeakProcessor.java @@ -16,7 +16,6 @@ import com.tencent.matrix.resource.config.ResourceConfig; import com.tencent.matrix.resource.config.SharePluginInfo; import com.tencent.matrix.resource.dumper.AndroidHeapDumper; -import com.tencent.matrix.resource.dumper.DumpStorageManager; import com.tencent.matrix.resource.watcher.ActivityRefWatcher; import com.tencent.matrix.util.MatrixLog; @@ -34,7 +33,6 @@ public abstract class BaseLeakProcessor { private final ActivityRefWatcher mWatcher; - private DumpStorageManager mDumpStorageManager; private AndroidHeapDumper mHeapDumper; private AndroidHeapDumper.HeapDumpHandler mHeapDumpHandler; @@ -44,16 +42,10 @@ public BaseLeakProcessor(ActivityRefWatcher watcher) { public abstract boolean process(DestroyedActivityInfo destroyedActivityInfo); - public DumpStorageManager getDumpStorageManager() { - if (mDumpStorageManager == null) { - mDumpStorageManager = new DumpStorageManager(mWatcher.getContext()); - } - return mDumpStorageManager; - } - + @Deprecated public AndroidHeapDumper getHeapDumper() { if (mHeapDumper == null) { - mHeapDumper = new AndroidHeapDumper(mWatcher.getContext(), getDumpStorageManager()); + mHeapDumper = new AndroidHeapDumper(mWatcher.getContext()); } return mHeapDumper; } @@ -106,6 +98,14 @@ protected ActivityLeakResult analyze(File hprofFile, String referenceKey) { } final protected void publishIssue(int issueType, ResourceConfig.DumpMode dumpMode, String activity, String refKey, String detail, String cost) { + publishIssue(issueType, dumpMode, activity, refKey, detail, cost, 0); + } + + final protected void publishIssue(int issueType, ResourceConfig.DumpMode dumpMode, String activity, String refKey, String detail, String cost, int retryCount) { + publishIssue(issueType, dumpMode, activity, refKey, detail, cost, retryCount, null); + } + + final protected void publishIssue(int issueType, ResourceConfig.DumpMode dumpMode, String activity, String refKey, String detail, String cost, int retryCount, String hprofPath) { Issue issue = new Issue(issueType); JSONObject content = new JSONObject(); try { @@ -114,6 +114,8 @@ final protected void publishIssue(int issueType, ResourceConfig.DumpMode dumpMod content.put(SharePluginInfo.ISSUE_REF_KEY, refKey); content.put(SharePluginInfo.ISSUE_LEAK_DETAIL, detail); content.put(SharePluginInfo.ISSUE_COST_MILLIS, cost); + content.put(SharePluginInfo.ISSUE_RETRY_COUNT, retryCount); + content.put(SharePluginInfo.ISSUE_HPROF_PATH, hprofPath); } catch (JSONException jsonException) { MatrixLog.printErrStackTrace(TAG, jsonException, ""); } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ForkAnalyseProcessor.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ForkAnalyseProcessor.java index eb53b4598..69afce5f5 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ForkAnalyseProcessor.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ForkAnalyseProcessor.java @@ -2,15 +2,17 @@ import android.os.Build; -import com.tencent.matrix.memorydump.MemoryDumpManager; +import com.tencent.matrix.resource.MemoryUtil; import com.tencent.matrix.resource.analyzer.model.ActivityLeakResult; import com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo; import com.tencent.matrix.resource.config.ResourceConfig; import com.tencent.matrix.resource.config.SharePluginInfo; +import com.tencent.matrix.resource.dumper.HprofFileManager; import com.tencent.matrix.resource.watcher.ActivityRefWatcher; import com.tencent.matrix.util.MatrixLog; import java.io.File; +import java.io.FileNotFoundException; /** * HPROF file analysis processor using fork dump. @@ -28,6 +30,8 @@ public ForkAnalyseProcessor(ActivityRefWatcher watcher) { @Override public boolean process(DestroyedActivityInfo destroyedActivityInfo) { + publishIssue(SharePluginInfo.IssueType.LEAK_FOUND, ResourceConfig.DumpMode.NO_DUMP, destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey, "no dump", "0"); + if (Build.VERSION.SDK_INT > ResourceConfig.FORK_DUMP_SUPPORTED_API_GUARD) { MatrixLog.e(TAG, "cannot fork-dump with unsupported API version " + Build.VERSION.SDK_INT); publishIssue( @@ -57,10 +61,15 @@ private boolean dumpAndAnalyse(String activity, String key) { final long dumpStart = System.currentTimeMillis(); - final File hprof = getDumpStorageManager().newHprofFile(); + File hprof = null; + try { + hprof = HprofFileManager.INSTANCE.prepareHprofFile("FAP", true); + } catch (FileNotFoundException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } if (hprof != null) { - if (!MemoryDumpManager.dumpBlock(hprof.getPath())) { + if (!MemoryUtil.dump(hprof.getPath(), 600)) { MatrixLog.e(TAG, String.format("heap dump for further analyzing activity with key [%s] was failed, just ignore.", key)); return false; diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ForkDumpProcessor.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ForkDumpProcessor.java index 27d62e231..d498a1514 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ForkDumpProcessor.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ForkDumpProcessor.java @@ -2,14 +2,17 @@ import android.os.Build; -import com.tencent.matrix.memorydump.MemoryDumpManager; +import com.tencent.matrix.resource.MemoryUtil; import com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo; import com.tencent.matrix.resource.analyzer.model.HeapDump; import com.tencent.matrix.resource.config.ResourceConfig; +import com.tencent.matrix.resource.config.SharePluginInfo; +import com.tencent.matrix.resource.dumper.HprofFileManager; import com.tencent.matrix.resource.watcher.ActivityRefWatcher; import com.tencent.matrix.util.MatrixLog; import java.io.File; +import java.io.FileNotFoundException; /** * HPROF file dump processor using fork dump. @@ -27,6 +30,8 @@ public ForkDumpProcessor(ActivityRefWatcher watcher) { @Override public boolean process(DestroyedActivityInfo destroyedActivityInfo) { + publishIssue(SharePluginInfo.IssueType.LEAK_FOUND, ResourceConfig.DumpMode.NO_DUMP, destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey, "no dump", "0"); + if (Build.VERSION.SDK_INT > ResourceConfig.FORK_DUMP_SUPPORTED_API_GUARD) { MatrixLog.e(TAG, "unsupported API version " + Build.VERSION.SDK_INT); return false; @@ -34,14 +39,19 @@ public boolean process(DestroyedActivityInfo destroyedActivityInfo) { final long dumpStart = System.currentTimeMillis(); - final File hprof = getDumpStorageManager().newHprofFile(); + File hprof = null; + try { + hprof = HprofFileManager.INSTANCE.prepareHprofFile("FDP", true); + } catch (FileNotFoundException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } if (hprof == null) { MatrixLog.e(TAG, "cannot create hprof file, just ignore"); return true; } - if (!MemoryDumpManager.dumpBlock(hprof.getPath())) { + if (!MemoryUtil.dump(hprof.getPath(), 600)) { MatrixLog.e(TAG, String.format("heap dump for further analyzing activity with key [%s] was failed, just ignore.", destroyedActivityInfo.mKey)); return true; diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/LazyForkAnalyzeProcessor.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/LazyForkAnalyzeProcessor.java index 4bfd23acd..fd23bd76a 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/LazyForkAnalyzeProcessor.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/LazyForkAnalyzeProcessor.java @@ -6,16 +6,18 @@ import android.content.IntentFilter; import android.os.Build; -import com.tencent.matrix.memorydump.MemoryDumpManager; +import com.tencent.matrix.resource.MemoryUtil; import com.tencent.matrix.resource.analyzer.model.ActivityLeakResult; import com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo; import com.tencent.matrix.resource.config.ResourceConfig; import com.tencent.matrix.resource.config.SharePluginInfo; +import com.tencent.matrix.resource.dumper.HprofFileManager; import com.tencent.matrix.resource.watcher.ActivityRefWatcher; import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; import java.io.File; +import java.io.FileNotFoundException; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; @@ -111,6 +113,8 @@ public void onDestroy() { @Override public boolean process(DestroyedActivityInfo destroyedActivityInfo) { + publishIssue(SharePluginInfo.IssueType.LEAK_FOUND, ResourceConfig.DumpMode.NO_DUMP, destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey, "no dump", "0"); + if (Build.VERSION.SDK_INT > ResourceConfig.FORK_DUMP_SUPPORTED_API_GUARD) { MatrixLog.e(TAG, "cannot fork-dump with unsupported API version " + Build.VERSION.SDK_INT); publishIssue( @@ -138,10 +142,15 @@ private boolean dumpAndAnalyse(String activity, String key) { final long dumpStart = System.currentTimeMillis(); - final File hprof = getDumpStorageManager().newHprofFile(); + File hprof = null; + try { + hprof = HprofFileManager.INSTANCE.prepareHprofFile("LFAP", true); + } catch (FileNotFoundException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } if (hprof != null) { - if (!MemoryDumpManager.dumpBlock(hprof.getPath())) { + if (!MemoryUtil.dump(hprof.getPath(), 600)) { MatrixLog.e(TAG, String.format("heap dump for further analyzing activity with key [%s] was failed, just ignore.", key)); return false; diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ManualDumpProcessor.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ManualDumpProcessor.java index 3da36c114..3fa377160 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ManualDumpProcessor.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/ManualDumpProcessor.java @@ -1,5 +1,7 @@ package com.tencent.matrix.resource.processor; +import static android.os.Build.VERSION.SDK_INT; + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -7,30 +9,24 @@ import android.content.Context; import android.content.Intent; import android.os.Build; -import android.os.Parcel; -import android.os.Parcelable; +import android.util.Pair; -import com.tencent.matrix.memorydump.MemoryDumpManager; +import com.tencent.matrix.resource.MemoryUtil; import com.tencent.matrix.resource.R; import com.tencent.matrix.resource.analyzer.model.ActivityLeakResult; import com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo; import com.tencent.matrix.resource.config.ResourceConfig; import com.tencent.matrix.resource.config.SharePluginInfo; +import com.tencent.matrix.resource.dumper.HprofFileManager; import com.tencent.matrix.resource.watcher.ActivityRefWatcher; -import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; import com.tencent.matrix.util.MatrixUtil; import java.io.File; -import java.util.ArrayList; -import java.util.List; +import java.io.FileNotFoundException; import java.util.Locale; import java.util.concurrent.TimeUnit; -import static android.os.Build.VERSION.SDK_INT; - -import androidx.annotation.Nullable; - /** * X process leaked -> send notification -> main process activity -> dump and analyse in X process -> show result in main process activity * Created by Yves on 2021/3/4 @@ -45,8 +41,6 @@ public class ManualDumpProcessor extends BaseLeakProcessor { private final NotificationManager mNotificationManager; - private final List mLeakedActivities = new ArrayList<>(); - private boolean isMuted; public ManualDumpProcessor(ActivityRefWatcher watcher, String targetActivity) { @@ -64,8 +58,6 @@ public boolean process(final DestroyedActivityInfo destroyedActivityInfo) { return true; } - mLeakedActivities.add(destroyedActivityInfo); - MatrixLog.i(TAG, "show notification for activity leak. %s", destroyedActivityInfo.mActivityName); if (isMuted) { @@ -73,24 +65,26 @@ public boolean process(final DestroyedActivityInfo destroyedActivityInfo) { return true; } - dumpAndAnalyzeAsync(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey, new ManualDumpCallback() { - @Override - public void onDumpComplete(@Nullable ManualDumpData data) { - if (data != null) { - if (!isMuted) { - MatrixLog.i(TAG, "shown notification!!!3"); - sendResultNotification(destroyedActivityInfo, data); - } else { - MatrixLog.i(TAG, "mute mode, notification will not be shown."); - } - } + Pair data = dumpAndAnalyse(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey); + if (data != null) { + if (!isMuted) { + MatrixLog.i(TAG, "shown notification!!!3"); + sendResultNotification(destroyedActivityInfo, data.first, data.second); + } else { + MatrixLog.i(TAG, "mute mode, notification will not be shown."); } - }); + } + return true; } - private void sendResultNotification(DestroyedActivityInfo activityInfo, ManualDumpData data) { + private void sendResultNotification(DestroyedActivityInfo activityInfo, String hprofPath, String refChain) { + if (!getWatcher().getResourcePlugin().getConfig().isManualDumpNotificationEnabled()) { + MatrixLog.i(TAG, "Manual dump notification is disabled"); + return; + } + final Context context = getWatcher().getContext(); Intent targetIntent = new Intent(); @@ -98,9 +92,10 @@ private void sendResultNotification(DestroyedActivityInfo activityInfo, ManualDu targetIntent.putExtra(SharePluginInfo.ISSUE_ACTIVITY_NAME, activityInfo.mActivityName); targetIntent.putExtra(SharePluginInfo.ISSUE_REF_KEY, activityInfo.mKey); targetIntent.putExtra(SharePluginInfo.ISSUE_LEAK_PROCESS, MatrixUtil.getProcessName(context)); - targetIntent.putExtra(SharePluginInfo.ISSUE_DUMP_DATA, data); + targetIntent.putExtra(SharePluginInfo.ISSUE_HPROF_PATH, hprofPath); + targetIntent.putExtra(SharePluginInfo.ISSUE_LEAK_DETAIL, refChain); - PendingIntent pIntent = PendingIntent.getActivity(context, 0, targetIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pIntent = PendingIntent.getActivity(context, 0, targetIntent, Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT); String dumpingHeapTitle = context.getString(R.string.resource_canary_leak_tip); ResourceConfig config = getWatcher().getResourcePlugin().getConfig(); @@ -149,19 +144,6 @@ public void setMuted(boolean mute) { isMuted = mute; } - private void dumpAndAnalyzeAsync(final String activity, final String refString, final ManualDumpCallback callback) { - MatrixHandlerThread.getDefaultHandler().postAtFrontOfQueue(new Runnable() { - @Override - public void run() { - callback.onDumpComplete(dumpAndAnalyse(activity, refString)); - } - }); - } - - private interface ManualDumpCallback { - void onDumpComplete(@Nullable ManualDumpData data); - } - /** * run in leaked process * @@ -169,90 +151,42 @@ private interface ManualDumpCallback { * @param key * @return */ - private ManualDumpData dumpAndAnalyse(String activity, String key) { - long dumpStart = System.currentTimeMillis(); + private Pair dumpAndAnalyse(String activity, String key) { getWatcher().triggerGc(); - File file = getDumpStorageManager().newHprofFile(); - if (file != null) { - MemoryDumpManager.dumpBlock(file.getPath()); + File file = null; + try { + file = HprofFileManager.INSTANCE.prepareHprofFile("MDP", false); + } catch (FileNotFoundException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); } - if (file == null || file.length() <= 0) { - publishIssue( - SharePluginInfo.IssueType.ERR_FILE_NOT_FOUND, - ResourceConfig.DumpMode.MANUAL_DUMP, - activity, key, "FileNull", "0"); - MatrixLog.e(TAG, "file is null!"); + + if (file == null) { + MatrixLog.e(TAG, "prepare hprof file failed, see log above"); return null; } - MatrixLog.i(TAG, String.format("dump cost=%sms refString=%s path=%s", - System.currentTimeMillis() - dumpStart, key, file.getAbsolutePath())); - - long analyseBegin = System.currentTimeMillis(); - try { - final ActivityLeakResult result = analyze(file, key); - MatrixLog.i(TAG, String.format("analyze cost=%sms refString=%s", - System.currentTimeMillis() - analyseBegin, key)); - String leakChain = result.toString(); - if (result.mLeakFound) { - MatrixLog.i(TAG, "leakFound,refcChain = %s", leakChain); - publishIssue( - SharePluginInfo.IssueType.LEAK_FOUND, - ResourceConfig.DumpMode.MANUAL_DUMP, - activity, key, leakChain, - String.valueOf(System.currentTimeMillis() - dumpStart)); - return new ManualDumpData(file.getAbsolutePath(), leakChain); - } else { - MatrixLog.i(TAG, "leak not found"); - return new ManualDumpData(file.getAbsolutePath(), null); - } - } catch (OutOfMemoryError error) { + final ActivityLeakResult result = MemoryUtil.dumpAndAnalyze(file.getAbsolutePath(), key, 600); + if (result.mLeakFound) { + final String leakChain = result.toString(); publishIssue( - SharePluginInfo.IssueType.ERR_ANALYSE_OOM, + SharePluginInfo.IssueType.LEAK_FOUND, ResourceConfig.DumpMode.MANUAL_DUMP, - activity, key, "OutOfMemoryError", "0"); - MatrixLog.printErrStackTrace(TAG, error.getCause(), ""); - } - return null; - } - - public static class ManualDumpData implements Parcelable { - public final String hprofPath; - public final String refChain; - - public ManualDumpData(String hprofPath, String refChain) { - this.hprofPath = hprofPath; - this.refChain = refChain; - } - - protected ManualDumpData(Parcel in) { - hprofPath = in.readString(); - refChain = in.readString(); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(hprofPath); - dest.writeString(refChain); - } - - @Override - public int describeContents() { - return 0; + activity, key, leakChain, String.valueOf(result.mAnalysisDurationMs), + 0, + file.getAbsolutePath() + ); + return new Pair<>(file.getAbsolutePath(), leakChain); + } else if (result.mFailure != null) { + publishIssue( + SharePluginInfo.IssueType.ERR_EXCEPTION, + ResourceConfig.DumpMode.MANUAL_DUMP, + activity, key, result.mFailure.toString(), "0" + ); + return null; + } else { + return new Pair<>(file.getAbsolutePath(), null); } - - public static final Creator CREATOR = new Creator() { - @Override - public ManualDumpData createFromParcel(Parcel in) { - return new ManualDumpData(in); - } - - @Override - public ManualDumpData[] newArray(int size) { - return new ManualDumpData[size]; - } - }; } } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/NativeForkAnalyzeProcessor.kt b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/NativeForkAnalyzeProcessor.kt new file mode 100644 index 000000000..1efddbfb0 --- /dev/null +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/NativeForkAnalyzeProcessor.kt @@ -0,0 +1,248 @@ +package com.tencent.matrix.resource.processor + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.tencent.matrix.resource.MemoryUtil +import com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo +import com.tencent.matrix.resource.config.ResourceConfig +import com.tencent.matrix.resource.config.SharePluginInfo +import com.tencent.matrix.resource.dumper.HprofFileManager +import com.tencent.matrix.resource.watcher.ActivityRefWatcher +import com.tencent.matrix.util.MatrixLog +import com.tencent.matrix.util.MatrixUtil +import com.tencent.matrix.util.safeLet +import com.tencent.matrix.util.safeLetOrNull +import java.io.File +import java.util.* + +private fun File.deleteIfExist() { + if (exists()) delete() +} + +private const val TAG = "Matrix.LeakProcessor.NativeForkAnalyze" + +private class RetryRepository(private val dir: File) { + + private val hprofDir: File + get() = File(dir, "hprof").apply { + if (!exists()) mkdirs() + } + + private val keyDir: File + get() = File(dir, "key").apply { + if (!exists()) mkdirs() + } + + /** + * The lock used to avoid processing uncopied files. + */ + private val accessLock = Any() + + fun save(hprof: File, activity: String, key: String, failure: String): Boolean { + try { + if (!hprof.isFile) return false + val id = UUID.randomUUID().toString() + MatrixLog.i(TAG, "Save HPROF analyze retry record ${activity}(${id}).") + synchronized(accessLock) { + File(hprofDir, id) + .also { + hprof.copyTo(it, true) + } + File(keyDir, id) + .apply { + createNewFile() + } + .bufferedWriter() + .use { + it.write(activity) + it.newLine() + it.write(key) + it.newLine() + it.write(failure) + it.flush() + } + } + return true + } catch (throwable: Throwable) { + MatrixLog.printErrStackTrace( + TAG, throwable, + "Failed to save HPROF record into retry repository" + ) + return false + } + } + + fun process(action: (File, File, String, String, String) -> Unit) { + val hprofs = synchronized(accessLock) { + hprofDir.listFiles() ?: emptyArray() + } + hprofs.forEach { hprofFile -> + val keyFile = File(keyDir, hprofFile.name) + try { + if (keyFile.isFile) { + val (activity, key, failure) = keyFile.bufferedReader().use { + Triple(it.readLine(), it.readLine(), it.readLine()) + } + action.invoke(hprofFile, keyFile, activity, key, failure) + } + } catch (throwable: Throwable) { + MatrixLog.printErrStackTrace( + TAG, throwable, + "Failed to read HPROF record from retry repository" + ) + } finally { + hprofFile.deleteIfExist() + keyFile.deleteIfExist() + } + } + } +} + +class NativeForkAnalyzeProcessor(watcher: ActivityRefWatcher) : BaseLeakProcessor(watcher) { + + companion object { + private const val RETRY_REPO_NAME = "matrix_res_process_retry" + } + + private val retryRepo: RetryRepository? by lazy { + try { + File(watcher.context.cacheDir, RETRY_REPO_NAME) + .apply { + if (!isDirectory) { + deleteIfExist() + mkdirs() + } + } + .let { + RetryRepository(it) + } + } catch (throwable: Throwable) { + MatrixLog.printErrStackTrace(TAG, throwable, "Failed to initialize retry repository") + null + } + } + + private val screenStateReceiver by lazy { + return@lazy object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + // run in detector thread to prevent parallel analyzing + watcher.detectHandler.post { + // fixme : should separate native analyse and java analyse into smaller parts. + // It is possible to unlock screen and turn foreground when analyzing + retryRepo?.process { hprof, keyFile, activity, key, failure -> + MatrixLog.i(TAG, "Found record ${activity}(${hprof.name}).") + val historyFailure = mutableListOf().apply { + add(failure) + } + var retryCount = 1 + var result = MemoryUtil.analyze(hprof.absolutePath, key, timeout = 1200) + if (result.mFailure != null) { + historyFailure.add(result.mFailure.toString()) +// retryCount++ +// safeLetOrNull(TAG) { +// HprofFileManager.prepareHprofFile("RETRY", false) // prevent duplicated analyse after OOM +// }?.let { cpy -> +// hprof.copyTo(cpy, true) +// hprof.deleteIfExist() +// keyFile.deleteIfExist() +// safeLet({ +// result = analyze(cpy, key) // if crashed, the copied file could be auto-cleared by HprofFileManager later (lru or expired) +// }, success = { cpy.deleteIfExist() }, failed = { cpy.deleteIfExist() }) +// } + } + if (result.mLeakFound) { + watcher.markPublished(activity, false) + result.toString().let { + publishIssue( + SharePluginInfo.IssueType.LEAK_FOUND, + ResourceConfig.DumpMode.FORK_ANALYSE, + activity, key, it, + result.mAnalysisDurationMs.toString(), retryCount + ) + MatrixLog.i(TAG, "retry leak found: $it") + } + } else if (result.mFailure != null) { + historyFailure.add(result.mFailure.toString()) + publishIssue( + SharePluginInfo.IssueType.ERR_EXCEPTION, + ResourceConfig.DumpMode.FORK_ANALYSE, + activity, key, historyFailure.joinToString(";"), + result.mAnalysisDurationMs.toString(), retryCount + ) + } + } + } + } + } + } + + init { + if (MatrixUtil.isInMainProcess(watcher.context)) { + try { + IntentFilter().apply { + addAction(Intent.ACTION_SCREEN_OFF) + }.let { + screenStateReceiver.also { receiver -> + MatrixLog.i(TAG, "Screen state receiver $receiver registered.") + watcher.resourcePlugin.application.registerReceiver(receiver, it) + } + } + } catch (throwable: Throwable) { + MatrixLog.printErrStackTrace(TAG, throwable, "") + } + } + } + + override fun process(destroyedActivityInfo: DestroyedActivityInfo): Boolean { + publishIssue(SharePluginInfo.IssueType.LEAK_FOUND, ResourceConfig.DumpMode.NO_DUMP, destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey, "no dump", "0") + + val hprof = safeLetOrNull { HprofFileManager.prepareHprofFile("NFAP", true) } ?: run { + publishIssue( + SharePluginInfo.IssueType.LEAK_FOUND, + ResourceConfig.DumpMode.FORK_ANALYSE, + destroyedActivityInfo.mActivityName, "[unknown]", "Failed to create hprof file.", "0" + ) + return true + } + + val activity = destroyedActivityInfo.mActivityName + val key = destroyedActivityInfo.mKey + + watcher.triggerGc() + + MatrixLog.i(TAG, "fork dump and analyse") + val result = MemoryUtil.dumpAndAnalyze(hprof.absolutePath, key, timeout = 600) + MatrixLog.i(TAG, "fork dump and analyse done") + if (result.mFailure != null) { + // Copies file to retry repository and analyzes it again when the screen is locked. + MatrixLog.i(TAG, "Process failed, move into retry repository.") + val failure = result.mFailure.toString() + if (retryRepo?.save(hprof, activity, key, failure) != true) { + publishIssue( + SharePluginInfo.IssueType.ERR_EXCEPTION, + ResourceConfig.DumpMode.FORK_ANALYSE, + activity, key, failure, + result.mAnalysisDurationMs.toString() + ) + } + } else { + if (result.mLeakFound) { + watcher.markPublished(activity, false) + result.toString().let { + publishIssue( + SharePluginInfo.IssueType.LEAK_FOUND, + ResourceConfig.DumpMode.FORK_ANALYSE, + activity, key, it, result.mAnalysisDurationMs.toString() + ) + MatrixLog.i(TAG, "leak found: $it") + } + } + } + + hprof.deleteIfExist() + + return true + } +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/NoDumpProcessor.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/NoDumpProcessor.java index d70c6ed3d..48e30a41f 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/NoDumpProcessor.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/NoDumpProcessor.java @@ -22,7 +22,6 @@ public boolean process(DestroyedActivityInfo destroyedActivityInfo) { // Lightweight mode, just report leaked activity name. MatrixLog.i(TAG, "lightweight mode, just report leaked activity name."); getWatcher().markPublished(destroyedActivityInfo.mActivityName); - getWatcher().triggerGc(); publishIssue(SharePluginInfo.IssueType.LEAK_FOUND, ResourceConfig.DumpMode.NO_DUMP, destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey, "no dump", "0"); diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/SilenceAnalyseProcessor.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/SilenceAnalyseProcessor.java index 3362abbc3..446bc5f05 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/SilenceAnalyseProcessor.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/processor/SilenceAnalyseProcessor.java @@ -55,6 +55,7 @@ public void onReceive(Context context, Intent intent) { @Override public boolean process(DestroyedActivityInfo destroyedActivityInfo) { + publishIssue(SharePluginInfo.IssueType.LEAK_FOUND, ResourceConfig.DumpMode.NO_DUMP, destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey, "no dump", "0"); return onLeak(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey); } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/watcher/ActivityLifeCycleCallbacksAdapter.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/watcher/ActivityLifeCycleCallbacksAdapter.java index 9fd6c392e..437024e45 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/watcher/ActivityLifeCycleCallbacksAdapter.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/watcher/ActivityLifeCycleCallbacksAdapter.java @@ -16,48 +16,11 @@ package com.tencent.matrix.resource.watcher; -import android.app.Activity; -import android.app.Application; -import android.os.Bundle; +import com.tencent.matrix.lifecycle.EmptyActivityLifecycleCallbacks; /** * Created by tangyinsheng on 2017/6/2. */ - -public class ActivityLifeCycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks { - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - // Override it if needed. - } - - @Override - public void onActivityStarted(Activity activity) { - // Override it if needed. - } - - @Override - public void onActivityResumed(Activity activity) { - // Override it if needed. - } - - @Override - public void onActivityPaused(Activity activity) { - // Override it if needed. - } - - @Override - public void onActivityStopped(Activity activity) { - // Override it if needed. - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - // Override it if needed. - } - - @Override - public void onActivityDestroyed(Activity activity) { - // Override it if needed. - } +@Deprecated +public class ActivityLifeCycleCallbacksAdapter extends EmptyActivityLifecycleCallbacks { } diff --git a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/watcher/ActivityRefWatcher.java b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/watcher/ActivityRefWatcher.java index 8151c313b..39e30a970 100644 --- a/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/watcher/ActivityRefWatcher.java +++ b/matrix/matrix-android/matrix-resource-canary/matrix-resource-canary-android/src/main/java/com/tencent/matrix/resource/watcher/ActivityRefWatcher.java @@ -21,16 +21,17 @@ import android.os.Handler; import android.os.HandlerThread; +import com.tencent.matrix.lifecycle.EmptyActivityLifecycleCallbacks; import com.tencent.matrix.report.FilePublisher; import com.tencent.matrix.resource.ResourcePlugin; import com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo; import com.tencent.matrix.resource.config.ResourceConfig; import com.tencent.matrix.resource.processor.AutoDumpProcessor; import com.tencent.matrix.resource.processor.BaseLeakProcessor; -import com.tencent.matrix.resource.processor.ForkAnalyseProcessor; import com.tencent.matrix.resource.processor.ForkDumpProcessor; import com.tencent.matrix.resource.processor.LazyForkAnalyzeProcessor; import com.tencent.matrix.resource.processor.ManualDumpProcessor; +import com.tencent.matrix.resource.processor.NativeForkAnalyzeProcessor; import com.tencent.matrix.resource.processor.NoDumpProcessor; import com.tencent.matrix.resource.processor.SilenceAnalyseProcessor; import com.tencent.matrix.resource.watcher.RetryableTaskExecutor.RetryableTask; @@ -99,7 +100,7 @@ private BaseLeakProcessor createLeakProcess(ResourceConfig.DumpMode dumpMode, Ac case FORK_DUMP: return new ForkDumpProcessor(watcher); case FORK_ANALYSE: - return new ForkAnalyseProcessor(watcher); + return new NativeForkAnalyzeProcessor(watcher); case LAZY_FORK_ANALYZE: return new LazyForkAnalyzeProcessor(watcher); case NO_DUMP: @@ -143,7 +144,7 @@ public void onForeground(boolean isForeground) { } } - private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() { + private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new EmptyActivityLifecycleCallbacks() { @Override public void onActivityDestroyed(Activity activity) { @@ -201,6 +202,7 @@ && isPublished(activityName)) { final UUID uuid = UUID.randomUUID(); final StringBuilder keyBuilder = new StringBuilder(); keyBuilder.append(ACTIVITY_REFKEY_PREFIX).append(activityName) + .append("@").append(activity.hashCode()) .append('_').append(Long.toHexString(uuid.getMostSignificantBits())).append(Long.toHexString(uuid.getLeastSignificantBits())); final String key = keyBuilder.toString(); final DestroyedActivityInfo destroyedActivityInfo @@ -249,8 +251,6 @@ public Status execute() { // final WeakReference sentinelRef = new WeakReference<>(new Object[1024 * 1024]); // alloc big object triggerGc(); - triggerGc(); - triggerGc(); // if (sentinelRef.get() != null) { // // System ignored our gc request, we will retry later. // MatrixLog.d(TAG, "system ignore our gc request, wait for next detection."); @@ -295,14 +295,12 @@ && isPublished(destroyedActivityInfo.mActivityName)) { throw new NullPointerException("LeakProcessor not found!!!"); } - triggerGc(); if (mLeakProcessor.process(destroyedActivityInfo)) { MatrixLog.i(TAG, "the leaked activity [%s] with key [%s] has been processed. stop polling", destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey); infoIt.remove(); } } - triggerGc(); return Status.RETRY; } }; @@ -315,11 +313,24 @@ public ResourcePlugin getResourcePlugin() { return mResourcePlugin; } + public Handler getDetectHandler() { + return mHandler; + } + public Collection getDestroyedActivityInfos() { return mDestroyedActivityInfos; } + private long lastTriggeredTime = 0; + public void triggerGc() { + long current = System.currentTimeMillis(); + if (mDumpHprofMode == ResourceConfig.DumpMode.NO_DUMP + && current - lastTriggeredTime < getResourcePlugin().getConfig().getScanIntervalMillis() / 2 - 100) { + MatrixLog.v(TAG, "skip triggering gc for frequency"); + return; + } + lastTriggeredTime = current; MatrixLog.v(TAG, "triggering gc..."); Runtime.getRuntime().gc(); try { diff --git a/matrix/matrix-android/matrix-sqlite-lint/matrix-sqlite-lint-android-sdk/AndroidBuildSQLiteLint.cmake b/matrix/matrix-android/matrix-sqlite-lint/matrix-sqlite-lint-android-sdk/AndroidBuildSQLiteLint.cmake index e37cbb0da..2d0c0c4c3 100644 --- a/matrix/matrix-android/matrix-sqlite-lint/matrix-sqlite-lint-android-sdk/AndroidBuildSQLiteLint.cmake +++ b/matrix/matrix-android/matrix-sqlite-lint/matrix-sqlite-lint-android-sdk/AndroidBuildSQLiteLint.cmake @@ -21,4 +21,9 @@ TARGET_INCLUDE_DIRECTORIES(SqliteLint-lib PRIVATE ${EXT_DEP}/include) # link the log find_library( log-lib log ) -target_link_libraries(SqliteLint-lib ${log-lib} ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a) \ No newline at end of file +target_link_libraries( + SqliteLint-lib + PRIVATE ${log-lib} + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libsemi_dlfcn.a +) \ No newline at end of file diff --git a/matrix/matrix-android/matrix-trace-canary/CMakeLists.txt b/matrix/matrix-android/matrix-trace-canary/CMakeLists.txt index 887cc2061..0f74c6bf1 100644 --- a/matrix/matrix-android/matrix-trace-canary/CMakeLists.txt +++ b/matrix/matrix-android/matrix-trace-canary/CMakeLists.txt @@ -14,8 +14,14 @@ add_library(trace-canary src/main/cpp/nativehelper/managed_jnienv.cc ) -TARGET_INCLUDE_DIRECTORIES(trace-canary PRIVATE ${EXT_DEP}/include) +target_include_directories( + trace-canary + PRIVATE ${EXT_DEP}/include + PRIVATE ${EXT_DEP}/include/backtrace + PRIVATE ${EXT_DEP}/include/backtrace/common +) +TARGET_INCLUDE_DIRECTORIES(trace-canary PRIVATE ${EXT_DEP}/include) find_library( log-lib log ) @@ -24,6 +30,8 @@ find_library( log-lib target_link_libraries( trace-canary PRIVATE ${log-lib} PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libsemi_dlfcn.a + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libwechatbacktrace.so ) set_target_properties(trace-canary PROPERTIES diff --git a/matrix/matrix-android/matrix-trace-canary/build.gradle b/matrix/matrix-android/matrix-trace-canary/build.gradle index d7bc40620..70adce006 100644 --- a/matrix/matrix-android/matrix-trace-canary/build.gradle +++ b/matrix/matrix-android/matrix-trace-canary/build.gradle @@ -17,7 +17,7 @@ android { cppFlags "-std=gnu++11 -frtti -fexceptions" } ndk { - abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" + abiFilters "armeabi-v7a", "arm64-v8a" } } @@ -69,6 +69,7 @@ dependencies { testImplementation 'junit:junit:4.12' implementation project(':matrix-android-lib') implementation project(':matrix-android-commons') + implementation project(':matrix-backtrace') } version = rootProject.ext.VERSION_NAME diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/cpp/MatrixTracer.cc b/matrix/matrix-android/matrix-trace-canary/src/main/cpp/MatrixTracer.cc index 5ad393ed4..2a9a0d553 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/cpp/MatrixTracer.cc +++ b/matrix/matrix-android/matrix-trace-canary/src/main/cpp/MatrixTracer.cc @@ -31,6 +31,11 @@ #include #include #include +#include + +#include "BacktraceDefine.h" +#include "Backtrace.h" +#include "cxxabi.h" #include #include @@ -55,9 +60,12 @@ #define HOOK_CONNECT_PATH "/dev/socket/tombstoned_java_trace" #define HOOK_OPEN_PATH "/data/anr/traces.txt" #define VALIDATE_RET 50 +#define KEY_VALID_FLAG (1 << 31) -#define HOOK_REQUEST_GROUPID_THREAD_PRIO_TRACE 0x01 +#define HOOK_REQUEST_GROUPID_THREAD_PRIORITY_TRACE 0x01 +#define HOOK_REQUEST_GROUPID_THREAD_PTHREAD_KEY_TRACE 0x13 #define HOOK_REQUEST_GROUPID_TOUCH_EVENT_TRACE 0x07 +#define HOOK_REQUEST_GROUPID_ANR_DUMP_TRACE 0x12 using namespace MatrixTracer; using namespace std; @@ -84,6 +92,7 @@ static struct StacktraceJNI { jmethodID ThreadPriorityDetective_onMainThreadPriorityModified; jmethodID ThreadPriorityDetective_onMainThreadTimerSlackModified; + jmethodID ThreadPriorityDetective_pthreadKeyCallback; jmethodID TouchEventLagTracer_onTouchEvenLag; jmethodID TouchEventLagTracer_onTouchEvenLagDumpTrace; @@ -91,7 +100,6 @@ static struct StacktraceJNI { int (*original_setpriority)(int __which, id_t __who, int __priority); int my_setpriority(int __which, id_t __who, int __priority) { - if ((__who == 0 && getpid() == gettid()) || __who == getpid()) { int priorityBefore = getpriority(__which, __who); JNIEnv *env = JniInvocation::getEnv(); @@ -223,6 +231,106 @@ ssize_t my_sendto(int sockfd, const void *buf, size_t len, int flags, return ret; } +void pthreadKeyCallback(int type, int ret, int keySeq, const char* soName, const char* backtrace) { + JNIEnv *env = JniInvocation::getEnv(); + if (!env) return; + + jstring soNameJS = env->NewStringUTF(soName); + jstring nativeBacktraceJS = env->NewStringUTF(backtrace); + + env->CallStaticVoidMethod(gJ.ThreadPriorityDetective, gJ.ThreadPriorityDetective_pthreadKeyCallback, type, ret, keySeq, soNameJS, nativeBacktraceJS); + env->DeleteLocalRef(soNameJS); + env->DeleteLocalRef(nativeBacktraceJS); +} + +void makeNativeStack(wechat_backtrace::Backtrace* backtrace, char *&stack) { + std::string caller_so_name; + std::stringstream full_stack_builder; + std::stringstream brief_stack_builder; + std::string last_so_name; + int index = 0; + auto _callback = [&](wechat_backtrace::FrameDetail it) { + std::string so_name = it.map_name; + + char *demangled_name = nullptr; + int status = 0; + + demangled_name = abi::__cxa_demangle(it.function_name, nullptr, 0, &status); + + if (strstr(it.map_name, "libtrace-canary.so") || strstr(it.map_name, "libwechatbacktrace.so")) { + return; + } + + full_stack_builder + << "#" << std::dec << (index++) + << " pc " << std::hex << it.rel_pc << " " + << it.map_name + << " (" + << (demangled_name ? demangled_name : "null") + << ")" + << std::endl; + if (last_so_name != it.map_name) { + last_so_name = it.map_name; + brief_stack_builder << it.map_name << ";"; + } + + brief_stack_builder << std::hex << it.rel_pc << ";"; + + if (demangled_name) { + free(demangled_name); + } + }; + + wechat_backtrace::restore_frame_detail(backtrace->frames.get(), backtrace->frame_size, + _callback); + + stack = new char[full_stack_builder.str().size() + 1]; + strcpy(stack, full_stack_builder.str().c_str()); +} + +static char* getNativeBacktrace() { + wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( + 16); + wechat_backtrace::unwind_adapter(backtrace_zero.frames.get(), backtrace_zero.max_frames, + backtrace_zero.frame_size); + char* nativeStack; + makeNativeStack(&backtrace_zero, nativeStack); + return nativeStack; +} + +int (*original_pthread_key_create)(pthread_key_t *key, void (*destructor)(void*)); +int my_pthread_key_create(pthread_key_t *key, void (*destructor)(void*)) { + int ret = original_pthread_key_create(key, destructor); + int keySeq = *key & ~KEY_VALID_FLAG; + + void * __caller_addr = __builtin_return_address(0); + Dl_info dl_info; + dladdr(__caller_addr, &dl_info); + const char* soName = dl_info.dli_fname; + char* backtrace = getNativeBacktrace(); + if (!strstr(soName, "libc.so")) { + pthreadKeyCallback(0, ret, keySeq, soName, backtrace); + } + delete[] backtrace; + return ret; +} + +int (*original_pthread_key_delete)(pthread_key_t key); +int my_pthread_key_delete(pthread_key_t key) { + int ret = original_pthread_key_delete(key); + int keySeq = key & ~KEY_VALID_FLAG; + void * __caller_addr = __builtin_return_address(0); + Dl_info dl_info; + dladdr(__caller_addr, &dl_info); + const char* soName = dl_info.dli_fname; + char* backtrace = getNativeBacktrace(); + if (!strstr(soName, "libc.so")) { + pthreadKeyCallback(1, ret, keySeq, soName, backtrace); + } + delete[] backtrace; + return 0; +} + bool anrDumpCallback() { JNIEnv *env = JniInvocation::getEnv(); if (!env) return false; @@ -281,52 +389,28 @@ void hookAnrTraceWrite(bool isSiUser) { isHooking = true; if (apiLevel >= 27) { - void *libcutils_info = xhook_elf_open("/system/lib64/libcutils.so"); - if(!libcutils_info) { - libcutils_info = xhook_elf_open("/system/lib/libcutils.so"); - } - xhook_got_hook_symbol(libcutils_info, "connect", (void*) my_connect, (void**) (&original_connect)); + xhook_grouped_register(HOOK_REQUEST_GROUPID_ANR_DUMP_TRACE, ".*libcutils\\.so$", + "connect", (void *) my_connect, (void **) (&original_connect)); } else { - void* libart_info = xhook_elf_open("libart.so"); - xhook_got_hook_symbol(libart_info, "open", (void*) my_open, (void**) (&original_open)); + xhook_grouped_register(HOOK_REQUEST_GROUPID_ANR_DUMP_TRACE, ".*libart\\.so$", + "open", (void *) my_open, (void **) (&original_open)); } if (apiLevel >= 30 || apiLevel == 25 || apiLevel == 24) { - void* libc_info = xhook_elf_open("libc.so"); - xhook_got_hook_symbol(libc_info, "write", (void*) my_write, (void**) (&original_write)); + xhook_grouped_register(HOOK_REQUEST_GROUPID_ANR_DUMP_TRACE, ".*libc\\.so$", + "write", (void *) my_write, (void **) (&original_write)); } else if (apiLevel == 29) { - void* libbase_info = xhook_elf_open("/system/lib64/libbase.so"); - if(!libbase_info) { - libbase_info = xhook_elf_open("/system/lib/libbase.so"); - } - xhook_got_hook_symbol(libbase_info, "write", (void*) my_write, (void**) (&original_write)); - xhook_elf_close(libbase_info); + xhook_grouped_register(HOOK_REQUEST_GROUPID_ANR_DUMP_TRACE, ".*libbase\\.so$", + "write", (void *) my_write, (void **) (&original_write)); } else { - void* libart_info = xhook_elf_open("libart.so"); - xhook_got_hook_symbol(libart_info, "write", (void*) my_write, (void**) (&original_write)); + xhook_grouped_register(HOOK_REQUEST_GROUPID_ANR_DUMP_TRACE, ".*libart\\.so$", + "write", (void *) my_write, (void **) (&original_write)); } + + xhook_refresh(true); } void unHookAnrTraceWrite() { - int apiLevel = getApiLevel(); - if (apiLevel >= 27) { - void *libcutils_info = xhook_elf_open("/system/lib64/libcutils.so"); - xhook_got_hook_symbol(libcutils_info, "connect", (void*) original_connect, nullptr); - } else { - void* libart_info = xhook_elf_open("libart.so"); - xhook_got_hook_symbol(libart_info, "open", (void*) original_connect, nullptr); - } - - if (apiLevel >= 30 || apiLevel == 25 || apiLevel ==24) { - void* libc_info = xhook_elf_open("libc.so"); - xhook_got_hook_symbol(libc_info, "write", (void*) original_write, nullptr); - } else if (apiLevel == 29) { - void* libbase_info = xhook_elf_open("/system/lib64/libbase.so"); - xhook_got_hook_symbol(libbase_info, "write", (void*) original_write, nullptr); - } else { - void* libart_info = xhook_elf_open("libart.so"); - xhook_got_hook_symbol(libart_info, "write", (void*) original_write, nullptr); - } isHooking = false; } @@ -342,12 +426,37 @@ static void nativeFreeSignalAnrDetective(JNIEnv *env, jclass) { sAnrDumper.reset(); } -static void nativeInitMainThreadPriorityDetective(JNIEnv *env, jclass) { - xhook_grouped_register(HOOK_REQUEST_GROUPID_THREAD_PRIO_TRACE, ".*\\.so$", "setpriority", - (void *) my_setpriority, (void **) (&original_setpriority)); - xhook_grouped_register(HOOK_REQUEST_GROUPID_THREAD_PRIO_TRACE, ".*\\.so$", "prctl", - (void *) my_prctl, (void **) (&original_prctl)); - xhook_refresh(true); +static int nativeGetPthreadKeySeq(JNIEnv *env, jclass) { + pthread_key_t key; + pthread_key_create(&key, nullptr); + int key_seq = key & ~KEY_VALID_FLAG; + pthread_key_delete(key); + return key_seq; +} + +static void nativeInitThreadHook(JNIEnv *env, jclass, jint priority, jint pthreadKey) { + if (priority == 1) { + xhook_grouped_register(HOOK_REQUEST_GROUPID_THREAD_PRIORITY_TRACE, ".*\\.so$", "setpriority", + (void *) my_setpriority, (void **) (&original_setpriority)); + xhook_grouped_register(HOOK_REQUEST_GROUPID_THREAD_PRIORITY_TRACE, ".*\\.so$", "prctl", + (void *) my_prctl, (void **) (&original_prctl)); + } + + if (pthreadKey == 1) { + xhook_grouped_register(HOOK_REQUEST_GROUPID_THREAD_PTHREAD_KEY_TRACE, ".*\\.so$", "pthread_key_create", + (void *) my_pthread_key_create, (void **) (&original_pthread_key_create)); + + xhook_grouped_register(HOOK_REQUEST_GROUPID_THREAD_PTHREAD_KEY_TRACE, ".*\\.so$", "pthread_key_delete", + (void *) my_pthread_key_delete, (void **) (&original_pthread_key_delete)); + + xhook_export_symtable_hook("libc.so", "pthread_key_create", + (void *) my_pthread_key_create, (void **) (&original_pthread_key_create)); + xhook_export_symtable_hook("libc.so", "pthread_key_delete", + (void *) my_pthread_key_delete, (void **) (&original_pthread_key_delete)); + } + + xhook_enable_sigsegv_protection(0); + xhook_refresh(0); } static void nativeInitTouchEventLagDetective(JNIEnv *env, jclass, jint threshold) { @@ -378,8 +487,8 @@ static const JNINativeMethod ANR_METHODS[] = { }; static const JNINativeMethod THREAD_PRIORITY_METHODS[] = { - {"nativeInitMainThreadPriorityDetective", "()V", (void *) nativeInitMainThreadPriorityDetective}, - + {"nativeInitThreadHook", "(II)V", (void *) nativeInitThreadHook}, + {"nativeGetPthreadKeySeq", "()I", (void *) nativeGetPthreadKeySeq}, }; static const JNINativeMethod TOUCH_EVENT_TRACE_METHODS[] = { @@ -397,6 +506,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { jclass anrDetectiveCls = env->FindClass("com/tencent/matrix/trace/tracer/SignalAnrTracer"); if (!anrDetectiveCls) return -1; + + gJ.AnrDetective = static_cast(env->NewGlobalRef(anrDetectiveCls)); gJ.AnrDetector_onANRDumped = env->GetStaticMethodID(anrDetectiveCls, "onANRDumped", "()V"); @@ -416,7 +527,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { env->DeleteLocalRef(anrDetectiveCls); - jclass threadPriorityDetectiveCls = env->FindClass("com/tencent/matrix/trace/tracer/ThreadPriorityTracer"); + jclass threadPriorityDetectiveCls = env->FindClass("com/tencent/matrix/trace/tracer/ThreadTracer"); jclass touchEventLagTracerCls = env->FindClass("com/tencent/matrix/trace/tracer/TouchEventLagTracer"); @@ -428,6 +539,10 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { gJ.ThreadPriorityDetective_onMainThreadPriorityModified = env->GetStaticMethodID(threadPriorityDetectiveCls, "onMainThreadPriorityModified", "(II)V"); + + gJ.ThreadPriorityDetective_pthreadKeyCallback = + env->GetStaticMethodID(threadPriorityDetectiveCls, "pthreadKeyCallback", "(IIILjava/lang/String;Ljava/lang/String;)V"); + gJ.ThreadPriorityDetective_onMainThreadTimerSlackModified = env->GetStaticMethodID(threadPriorityDetectiveCls, "onMainThreadTimerSlackModified", "(J)V"); diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/cpp/TouchEventTracer.cc b/matrix/matrix-android/matrix-trace-canary/src/main/cpp/TouchEventTracer.cc index c0c229ce1..f465be880 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/cpp/TouchEventTracer.cc +++ b/matrix/matrix-android/matrix-trace-canary/src/main/cpp/TouchEventTracer.cc @@ -15,7 +15,7 @@ using namespace std; static mutex queueMutex; -static lock_guard lock(queueMutex); +static condition_variable cv; static bool loopRunning = false; static bool startDetect = false; static int LAG_THRESHOLD; @@ -37,7 +37,7 @@ void TouchEventTracer::touchRecv(int fd) { lastRecvTouchEventTimeStamp = 0; } else { lastRecvTouchEventTimeStamp = time(nullptr); - queueMutex.unlock(); + cv.notify_one(); } } @@ -52,10 +52,10 @@ void TouchEventTracer::touchSendFinish(int fd) { void recvQueueLooper() { - queueMutex.lock(); + unique_lock lk(queueMutex); while (loopRunning) { if (lastRecvTouchEventTimeStamp == 0) { - queueMutex.lock(); + cv.wait(lk); } else { long lastRecvTouchEventTimeStampNow = lastRecvTouchEventTimeStamp; if (lastRecvTouchEventTimeStampNow <= 0) { @@ -65,7 +65,7 @@ void recvQueueLooper() { if (time(nullptr) - lastRecvTouchEventTimeStampNow >= LAG_THRESHOLD && startDetect) { lagFd = currentFd; onTouchEventLagDumpTrace(currentFd); - queueMutex.lock(); + cv.wait(lk); } } } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/TracePlugin.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/TracePlugin.java index 319924708..5b709dbaf 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/TracePlugin.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/TracePlugin.java @@ -32,7 +32,6 @@ import com.tencent.matrix.trace.tracer.LooperAnrTracer; import com.tencent.matrix.trace.tracer.SignalAnrTracer; import com.tencent.matrix.trace.tracer.StartupTracer; -import com.tencent.matrix.trace.tracer.ThreadPriorityTracer; import com.tencent.matrix.trace.tracer.TouchEventLagTracer; import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; @@ -51,8 +50,7 @@ public class TracePlugin extends Plugin { private SignalAnrTracer signalAnrTracer; private IdleHandlerLagTracer idleHandlerLagTracer; private TouchEventLagTracer touchEventLagTracer; - private ThreadPriorityTracer threadPriorityTracer; - private static boolean supportFrameMetrics; + private final int sdkInt = Build.VERSION.SDK_INT; public TracePlugin(TraceConfig config) { this.traceConfig = config; @@ -62,18 +60,15 @@ public TracePlugin(TraceConfig config) { public void init(Application app, PluginListener listener) { super.init(app, listener); MatrixLog.i(TAG, "trace plugin init, trace config: %s", traceConfig.toString()); - int sdkInt = Build.VERSION.SDK_INT; if (sdkInt < Build.VERSION_CODES.JELLY_BEAN) { MatrixLog.e(TAG, "[FrameBeat] API is low Build.VERSION_CODES.JELLY_BEAN(16), TracePlugin is not supported"); unSupportPlugin(); return; - } else if (sdkInt >= Build.VERSION_CODES.O) { - supportFrameMetrics = true; } looperAnrTracer = new LooperAnrTracer(traceConfig); - frameTracer = new FrameTracer(traceConfig, supportFrameMetrics); + frameTracer = new FrameTracer(traceConfig); evilMethodTracer = new EvilMethodTracer(traceConfig); @@ -92,10 +87,10 @@ public void start() { @Override public void run() { - if (willUiThreadMonitorRunning(traceConfig)) { + if (sdkInt < Build.VERSION_CODES.N && willUiThreadMonitorRunning(traceConfig)) { if (!UIThreadMonitor.getMonitor().isInit()) { try { - UIThreadMonitor.getMonitor().init(traceConfig, supportFrameMetrics); + UIThreadMonitor.getMonitor().init(traceConfig); } catch (java.lang.RuntimeException e) { MatrixLog.e(TAG, "[start] RuntimeException:%s", e); return; @@ -132,11 +127,6 @@ public void run() { } } - if (traceConfig.isMainThreadPriorityTraceEnable()) { - threadPriorityTracer = new ThreadPriorityTracer(); - threadPriorityTracer.onStartTrace(); - } - if (traceConfig.isFPSEnable()) { frameTracer.onStartTrace(); } @@ -192,11 +182,6 @@ public void run() { if (idleHandlerLagTracer != null) { idleHandlerLagTracer.onCloseTrace(); } - - if (threadPriorityTracer != null) { - threadPriorityTracer.onCloseTrace(); - } - } }; diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/config/SharePluginInfo.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/config/SharePluginInfo.java index f2acb387b..966090d3c 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/config/SharePluginInfo.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/config/SharePluginInfo.java @@ -32,7 +32,6 @@ public class SharePluginInfo { public static final String ISSUE_DROP_LEVEL = "dropLevel"; public static final String ISSUE_DROP_SUM = "dropSum"; public static final String ISSUE_FPS = "fps"; - public static final String ISSUE_SUM_TASK_FRAME = "dropTaskFrameSum"; public static final String ISSUE_TRACE_STACK = "stack"; public static final String ISSUE_THREAD_STACK = "threadStack"; public static final String ISSUE_PROCESS_PRIORITY = "processPriority"; @@ -45,7 +44,6 @@ public class SharePluginInfo { public static final String ISSUE_MEMORY_DALVIK = "dalvik_heap"; public static final String ISSUE_MEMORY_VM_SIZE = "vm_size"; public static final String ISSUE_COST = "cost"; - public static final String ISSUE_CPU_USAGE = "usage"; public static final String ISSUE_STACK_TYPE = "detail"; public static final String ISSUE_IS_WARM_START_UP = "is_warm_start_up"; public static final String ISSUE_SUB_TYPE = "subType"; diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/config/TraceConfig.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/config/TraceConfig.java index b6aa3901b..052c71cb9 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/config/TraceConfig.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/config/TraceConfig.java @@ -35,6 +35,7 @@ public class TraceConfig implements IDefaultConfig { private static final String TAG = "Matrix.TraceConfig"; public static final int STACK_STYLE_SIMPLE = 0; public static final int STACK_STYLE_WHOLE = 1; + public static final int STACK_STYLE_RAW = 2; public IDynamicConfig dynamicConfig; public boolean defaultFpsEnable; public boolean defaultMethodTraceEnable; @@ -49,7 +50,6 @@ public class TraceConfig implements IDefaultConfig { public boolean isDevEnv; public boolean defaultSignalAnrEnable; public int stackStyle = STACK_STYLE_SIMPLE; - public boolean defaultMainThreadPriorityTraceEnable; public String splashActivities; public Set splashActivitiesSet; public String anrTraceFilePath = ""; @@ -137,11 +137,6 @@ public boolean isSignalAnrTraceEnable() { return defaultSignalAnrEnable; } - @Override - public boolean isMainThreadPriorityTraceEnable() { - return defaultMainThreadPriorityTraceEnable; - } - @Override public String getAnrTraceFilePath() { return anrTraceFilePath; @@ -245,7 +240,7 @@ public int getNormalThreshold() { public static class Builder { - private TraceConfig config = new TraceConfig(); + private final TraceConfig config = new TraceConfig(); public Builder dynamicConfig(IDynamicConfig dynamicConfig) { config.dynamicConfig = dynamicConfig; @@ -337,11 +332,6 @@ public Builder setTouchEventThreshold(int threshold) { return this; } - public Builder enableMainThreadPriorityTrace(boolean enable) { - config.defaultMainThreadPriorityTraceEnable = enable; - return this; - } - public Builder enableHistoryMsgRecorder(boolean enable) { config.historyMsgRecorder = enable; return this; diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/AppMethodBeat.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/AppMethodBeat.java index 1a326874b..70999c90d 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/AppMethodBeat.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/AppMethodBeat.java @@ -22,10 +22,11 @@ import android.os.Looper; import android.os.SystemClock; -import com.tencent.matrix.AppActiveMatrixDelegate; +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner; import com.tencent.matrix.trace.constants.Constants; import com.tencent.matrix.trace.hacker.ActivityThreadHacker; import com.tencent.matrix.trace.listeners.IAppMethodBeatListener; +import com.tencent.matrix.trace.listeners.ILooperListener; import com.tencent.matrix.trace.util.Utils; import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; @@ -68,21 +69,19 @@ public interface MethodEnterListener { private static final Object updateTimeLock = new Object(); private static volatile boolean isPauseUpdateTime = false; private static Runnable checkStartExpiredRunnable = null; - private static LooperMonitor.LooperDispatchListener looperMonitorListener = new LooperMonitor.LooperDispatchListener() { + private static ILooperListener looperMonitorListener = new ILooperListener() { @Override public boolean isValid() { return status >= STATUS_READY; } @Override - public void dispatchStart() { - super.dispatchStart(); + public void onDispatchBegin(String log) { AppMethodBeat.dispatchBegin(); } @Override - public void dispatchEnd() { - super.dispatchEnd(); + public void onDispatchEnd(String log, long beginNs, long endNs) { AppMethodBeat.dispatchEnd(); } }; @@ -311,7 +310,7 @@ public static void at(Activity activity, boolean isFocus) { } public static String getVisibleScene() { - return AppActiveMatrixDelegate.INSTANCE.getVisibleScene(); + return ProcessUILifecycleOwner.INSTANCE.getVisibleScene(); } /** diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/LooperMonitor.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/LooperMonitor.java index 9e78ef7fd..d3a9b9452 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/LooperMonitor.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/LooperMonitor.java @@ -22,16 +22,18 @@ import android.os.Looper; import android.os.MessageQueue; import android.os.SystemClock; -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; - import android.util.Log; import android.util.Printer; +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; + +import com.tencent.matrix.trace.listeners.ILooperListener; import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; import com.tencent.matrix.util.ReflectUtils; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; @@ -46,17 +48,20 @@ public class LooperMonitor implements MessageQueue.IdleHandler { private static final HandlerThread historyMsgHandlerThread = MatrixHandlerThread.getNewHandlerThread("historyMsgHandlerThread", HandlerThread.NORM_PRIORITY); private static final Handler historyMsgHandler = new Handler(historyMsgHandlerThread.getLooper()); - private static long messageStartTime = 0; private static final int HISTORY_QUEUE_MAX_SIZE = 200; private static final int RECENT_QUEUE_MAX_SIZE = 5000; - private static final Queue anrHistoryMQ = new ConcurrentLinkedQueue<>(); - private static final Queue recentMsgQ = new ConcurrentLinkedQueue<>(); + private final Queue anrHistoryMQ = new ConcurrentLinkedQueue<>(); + private final Queue recentMsgQ = new ConcurrentLinkedQueue<>(); - private static String latestMsgLog = ""; - private static long recentMCount = 0; - private static long recentMDuration = 0; + private long messageStartTime = 0; + private String latestMsgLog = ""; + private long recentMCount = 0; + private long recentMDuration = 0; + private boolean denseMsgTracer = false; + private boolean historyMsgRecorder = false; + @Deprecated public abstract static class LooperDispatchListener { boolean isHasDispatchStart = false; @@ -83,14 +88,18 @@ public void dispatchStart() { @CallSuper public void onDispatchStart(String x) { - this.isHasDispatchStart = true; - dispatchStart(); + if (!isHasDispatchStart) { + isHasDispatchStart = true; + dispatchStart(); + } } @CallSuper public void onDispatchEnd(String x) { - this.isHasDispatchStart = false; - dispatchEnd(); + if (isHasDispatchStart) { + isHasDispatchStart = false; + dispatchEnd(); + } } @@ -98,6 +107,39 @@ public void dispatchEnd() { } } + private final static class DispatchListenerWrapper { + private boolean isHasDispatchStart = false; + private long beginNs; + private final ILooperListener dispatchListener; + + DispatchListenerWrapper(ILooperListener dispatchListener) { + this.dispatchListener = dispatchListener; + } + + public boolean isValid() { + return dispatchListener.isValid(); + } + + public void onDispatchBegin(String x) { + if (!isHasDispatchStart) { + isHasDispatchStart = true; + beginNs = System.nanoTime(); + dispatchListener.onDispatchBegin(x); + } + } + + public void onDispatchEnd(String x) { + if (isHasDispatchStart) { + isHasDispatchStart = false; + dispatchListener.onDispatchEnd(x, beginNs, System.nanoTime()); + } + } + } + + public static LooperMonitor getMainMonitor() { + return sMainMonitor; + } + public static LooperMonitor of(@NonNull Looper looper) { LooperMonitor looperMonitor = sLooperMonitorMap.get(looper); if (looperMonitor == null) { @@ -107,15 +149,27 @@ public static LooperMonitor of(@NonNull Looper looper) { return looperMonitor; } + @Deprecated static void register(LooperDispatchListener listener) { sMainMonitor.addListener(listener); } + @Deprecated static void unregister(LooperDispatchListener listener) { sMainMonitor.removeListener(listener); } - private final HashSet listeners = new HashSet<>(); + public static void register(ILooperListener listener) { + sMainMonitor.addListener(listener); + } + + public static void unregister(ILooperListener listener) { + sMainMonitor.removeListener(listener); + } + + @Deprecated + private final HashSet oldListeners = new HashSet<>(); + private final Map listeners = new HashMap<>(); private LooperPrinter printer; private Looper looper; private static final long CHECK_TIME = 60 * 1000L; @@ -125,17 +179,32 @@ static void unregister(LooperDispatchListener listener) { * It will be thread-unsafe if you get the listeners and literate. */ @Deprecated - public HashSet getListeners() { - return listeners; + public HashSet getOldListeners() { + return oldListeners; } + @Deprecated public void addListener(LooperDispatchListener listener) { - synchronized (listeners) { - listeners.add(listener); + synchronized (oldListeners) { + oldListeners.add(listener); } } + @Deprecated public void removeListener(LooperDispatchListener listener) { + synchronized (oldListeners) { + oldListeners.remove(listener); + } + } + + public void addListener(ILooperListener listener) { + synchronized (listeners) { + DispatchListenerWrapper wrapper = new DispatchListenerWrapper(listener); + listeners.put(listener, wrapper); + } + } + + public void removeListener(ILooperListener listener) { synchronized (listeners) { listeners.remove(listener); } @@ -163,12 +232,15 @@ public boolean queueIdle() { public synchronized void onRelease() { if (printer != null) { + synchronized (oldListeners) { + oldListeners.clear(); + } synchronized (listeners) { listeners.clear(); } MatrixLog.v(TAG, "[onRelease] %s, origin printer:%s", looper.getThread().getName(), printer.origin); - looper.setMessageLogging(printer.origin); removeIdleHandler(looper); + looper.setMessageLogging(printer.origin); looper = null; printer = null; } @@ -270,7 +342,7 @@ public void println(String x) { } } - private static void recordMsg(final String log, final long duration, boolean denseMsgTracer) { + private void recordMsg(final String log, final long duration) { historyMsgHandler.post(new Runnable() { @Override public void run() { @@ -288,7 +360,7 @@ public void run() { } } - private static void enqueueRecentMQ(M m) { + private void enqueueRecentMQ(M m) { if (recentMsgQ.size() == RECENT_QUEUE_MAX_SIZE) { recentMsgQ.poll(); } @@ -297,59 +369,73 @@ private static void enqueueRecentMQ(M m) { recentMDuration += m.d; } - private static void enqueueHistoryMQ(M m) { + private void enqueueHistoryMQ(M m) { if (anrHistoryMQ.size() == HISTORY_QUEUE_MAX_SIZE) { anrHistoryMQ.poll(); } anrHistoryMQ.offer(m); } - public static Queue getHistoryMQ() { + public Queue getHistoryMQ() { enqueueHistoryMQ(new M(latestMsgLog, System.currentTimeMillis() - messageStartTime)); return anrHistoryMQ; } - public static Queue getRecentMsgQ() { + public Queue getRecentMsgQ() { return recentMsgQ; } - public static void cleanRecentMQ() { + public void cleanRecentMQ() { recentMsgQ.clear(); recentMCount = 0; recentMDuration = 0; } - public static long getRecentMCount() { + public long getRecentMCount() { return recentMCount; } - public static long getRecentMDuration() { + public long getRecentMDuration() { return recentMDuration; } private void dispatch(boolean isBegin, String log) { - synchronized (listeners) { - for (LooperDispatchListener listener : listeners) { - if (listener.isValid()) { - if (isBegin) { - if (!listener.isHasDispatchStart) { - if (listener.historyMsgRecorder) { - messageStartTime = System.currentTimeMillis(); - latestMsgLog = log; - recentMCount++; - } - listener.onDispatchStart(log); - } - } else { - if (listener.isHasDispatchStart) { - if (listener.historyMsgRecorder) { - recordMsg(log, System.currentTimeMillis() - messageStartTime, listener.denseMsgTracer); - } - listener.onDispatchEnd(log); - } + if (isBegin) { + if (historyMsgRecorder) { + messageStartTime = System.currentTimeMillis(); + latestMsgLog = log; + recentMCount++; + } + synchronized (oldListeners) { + for (LooperDispatchListener listener : oldListeners) { + if (listener.isValid()) { + listener.onDispatchStart(log); + } + } + } + synchronized (listeners) { + for (DispatchListenerWrapper listener : listeners.values()) { + if (listener.isValid()) { + listener.onDispatchBegin(log); + } + } + } + } else { + if (historyMsgRecorder) { + recordMsg(log, System.currentTimeMillis() - messageStartTime); + } + synchronized (oldListeners) { + for (LooperDispatchListener listener : oldListeners) { + if (listener.isValid()) { + listener.onDispatchEnd(log); + } + } + } + synchronized (listeners) { + for (DispatchListenerWrapper listener : listeners.values()) { + if (listener.isValid()) { + listener.onDispatchEnd(log); } - } else if (!isBegin && listener.isHasDispatchStart) { - listener.dispatchEnd(); } } } @@ -358,6 +444,7 @@ private void dispatch(boolean isBegin, String log) { public static class M { public String l; public long d; + M(String l, long d) { this.l = l; this.d = d; diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/UIThreadMonitor.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/UIThreadMonitor.java index 74c9f3a4b..f68b68b06 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/UIThreadMonitor.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/UIThreadMonitor.java @@ -33,6 +33,7 @@ import java.util.Arrays; import java.util.HashSet; +@Deprecated public class UIThreadMonitor implements BeatLifecycle, Runnable { private static final String TAG = "Matrix.UIThreadMonitor"; @@ -79,7 +80,6 @@ public class UIThreadMonitor implements BeatLifecycle, Runnable { private final static UIThreadMonitor sInstance = new UIThreadMonitor(); private TraceConfig config; - private static boolean useFrameMetrics; private Object callbackQueueLock; private Object[] callbackQueues; private Method addTraversalQueue; @@ -104,9 +104,8 @@ public boolean isInit() { return isInit; } - public void init(TraceConfig config, boolean supportFrameMetrics) { + public void init(TraceConfig config) { this.config = config; - useFrameMetrics = supportFrameMetrics; if (Thread.currentThread() != Looper.getMainLooper().getThread()) { throw new AssertionError("must be init in main thread!"); @@ -135,30 +134,28 @@ public void dispatchEnd() { }); this.isInit = true; + choreographer = Choreographer.getInstance(); frameIntervalNanos = ReflectUtils.reflectObject(choreographer, "mFrameIntervalNanos", Constants.DEFAULT_FRAME_DURATION); - if (!useFrameMetrics) { - choreographer = Choreographer.getInstance(); - callbackQueueLock = ReflectUtils.reflectObject(choreographer, "mLock", new Object()); - callbackQueues = ReflectUtils.reflectObject(choreographer, "mCallbackQueues", null); - if (null != callbackQueues) { - addInputQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class); - addAnimationQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class); - addTraversalQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class); - } - vsyncReceiver = ReflectUtils.reflectObject(choreographer, "mDisplayEventReceiver", null); + callbackQueueLock = ReflectUtils.reflectObject(choreographer, "mLock", new Object()); + callbackQueues = ReflectUtils.reflectObject(choreographer, "mCallbackQueues", null); + if (null != callbackQueues) { + addInputQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class); + addAnimationQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class); + addTraversalQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class); + } + vsyncReceiver = ReflectUtils.reflectObject(choreographer, "mDisplayEventReceiver", null); - MatrixLog.i(TAG, "[UIThreadMonitor] %s %s %s %s %s %s frameIntervalNanos:%s", callbackQueueLock == null, callbackQueues == null, - addInputQueue == null, addTraversalQueue == null, addAnimationQueue == null, vsyncReceiver == null, frameIntervalNanos); + MatrixLog.i(TAG, "[UIThreadMonitor] %s %s %s %s %s %s frameIntervalNanos:%s", callbackQueueLock == null, callbackQueues == null, + addInputQueue == null, addTraversalQueue == null, addAnimationQueue == null, vsyncReceiver == null, frameIntervalNanos); - if (config.isDevEnv()) { - addObserver(new LooperObserver() { - @Override - public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) { - MatrixLog.i(TAG, "focusedActivity[%s] frame cost:%sms isVsyncFrame=%s intendedFrameTimeNs=%s [%s|%s|%s]ns", - focusedActivity, (endNs - startNs) / Constants.TIME_MILLIS_TO_NANO, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); - } - }); - } + if (config.isDevEnv()) { + addObserver(new LooperObserver() { + @Override + public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) { + MatrixLog.i(TAG, "focusedActivity[%s] frame cost:%sms isVsyncFrame=%s intendedFrameTimeNs=%s [%s|%s|%s]ns", + focusedActivity, (endNs - startNs) / Constants.TIME_MILLIS_TO_NANO, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); + } + }); } } @@ -264,7 +261,7 @@ private void dispatchEnd() { traceBegin = System.nanoTime(); } - if (config.isFPSEnable() && !useFrameMetrics) { + if (config.isFPSEnable()) { long startNs = token; long intendedFrameTimeNs = startNs; if (isVsyncFrame) { @@ -330,11 +327,9 @@ public synchronized void onStart() { MatrixLog.i(TAG, "[onStart] callbackExist:%s %s", Arrays.toString(callbackExist), Utils.getStack()); callbackExist = new boolean[CALLBACK_LAST + 1]; } - if (!useFrameMetrics) { - queueStatus = new int[CALLBACK_LAST + 1]; - queueCost = new long[CALLBACK_LAST + 1]; - addFrameCallback(CALLBACK_INPUT, this, true); - } + queueStatus = new int[CALLBACK_LAST + 1]; + queueCost = new long[CALLBACK_LAST + 1]; + addFrameCallback(CALLBACK_INPUT, this, true); } } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDefaultConfig.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDefaultConfig.java index 00ab53187..872d2eb61 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDefaultConfig.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDefaultConfig.java @@ -36,8 +36,6 @@ public interface IDefaultConfig { boolean isSignalAnrTraceEnable(); - boolean isMainThreadPriorityTraceEnable(); - boolean isDebug(); boolean isDevEnv(); diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDoFrameListener.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDoFrameListener.java index 9844dba2a..eb9cf774d 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDoFrameListener.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDoFrameListener.java @@ -26,7 +26,10 @@ /** * Created by caichongyang on 2017/5/26. + *
+ * Use {@link IFrameListener} or {@link ISceneFrameListener} instead. **/ +@Deprecated public class IDoFrameListener { private Executor executor; diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDropFrameListener.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDropFrameListener.java new file mode 100644 index 000000000..f957a74a3 --- /dev/null +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IDropFrameListener.java @@ -0,0 +1,25 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.trace.listeners; + +import android.os.Build; + +import androidx.annotation.RequiresApi; + +@RequiresApi(api = Build.VERSION_CODES.N) +public interface IDropFrameListener extends IFrameListener { +} diff --git a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/PluginShareConstants.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IFrameListener.java similarity index 53% rename from matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/PluginShareConstants.java rename to matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IFrameListener.java index bcc36451e..51aed5589 100644 --- a/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/PluginShareConstants.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/IFrameListener.java @@ -14,20 +14,18 @@ * limitations under the License. */ -package com.tencent.matrix.plugin; +package com.tencent.matrix.trace.listeners; + +import android.os.Build; +import android.view.FrameMetrics; + +import androidx.annotation.RequiresApi; /** - * Created by zhouzhijie on 2019/3/13. + * Use {@link ISceneFrameListener} to analyze frame metrics of specified scene, or use + * {@link IDropFrameListener} to only analyze dropped frame. */ -public class PluginShareConstants { - public static class MemoryCanaryShareKeys { - public static final String SYSTEM_MEMORY = "sysMem"; - public static final String MEM_CLASS = "memClass"; - public static final String AVAILABLE = "available"; - public static final String DALVIK_HEAP = "dalvikHeap"; - public static final String NATIVE_HEAP = "nativeHeap"; - public static final String MEM_FREE = "memfree"; - public static final String IS_LOW = "islow"; - public static final String VM_SIZE = "vmSize"; - } +@RequiresApi(Build.VERSION_CODES.N) +public interface IFrameListener { + void onFrameMetricsAvailable(String sceneName, FrameMetrics frameMetrics, float droppedFrames, float refreshRate); } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/ILooperListener.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/ILooperListener.java new file mode 100644 index 000000000..cf3ed1398 --- /dev/null +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/ILooperListener.java @@ -0,0 +1,23 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.trace.listeners; + +public interface ILooperListener { + boolean isValid(); + void onDispatchBegin(String log); + void onDispatchEnd(String log, long beginNs, long endNs); +} diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/ISceneFrameListener.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/ISceneFrameListener.java new file mode 100644 index 000000000..768d037cd --- /dev/null +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/ISceneFrameListener.java @@ -0,0 +1,75 @@ +/* + * Tencent is pleased to support the open source community by making wechat-matrix available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.matrix.trace.listeners; + + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +public interface ISceneFrameListener { + + /** + * The interval returned indicates how long to call back {@code onFrameMetricsAvailable}. + * Usually, this value should not less than 17. Because for those display with a 60Hz + * refresh rate, it takes at least 16.6ms to generate a frame. + * + * @return The interval, measured in milliseconds. + */ + @IntRange(from = 1) + int getIntervalMs(); + + /** + * The name returned will be used to match the specified scene. + * + * @return Name of the specified scene, null or empty string will match all scene. + */ + String getName(); + + /** + * Whether skip the first frame. + * + * @return true for skip, false for not. + */ + boolean skipFirstFrame(); + + /** + * Frame metrics whose dropped frames less than threshold will be skipped. + * We always assume the refresh rate of display is 60Hz, and the threshold + * will be converted to corresponding value while the real refresh rate + * is not 60Hz. + *
+ * For example, if the threshold is 10 and refresh rate is 90Hz, all frame + * metrics whose dropped frames less than 15 will be skipped. + * + * @return integer value of threshold, zero indicates all frame metrics will be + * added to calculation. + */ + @IntRange(from = 0) + int getThreshold(); + + /** + * This method will be called when average frame metrics available. + * + * @param sceneName name of scene. + * @param avgDurations average avgDurations, include draw duration, animation duration, etc. + * See {@link com.tencent.matrix.trace.tracer.FrameTracer.FrameDuration}. + * @param dropLevel drop level distribution, sum of this array equals value returned by getSize(). + * See {@link com.tencent.matrix.trace.tracer.FrameTracer.DropStatus}. + * @param dropSum dropped frame distribution. + */ + void onFrameMetricsAvailable(@NonNull String sceneName, long[] avgDurations, int[] dropLevel, int[] dropSum, float avgDroppedFrame, float avgRefreshRate, float avgFps); +} diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/LooperObserver.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/LooperObserver.java index 36bc1d3b5..e993f52b5 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/LooperObserver.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/listeners/LooperObserver.java @@ -18,6 +18,10 @@ import androidx.annotation.CallSuper; +/** + * Use {@link ILooperListener} or {@link IFrameListener} instead. + */ +@Deprecated public abstract class LooperObserver { private boolean isDispatchBegin = false; diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/EvilMethodTracer.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/EvilMethodTracer.java index 70fad2d2b..9a7dbad07 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/EvilMethodTracer.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/EvilMethodTracer.java @@ -26,8 +26,9 @@ import com.tencent.matrix.trace.config.TraceConfig; import com.tencent.matrix.trace.constants.Constants; import com.tencent.matrix.trace.core.AppMethodBeat; -import com.tencent.matrix.trace.core.UIThreadMonitor; +import com.tencent.matrix.trace.core.LooperMonitor; import com.tencent.matrix.trace.items.MethodItem; +import com.tencent.matrix.trace.listeners.ILooperListener; import com.tencent.matrix.trace.util.TraceDataUtils; import com.tencent.matrix.trace.util.Utils; import com.tencent.matrix.util.DeviceUtil; @@ -41,14 +42,13 @@ import java.util.LinkedList; import java.util.List; -public class EvilMethodTracer extends Tracer { +public class EvilMethodTracer extends Tracer implements ILooperListener { private static final String TAG = "Matrix.EvilMethodTracer"; private final TraceConfig config; private AppMethodBeat.IndexRecord indexRecord; - private long[] queueTypeCosts = new long[3]; private long evilThresholdMs; - private boolean isEvilMethodTraceEnable; + private final boolean isEvilMethodTraceEnable; public EvilMethodTracer(TraceConfig config) { this.config = config; @@ -60,7 +60,7 @@ public EvilMethodTracer(TraceConfig config) { public void onAlive() { super.onAlive(); if (isEvilMethodTraceEnable) { - UIThreadMonitor.getMonitor().addObserver(this); + LooperMonitor.register(this); } } @@ -69,45 +69,31 @@ public void onAlive() { public void onDead() { super.onDead(); if (isEvilMethodTraceEnable) { - UIThreadMonitor.getMonitor().removeObserver(this); + LooperMonitor.unregister(this); } } - @Override - public void dispatchBegin(long beginNs, long cpuBeginMs, long token) { - super.dispatchBegin(beginNs, cpuBeginMs, token); - indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin"); + public boolean isValid() { + return true; } - @Override - public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) { - queueTypeCosts[0] = inputCostNs; - queueTypeCosts[1] = animationCostNs; - queueTypeCosts[2] = traversalCostNs; + public void onDispatchBegin(String log) { + indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin"); } @Override - public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) { - super.dispatchEnd(beginNs, cpuBeginMs, endNs, cpuEndMs, token, isVsyncFrame); - long start = config.isDevEnv() ? System.currentTimeMillis() : 0; + public void onDispatchEnd(String log, long beginNs, long endNs) { long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO; try { if (dispatchCost >= evilThresholdMs) { long[] data = AppMethodBeat.getInstance().copyData(indexRecord); - long[] queueCosts = new long[3]; - System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3); String scene = AppActiveMatrixDelegate.INSTANCE.getVisibleScene(); - MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO)); + MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, dispatchCost, endNs)); } } finally { indexRecord.release(); - if (config.isDevEnv()) { - String usage = Utils.calculateCpuUsage(cpuEndMs - cpuBeginMs, dispatchCost); - MatrixLog.v(TAG, "[dispatchEnd] token:%s cost:%sms cpu:%sms usage:%s innerCost:%s", - token, dispatchCost, cpuEndMs - cpuBeginMs, usage, System.currentTimeMillis() - start); - } } } @@ -115,22 +101,18 @@ public void modifyEvilThresholdMs(long evilThresholdMs) { this.evilThresholdMs = evilThresholdMs; } - private class AnalyseTask implements Runnable { - long[] queueCost; + private static class AnalyseTask implements Runnable { long[] data; - long cpuCost; long cost; long endMs; String scene; boolean isForeground; - AnalyseTask(boolean isForeground, String scene, long[] data, long[] queueCost, long cpuCost, long cost, long endMs) { + AnalyseTask(boolean isForeground, String scene, long[] data, long cost, long endMs) { this.isForeground = isForeground; this.scene = scene; this.cost = cost; - this.cpuCost = cpuCost; this.data = data; - this.queueCost = queueCost; this.endMs = endMs; } @@ -138,14 +120,13 @@ void analyse() { // process int[] processStat = Utils.getProcessPriority(Process.myPid()); - String usage = Utils.calculateCpuUsage(cpuCost, cost); - LinkedList stack = new LinkedList(); + LinkedList stack = new LinkedList<>(); if (data.length > 0) { TraceDataUtils.structuredDataToStack(data, stack, true, endMs); TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() { @Override public boolean isFilter(long during, int filterCount) { - return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS; + return during < (long) filterCount * Constants.TIME_UPDATE_CYCLE_MS; } @Override @@ -156,7 +137,7 @@ public int getFilterMaxCount() { @Override public void fallback(List stack, int size) { MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack); - Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK)); + Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK)); while (iterator.hasNext()) { iterator.next(); iterator.remove(); @@ -171,7 +152,7 @@ public void fallback(List stack, int size) { long stackCost = Math.max(cost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder)); String stackKey = TraceDataUtils.getTreeKey(stack, stackCost); - MatrixLog.w(TAG, "%s", printEvil(scene, processStat, isForeground, logcatBuilder, stack.size(), stackKey, usage, queueCost[0], queueCost[1], queueCost[2], cost)); // for logcat + MatrixLog.w(TAG, "%s", printEvil(scene, processStat, isForeground, logcatBuilder, stack.size(), stackKey, cost)); // for logcat // report try { @@ -180,11 +161,10 @@ public void fallback(List stack, int size) { return; } JSONObject jsonObject = new JSONObject(); - jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication()); + DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication()); jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.NORMAL); jsonObject.put(SharePluginInfo.ISSUE_COST, stackCost); - jsonObject.put(SharePluginInfo.ISSUE_CPU_USAGE, usage); jsonObject.put(SharePluginInfo.ISSUE_SCENE, scene); jsonObject.put(SharePluginInfo.ISSUE_TRACE_STACK, reportBuilder.toString()); jsonObject.put(SharePluginInfo.ISSUE_STACK_KEY, stackKey); @@ -205,8 +185,7 @@ public void run() { analyse(); } - private String printEvil(String scene, int[] processStat, boolean isForeground, StringBuilder stack, long stackSize, String stackKey, String usage, long inputCost, - long animationCost, long traversalCost, long allCost) { + private String printEvil(String scene, int[] processStat, boolean isForeground, StringBuilder stack, long stackSize, String stackKey, long allCost) { StringBuilder print = new StringBuilder(); print.append(String.format("-\n>>>>>>>>>>>>>>>>>>>>> maybe happens Jankiness!(%sms) <<<<<<<<<<<<<<<<<<<<<\n", allCost)); print.append("|* [Status]").append("\n"); @@ -214,10 +193,6 @@ private String printEvil(String scene, int[] processStat, boolean isForeground, print.append("|*\t\tForeground: ").append(isForeground).append("\n"); print.append("|*\t\tPriority: ").append(processStat[0]).append("\tNice: ").append(processStat[1]).append("\n"); print.append("|*\t\tis64BitRuntime: ").append(DeviceUtil.is64BitRuntime()).append("\n"); - print.append("|*\t\tCPU: ").append(usage).append("\n"); - print.append("|* [doFrame]").append("\n"); - print.append("|*\t\tinputCost:animationCost:traversalCost").append("\n"); - print.append("|*\t\t").append(inputCost).append(":").append(animationCost).append(":").append(traversalCost).append("\n"); if (stackSize > 0) { print.append("|*\t\tStackKey: ").append(stackKey).append("\n"); print.append(stack.toString()); diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/FrameTracer.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/FrameTracer.java index d6297c057..e73c9222d 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/FrameTracer.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/FrameTracer.java @@ -16,6 +16,7 @@ package com.tencent.matrix.trace.tracer; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; import android.os.Build; @@ -24,11 +25,13 @@ import android.os.SystemClock; import android.view.FrameMetrics; import android.view.Window; +import android.view.WindowManager; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; -import com.tencent.matrix.AppActiveMatrixDelegate; import com.tencent.matrix.Matrix; +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner; import com.tencent.matrix.report.Issue; import com.tencent.matrix.trace.TracePlugin; import com.tencent.matrix.trace.config.SharePluginInfo; @@ -36,10 +39,14 @@ import com.tencent.matrix.trace.constants.Constants; import com.tencent.matrix.trace.core.UIThreadMonitor; import com.tencent.matrix.trace.listeners.IDoFrameListener; -import com.tencent.matrix.trace.util.Utils; +import com.tencent.matrix.trace.listeners.IDropFrameListener; +import com.tencent.matrix.trace.listeners.IFrameListener; +import com.tencent.matrix.trace.listeners.ISceneFrameListener; +import com.tencent.matrix.trace.listeners.LooperObserver; import com.tencent.matrix.util.DeviceUtil; import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; +import com.tencent.matrix.util.MatrixUtil; import org.json.JSONException; import org.json.JSONObject; @@ -47,345 +54,485 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.concurrent.Executor; +import java.util.concurrent.ConcurrentHashMap; public class FrameTracer extends Tracer implements Application.ActivityLifecycleCallbacks { - private static final String TAG = "Matrix.FrameTracer"; - private static boolean useFrameMetrics; - private final HashSet listeners = new HashSet<>(); - private DropFrameListener dropFrameListener; - private int dropFrameListenerThreshold = 0; - private long frameIntervalNs; - private int refreshRate; - private final TraceConfig config; - private final long timeSliceMs; - private boolean isFPSEnable; - private long frozenThreshold; - private long highThreshold; - private long middleThreshold; - private long normalThreshold; - private int droppedSum = 0; + + private static final long HALF_MAX = (Long.MAX_VALUE >>> 1); + public final static int sdkInt = Build.VERSION.SDK_INT; + public static float defaultRefreshRate = 60; + + private double droppedSum = 0; + + @Deprecated private long durationSum = 0; - private Map lastResumeTimeMap = new HashMap<>(); - private Map frameListenerMap = new HashMap<>(); + @Deprecated + private final HashSet oldListeners = new HashSet<>(); + @Deprecated + private long frameIntervalNs; + @Deprecated + private LooperObserver looperObserver = new LooperObserver() { + @Override + public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) { + if (isForeground()) { + notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); + } + } + + @Deprecated + private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame, + final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) { + long traceBegin = System.currentTimeMillis(); + try { + final long jitter = endNs - intendedFrameTimeNs; + final int dropFrame = (int) (jitter / frameIntervalNs); + if (oldDropFrameListener != null && dropFrame > dropFrameListenerThreshold) { + try { + if (MatrixUtil.getTopActivityName() != null) { + oldDropFrameListener.dropFrame(dropFrame, jitter, MatrixUtil.getTopActivityName()); + } + } catch (Exception e) { + MatrixLog.e(TAG, "dropFrameListener error e:" + e.getMessage()); + } + } + + droppedSum += dropFrame; + durationSum += Math.max(jitter, frameIntervalNs); + + synchronized (oldListeners) { + for (final IDoFrameListener listener : oldListeners) { + if (config.isDevEnv()) { + listener.time = SystemClock.uptimeMillis(); + } + if (null != listener.getExecutor()) { + if (listener.getIntervalFrameReplay() > 0) { + listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, + intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); + } else { + listener.getExecutor().execute(new Runnable() { + @Override + public void run() { + listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, + intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); + } + }); + } + } else { + listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, + intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); + } + + if (config.isDevEnv()) { + listener.time = SystemClock.uptimeMillis() - listener.time; + MatrixLog.d(TAG, "[notifyListener] cost:%sms listener:%s", listener.time, listener); + } + } + } + } finally { + long cost = System.currentTimeMillis() - traceBegin; + if (config.isDebug() && cost > frameIntervalNs) { + MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync! size:%s cost:%sms", oldListeners.size(), cost); + } + } + } + }; - public FrameTracer(TraceConfig config, boolean supportFrameMetrics) { - useFrameMetrics = supportFrameMetrics; + @Deprecated + private DropFrameListener oldDropFrameListener; + private IDropFrameListener dropFrameListener; + private int dropFrameListenerThreshold = 0; + + private final TraceConfig config; + private final HashSet listeners = new HashSet<>(); + private final long frozenThreshold; + private final long highThreshold; + private final long middleThreshold; + private final long normalThreshold; + SceneFrameCollector sceneFrameCollector; + private final Map frameListenerMap = new ConcurrentHashMap<>(); + + public FrameTracer(TraceConfig config) { this.config = config; this.frameIntervalNs = UIThreadMonitor.getMonitor().getFrameIntervalNanos(); - this.timeSliceMs = config.getTimeSliceMs(); - this.isFPSEnable = config.isFPSEnable(); this.frozenThreshold = config.getFrozenThreshold(); this.highThreshold = config.getHighThreshold(); this.normalThreshold = config.getNormalThreshold(); this.middleThreshold = config.getMiddleThreshold(); - MatrixLog.i(TAG, "[init] frameIntervalMs:%s isFPSEnable:%s", frameIntervalNs, isFPSEnable); - if (isFPSEnable) { - addListener(new FPSCollector()); - } + MatrixLog.i(TAG, "[init] frameIntervalMs:%s isFPSEnable:%s", frameIntervalNs, config.isFPSEnable()); } + @Deprecated public void addListener(IDoFrameListener listener) { + synchronized (oldListeners) { + oldListeners.add(listener); + } + } + + @Deprecated + public void removeListener(IDoFrameListener listener) { + synchronized (oldListeners) { + oldListeners.remove(listener); + } + } + + @RequiresApi(Build.VERSION_CODES.N) + public void addListener(IFrameListener listener) { synchronized (listeners) { listeners.add(listener); } } - public void removeListener(IDoFrameListener listener) { + @RequiresApi(Build.VERSION_CODES.N) + public void removeListener(IFrameListener listener) { synchronized (listeners) { listeners.remove(listener); } } + @RequiresApi(Build.VERSION_CODES.N) + public void register(ISceneFrameListener listener) { + if (sceneFrameCollector != null) { + sceneFrameCollector.register(listener); + } + } + + @RequiresApi(Build.VERSION_CODES.N) + public void unregister(ISceneFrameListener listener, boolean isCallbackRestAfterUnregister) { + if (sceneFrameCollector != null) { + sceneFrameCollector.unregister(listener, isCallbackRestAfterUnregister); + } + } + + @RequiresApi(Build.VERSION_CODES.N) + public void unregister(ISceneFrameListener listener) { + unregister(listener, false); + } + + @RequiresApi(Build.VERSION_CODES.N) + public void reset(ISceneFrameListener listener, boolean isCallbackRestBeforeReset) { + if (sceneFrameCollector != null) { + sceneFrameCollector.reset(listener, isCallbackRestBeforeReset); + } + } + + @RequiresApi(Build.VERSION_CODES.N) + public void reset(ISceneFrameListener listener) { + unregister(listener, false); + } + @Override public void onAlive() { super.onAlive(); - if (isFPSEnable) { - if (!useFrameMetrics) { - UIThreadMonitor.getMonitor().addObserver(this); - } + if (config.isFPSEnable()) { + forceEnable(); + } + } + + public void forceEnable() { + MatrixLog.i(TAG, "forceEnable"); + if (sdkInt >= Build.VERSION_CODES.N) { Matrix.with().getApplication().registerActivityLifecycleCallbacks(this); + sceneFrameCollector = new SceneFrameCollector(); + addListener(sceneFrameCollector); + register(new AllSceneFrameListener()); + } else { + UIThreadMonitor.getMonitor().addObserver(looperObserver); } } - @Override - public void onDead() { - super.onDead(); + public void forceDisable() { + MatrixLog.i(TAG, "forceDisable"); removeDropFrameListener(); - if (isFPSEnable) { - UIThreadMonitor.getMonitor().removeObserver(this); + if (sdkInt >= Build.VERSION_CODES.N) { Matrix.with().getApplication().unregisterActivityLifecycleCallbacks(this); + listeners.clear(); + frameListenerMap.clear(); + } else { + UIThreadMonitor.getMonitor().removeObserver(looperObserver); + oldListeners.clear(); } } @Override - public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) { - if (isForeground()) { - notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); + public void onDead() { + super.onDead(); + if (config.isFPSEnable()) { + forceDisable(); } } public int getDroppedSum() { - return droppedSum; + return (int) droppedSum; } + @Deprecated public long getDurationSum() { return durationSum; } - private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame, - final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) { - long traceBegin = System.currentTimeMillis(); - try { - final long jitter = endNs - intendedFrameTimeNs; - final int dropFrame = (int) (jitter / frameIntervalNs); - if (dropFrameListener != null) { - if (dropFrame > dropFrameListenerThreshold) { - try { - if (AppActiveMatrixDelegate.getTopActivityName() != null) { - long lastResumeTime = lastResumeTimeMap.get(AppActiveMatrixDelegate.getTopActivityName()); - dropFrameListener.dropFrame(dropFrame, jitter, AppActiveMatrixDelegate.getTopActivityName(), lastResumeTime); - } - } catch (Exception e) { - MatrixLog.e(TAG, "dropFrameListener error e:" + e.getMessage()); + @RequiresApi(Build.VERSION_CODES.N) + private class SceneFrameCollector implements IFrameListener { + + private final Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper()); + private final HashMap specifiedSceneMap = new HashMap<>(); + private final HashMap unspecifiedSceneMap = new HashMap<>(); + + public synchronized void register(@NonNull ISceneFrameListener listener) { + if (listener.getIntervalMs() < 1 || listener.getThreshold() < 0) { + MatrixLog.e(TAG, "Illegal value, intervalMs=%d, threshold=%d, activity=%s", + listener.getIntervalMs(), listener.getThreshold(), listener.getClass().getName()); + return; + } + String scene = listener.getName(); + SceneFrameCollectItem collectItem = new SceneFrameCollectItem(listener); + if (scene == null || scene.isEmpty()) { + unspecifiedSceneMap.put(listener, collectItem); + } else { + specifiedSceneMap.put(scene, collectItem); + } + } + + public synchronized void unregister(@NonNull ISceneFrameListener listener, boolean isCallbackRestAfterUnregister) { + final String scene = listener.getName(); + final SceneFrameCollectItem target = scene == null || scene.isEmpty() + ? unspecifiedSceneMap.remove(listener) + : specifiedSceneMap.remove(scene); + + if (target != null && isCallbackRestAfterUnregister) { + frameHandler.post(new Runnable() { + @Override + public void run() { + target.tryCallBackAndReset(); } - } + }); } + } - droppedSum += dropFrame; - durationSum += Math.max(jitter, frameIntervalNs); + public synchronized void reset(ISceneFrameListener listener, boolean isCallbackRestBeforeReset) { + final String scene = listener.getName(); + final SceneFrameCollectItem target = scene == null || scene.isEmpty() + ? unspecifiedSceneMap.get(listener) + : specifiedSceneMap.get(scene); + if (target != null && isCallbackRestBeforeReset) { + target.tryCallBackAndReset(); + } + } - synchronized (listeners) { - for (final IDoFrameListener listener : listeners) { - if (config.isDevEnv()) { - listener.time = SystemClock.uptimeMillis(); - } - if (null != listener.getExecutor()) { - if (listener.getIntervalFrameReplay() > 0) { - listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, - intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); - } else { - listener.getExecutor().execute(new Runnable() { - @Override - public void run() { - listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, - intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); - } - }); - } - } else { - listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, - intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); - } + public synchronized void resetAllAndCallBack() { + for (SceneFrameCollectItem value : unspecifiedSceneMap.values()) { + value.tryCallBackAndReset(); + } + for (SceneFrameCollectItem value : specifiedSceneMap.values()) { + value.tryCallBackAndReset(); + } + } - if (config.isDevEnv()) { - listener.time = SystemClock.uptimeMillis() - listener.time; - MatrixLog.d(TAG, "[notifyListener] cost:%sms listener:%s", listener.time, listener); + @Override + public void onFrameMetricsAvailable(final String sceneName, final FrameMetrics frameMetrics, final float droppedFrames, final float refreshRate) { + frameHandler.post(new Runnable() { + @Override + public void run() { + String scene = sceneName.getClass().getName(); + synchronized (SceneFrameCollector.this) { + SceneFrameCollectItem collectItem = specifiedSceneMap.get(scene); + if (collectItem != null) { + collectItem.append(sceneName, frameMetrics, droppedFrames, refreshRate); + } + for (SceneFrameCollectItem frameCollectItem : unspecifiedSceneMap.values()) { + frameCollectItem.append(sceneName, frameMetrics, droppedFrames, refreshRate); + } } } - } - } finally { - long cost = System.currentTimeMillis() - traceBegin; - if (config.isDebug() && cost > frameIntervalNs) { - MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync! size:%s cost:%sms", listeners.size(), cost); - } + }); } } + public enum DropStatus { + DROPPED_BEST, DROPPED_NORMAL, DROPPED_MIDDLE, DROPPED_HIGH, DROPPED_FROZEN; - private class FPSCollector extends IDoFrameListener { - - private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper()); + public static String stringify(int[] level, int[] sum) { + StringBuilder sb = new StringBuilder(); + sb.append('{'); - Executor executor = new Executor() { - @Override - public void execute(Runnable command) { - frameHandler.post(command); + for (DropStatus item : values()) { + sb.append('(').append(item.name()).append("_LEVEL=").append(level[item.ordinal()]).append(" "); + sb.append(item.name()).append("_SUM=").append(sum[item.ordinal()]).append("); "); } - }; - - private HashMap map = new HashMap<>(); + sb.setLength(sb.length() - 2); // remove the last "; " + sb.append("}"); - @Override - public Executor getExecutor() { - return executor; + return sb.toString(); } + } - @Override - public int getIntervalFrameReplay() { - return 300; - } + public enum FrameDuration { + UNKNOWN_DELAY_DURATION, INPUT_HANDLING_DURATION, ANIMATION_DURATION, LAYOUT_MEASURE_DURATION, DRAW_DURATION, + SYNC_DURATION, COMMAND_ISSUE_DURATION, SWAP_BUFFERS_DURATION, TOTAL_DURATION, GPU_DURATION; - @Override - public void doReplay(List list) { - super.doReplay(list); - for (FrameReplay replay : list) { - doReplayInner(replay.focusedActivity, replay.startNs, replay.endNs, replay.dropFrame, replay.isVsyncFrame, - replay.intendedFrameTimeNs, replay.inputCostNs, replay.animationCostNs, replay.traversalCostNs); + @SuppressLint("InlinedApi") + static final int[] indices = {FrameMetrics.UNKNOWN_DELAY_DURATION, FrameMetrics.INPUT_HANDLING_DURATION, + FrameMetrics.ANIMATION_DURATION, FrameMetrics.LAYOUT_MEASURE_DURATION, FrameMetrics.DRAW_DURATION, + FrameMetrics.SYNC_DURATION, FrameMetrics.COMMAND_ISSUE_DURATION, FrameMetrics.SWAP_BUFFERS_DURATION, + FrameMetrics.TOTAL_DURATION, FrameMetrics.GPU_DURATION}; + + public static String stringify(long[] durations) { + StringBuilder sb = new StringBuilder(); + sb.append('{'); + + for (FrameDuration item : values()) { + sb.append(item.name()).append('=').append(durations[item.ordinal()]).append("; "); } - } + sb.setLength(sb.length() - 2); // remove the last "; " + sb.append("}"); - public void doReplayInner(String visibleScene, long startNs, long endNs, int droppedFrames, - boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, - long animationCostNs, long traversalCostNs) { + return sb.toString(); + } + } - if (Utils.isEmpty(visibleScene)) return; - if (!isVsyncFrame) return; + @RequiresApi(Build.VERSION_CODES.N) + private class SceneFrameCollectItem { + private final long[] durations = new long[FrameDuration.values().length]; + private final int[] dropLevel = new int[DropStatus.values().length]; + private final int[] dropSum = new int[DropStatus.values().length]; + private float dropCount; + private float refreshRate; + private float totalDuration; + private long beginMs; + private String lastScene; + private int count = 0; + + ISceneFrameListener listener; + + SceneFrameCollectItem(ISceneFrameListener listener) { + this.listener = listener; + } - FrameCollectItem item = map.get(visibleScene); - if (null == item) { - item = new FrameCollectItem(visibleScene); - map.put(visibleScene, item); + public void append(String scene, FrameMetrics frameMetrics, float droppedFrames, float refreshRate) { + if ((listener.skipFirstFrame() && frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1) + || droppedFrames < (refreshRate / 60) * listener.getThreshold()) { + return; + } + if (count == 0) { + beginMs = SystemClock.uptimeMillis(); + } + for (int i = FrameDuration.UNKNOWN_DELAY_DURATION.ordinal(); i <= FrameDuration.TOTAL_DURATION.ordinal(); i++) { + durations[i] += frameMetrics.getMetric(FrameDuration.indices[i]); + } + if (sdkInt >= Build.VERSION_CODES.S) { + durations[FrameDuration.GPU_DURATION.ordinal()] += frameMetrics.getMetric(FrameMetrics.GPU_DURATION); } - item.collect(droppedFrames); - if (item.sumFrameCost >= timeSliceMs) { // report - map.remove(visibleScene); - item.report(); + dropCount += droppedFrames; + collect(Math.round(droppedFrames)); + this.refreshRate += refreshRate; + float frameIntervalNanos = Constants.TIME_SECOND_TO_NANO / refreshRate; + totalDuration += Math.max(frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION), frameIntervalNanos); + ++count; + + lastScene = scene; + if (SystemClock.uptimeMillis() - beginMs >= listener.getIntervalMs()) { + tryCallBackAndReset(); } } - } - private class FrameCollectItem { - String visibleScene; - long sumFrameCost; - int sumFrame = 0; - int sumDroppedFrames; - // record the level of frames dropped each time - int[] dropLevel = new int[DropStatus.values().length]; - int[] dropSum = new int[DropStatus.values().length]; - - FrameCollectItem(String visibleScene) { - this.visibleScene = visibleScene; + void tryCallBackAndReset() { + if (count > 20) { + dropCount /= count; + this.refreshRate /= count; + totalDuration /= count; + for (int i = 0; i < durations.length; i++) { + durations[i] /= count; + } + listener.onFrameMetricsAvailable(lastScene, durations, dropLevel, dropSum, + dropCount, this.refreshRate, Constants.TIME_SECOND_TO_NANO / totalDuration); + } + reset(); } - void collect(int droppedFrames) { - float frameIntervalCost = 1f * FrameTracer.this.frameIntervalNs - / Constants.TIME_MILLIS_TO_NANO; - sumFrameCost += (droppedFrames + 1) * frameIntervalCost; - sumDroppedFrames += droppedFrames; - sumFrame++; + private void collect(int droppedFrames) { if (droppedFrames >= frozenThreshold) { - dropLevel[DropStatus.DROPPED_FROZEN.index]++; - dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames; + dropLevel[DropStatus.DROPPED_FROZEN.ordinal()]++; + dropSum[DropStatus.DROPPED_FROZEN.ordinal()] += droppedFrames; } else if (droppedFrames >= highThreshold) { - dropLevel[DropStatus.DROPPED_HIGH.index]++; - dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames; + dropLevel[DropStatus.DROPPED_HIGH.ordinal()]++; + dropSum[DropStatus.DROPPED_HIGH.ordinal()] += droppedFrames; } else if (droppedFrames >= middleThreshold) { - dropLevel[DropStatus.DROPPED_MIDDLE.index]++; - dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames; + dropLevel[DropStatus.DROPPED_MIDDLE.ordinal()]++; + dropSum[DropStatus.DROPPED_MIDDLE.ordinal()] += droppedFrames; } else if (droppedFrames >= normalThreshold) { - dropLevel[DropStatus.DROPPED_NORMAL.index]++; - dropSum[DropStatus.DROPPED_NORMAL.index] += droppedFrames; + dropLevel[DropStatus.DROPPED_NORMAL.ordinal()]++; + dropSum[DropStatus.DROPPED_NORMAL.ordinal()] += droppedFrames; } else { - dropLevel[DropStatus.DROPPED_BEST.index]++; - dropSum[DropStatus.DROPPED_BEST.index] += Math.max(droppedFrames, 0); - } - } - - void report() { - float fps = Math.min(refreshRate, 1000.f * sumFrame / sumFrameCost); - MatrixLog.i(TAG, "[report] FPS:%s %s", fps, toString()); - try { - TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class); - if (null == plugin) { - return; - } - JSONObject dropLevelObject = new JSONObject(); - dropLevelObject.put(DropStatus.DROPPED_FROZEN.name(), dropLevel[DropStatus.DROPPED_FROZEN.index]); - dropLevelObject.put(DropStatus.DROPPED_HIGH.name(), dropLevel[DropStatus.DROPPED_HIGH.index]); - dropLevelObject.put(DropStatus.DROPPED_MIDDLE.name(), dropLevel[DropStatus.DROPPED_MIDDLE.index]); - dropLevelObject.put(DropStatus.DROPPED_NORMAL.name(), dropLevel[DropStatus.DROPPED_NORMAL.index]); - dropLevelObject.put(DropStatus.DROPPED_BEST.name(), dropLevel[DropStatus.DROPPED_BEST.index]); - - JSONObject dropSumObject = new JSONObject(); - dropSumObject.put(DropStatus.DROPPED_FROZEN.name(), dropSum[DropStatus.DROPPED_FROZEN.index]); - dropSumObject.put(DropStatus.DROPPED_HIGH.name(), dropSum[DropStatus.DROPPED_HIGH.index]); - dropSumObject.put(DropStatus.DROPPED_MIDDLE.name(), dropSum[DropStatus.DROPPED_MIDDLE.index]); - dropSumObject.put(DropStatus.DROPPED_NORMAL.name(), dropSum[DropStatus.DROPPED_NORMAL.index]); - dropSumObject.put(DropStatus.DROPPED_BEST.name(), dropSum[DropStatus.DROPPED_BEST.index]); - - JSONObject resultObject = new JSONObject(); - resultObject = DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication()); - - resultObject.put(SharePluginInfo.ISSUE_SCENE, visibleScene); - resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject); - resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject); - resultObject.put(SharePluginInfo.ISSUE_FPS, fps); - - Issue issue = new Issue(); - issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS); - issue.setContent(resultObject); - plugin.onDetectIssue(issue); - - } catch (JSONException e) { - MatrixLog.e(TAG, "json error", e); - } finally { - sumFrame = 0; - sumDroppedFrames = 0; - sumFrameCost = 0; + dropLevel[DropStatus.DROPPED_BEST.ordinal()]++; + dropSum[DropStatus.DROPPED_BEST.ordinal()] += Math.max(droppedFrames, 0); } } + private void reset() { + dropCount = 0; + refreshRate = 0; + totalDuration = 0; + count = 0; - @Override - public String toString() { - return "visibleScene=" + visibleScene - + ", sumFrame=" + sumFrame - + ", sumDroppedFrames=" + sumDroppedFrames - + ", sumFrameCost=" + sumFrameCost - + ", dropLevel=" + Arrays.toString(dropLevel); + Arrays.fill(durations, 0); + Arrays.fill(dropLevel, 0); + Arrays.fill(dropSum, 0); } } - public enum DropStatus { - DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0); - public int index; - - DropStatus(int index) { - this.index = index; - } - + /** + * This method is reserved for compatibility. Using {@code setDropFrameListener} method + * as alternative for api level greater equal N(24). + */ + @Deprecated + public void addDropFrameListener(int dropFrameListenerThreshold, DropFrameListener dropFrameListener) { + this.oldDropFrameListener = dropFrameListener; + this.dropFrameListenerThreshold = dropFrameListenerThreshold; } - public void addDropFrameListener(int dropFrameListenerThreshold, DropFrameListener dropFrameListener) { + public void setDropFrameListener(int dropFrameListenerThreshold, IDropFrameListener dropFrameListener) { this.dropFrameListener = dropFrameListener; this.dropFrameListenerThreshold = dropFrameListenerThreshold; } public void removeDropFrameListener() { + this.oldDropFrameListener = null; this.dropFrameListener = null; } + @Deprecated public interface DropFrameListener { - void dropFrame(int droppedFrame, long jitter, String scene, long lastResumeTime); + void dropFrame(int droppedFrame, long jitter, String scene); + } + + @RequiresApi(Build.VERSION_CODES.N) + public static String metricsToString(FrameMetrics frameMetrics) { + StringBuilder sb = new StringBuilder(); + + sb.append("{unknown_delay_duration=").append(frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION)); + sb.append("; input_handling_duration=").append(frameMetrics.getMetric(FrameMetrics.INPUT_HANDLING_DURATION)); + sb.append("; animation_duration=").append(frameMetrics.getMetric(FrameMetrics.ANIMATION_DURATION)); + sb.append("; layout_measure_duration=").append(frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)); + sb.append("; draw_duration=").append(frameMetrics.getMetric(FrameMetrics.DRAW_DURATION)); + sb.append("; sync_duration=").append(frameMetrics.getMetric(FrameMetrics.SYNC_DURATION)); + sb.append("; command_issue_duration=").append(frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION)); + sb.append("; swap_buffers_duration=").append(frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION)); + sb.append("; total_duration=").append(frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)); + sb.append("; first_draw_frame=").append(frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME)); + if (FrameTracer.sdkInt >= Build.VERSION_CODES.S) { + sb.append("; gpu_duration=").append(frameMetrics.getMetric(FrameMetrics.GPU_DURATION)); + } + sb.append("}"); + + return sb.toString(); } - @RequiresApi(api = Build.VERSION_CODES.N) @Override public void onActivityCreated(final Activity activity, Bundle savedInstanceState) { - if (useFrameMetrics) { - this.refreshRate = (int) activity.getWindowManager().getDefaultDisplay().getRefreshRate(); - this.frameIntervalNs = Constants.TIME_SECOND_TO_NANO / (long) refreshRate; - Window.OnFrameMetricsAvailableListener onFrameMetricsAvailableListener = new Window.OnFrameMetricsAvailableListener() { - @RequiresApi(api = Build.VERSION_CODES.O) - @Override - public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) { - FrameMetrics frameMetricsCopy = new FrameMetrics(frameMetrics); - long vsynTime = frameMetricsCopy.getMetric(FrameMetrics.VSYNC_TIMESTAMP); - long intendedVsyncTime = frameMetricsCopy.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP); - frameMetricsCopy.getMetric(FrameMetrics.DRAW_DURATION); - notifyListener(AppActiveMatrixDelegate.INSTANCE.getVisibleScene(), intendedVsyncTime, vsynTime, true, intendedVsyncTime, 0, 0, 0); - } - }; - this.frameListenerMap.put(activity.hashCode(), onFrameMetricsAvailableListener); - activity.getWindow().addOnFrameMetricsAvailableListener(onFrameMetricsAvailableListener, new Handler()); - MatrixLog.i(TAG, "onActivityCreated addOnFrameMetricsAvailableListener"); - } + } @Override @@ -393,14 +540,85 @@ public void onActivityStarted(Activity activity) { } + private float getRefreshRate(Window window) { + if (sdkInt >= Build.VERSION_CODES.R) { + return window.getContext().getDisplay().getRefreshRate(); + } + return window.getWindowManager().getDefaultDisplay().getRefreshRate(); + } + + @RequiresApi(Build.VERSION_CODES.N) @Override public void onActivityResumed(Activity activity) { - lastResumeTimeMap.put(activity.getClass().getName(), System.currentTimeMillis()); + if (frameListenerMap.containsKey(activity.hashCode())) { + return; + } + + defaultRefreshRate = getRefreshRate(activity.getWindow()); + MatrixLog.i(TAG, "default refresh rate is %dHz", (int) defaultRefreshRate); + + Window.OnFrameMetricsAvailableListener onFrameMetricsAvailableListener = new Window.OnFrameMetricsAvailableListener() { + private float cachedRefreshRate = defaultRefreshRate; + private float cachedThreshold = dropFrameListenerThreshold / 60f * cachedRefreshRate; + private int lastModeId = -1; + private int lastThreshold = -1; + private WindowManager.LayoutParams attributes = null; + + private void updateRefreshRate(Window window) { + if (attributes == null) { + attributes = window.getAttributes(); + } + if (attributes.preferredDisplayModeId != lastModeId || lastThreshold != dropFrameListenerThreshold) { + lastModeId = attributes.preferredDisplayModeId; + lastThreshold = dropFrameListenerThreshold; + cachedRefreshRate = getRefreshRate(window); + cachedThreshold = dropFrameListenerThreshold / 60f * cachedRefreshRate; + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) { + if (isForeground()) { + // skip not available metrics. + for (int i = FrameDuration.UNKNOWN_DELAY_DURATION.ordinal(); i <= FrameDuration.TOTAL_DURATION.ordinal(); i++) { + long v = frameMetrics.getMetric(FrameDuration.indices[i]); + if (v < 0 || v >= HALF_MAX) { + // some devices will produce outliers, especially the Honor series, eg: NTH-AN00, ANY-AN00, etc. + return; + } + } + FrameMetrics frameMetricsCopy = new FrameMetrics(frameMetrics); + + updateRefreshRate(window); + + long totalDuration = frameMetricsCopy.getMetric(FrameMetrics.TOTAL_DURATION); + float frameIntervalNanos = Constants.TIME_SECOND_TO_NANO / cachedRefreshRate; + float droppedFrames = Math.max(0f, (totalDuration - frameIntervalNanos) / frameIntervalNanos); + + droppedSum += droppedFrames; + + if (dropFrameListener != null && droppedFrames >= cachedThreshold) { + dropFrameListener.onFrameMetricsAvailable(ProcessUILifecycleOwner.INSTANCE.getVisibleScene(), frameMetricsCopy, droppedFrames, cachedRefreshRate); + } + synchronized (listeners) { + for (IFrameListener observer : listeners) { + observer.onFrameMetricsAvailable(ProcessUILifecycleOwner.INSTANCE.getVisibleScene(), frameMetricsCopy, droppedFrames, cachedRefreshRate); + } + } + } + } + }; + + this.frameListenerMap.put(activity.hashCode(), onFrameMetricsAvailableListener); + activity.getWindow().addOnFrameMetricsAvailableListener(onFrameMetricsAvailableListener, MatrixHandlerThread.getDefaultHandler()); + MatrixLog.i(TAG, "onActivityResumed addOnFrameMetricsAvailableListener"); } + @RequiresApi(Build.VERSION_CODES.N) @Override public void onActivityPaused(Activity activity) { - + sceneFrameCollector.resetAllAndCallBack(); } @Override @@ -413,15 +631,83 @@ public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } - @RequiresApi(api = Build.VERSION_CODES.N) + @RequiresApi(Build.VERSION_CODES.N) @Override public void onActivityDestroyed(Activity activity) { - if (useFrameMetrics) { + try { + activity.getWindow().removeOnFrameMetricsAvailableListener(frameListenerMap.remove(activity.hashCode())); + } catch (Throwable t) { + MatrixLog.e(TAG, "removeOnFrameMetricsAvailableListener error : " + t.getMessage()); + } + } + + @RequiresApi(Build.VERSION_CODES.N) + static class AllSceneFrameListener implements ISceneFrameListener { + private static final String TAG = "AllSceneFrameListener"; + + @Override + public int getIntervalMs() { + return Constants.DEFAULT_FPS_TIME_SLICE_ALIVE_MS; + } + + @Override + public String getName() { + return null; + } + + @Override + public boolean skipFirstFrame() { + return false; + } + + @Override + public int getThreshold() { + return 0; + } + + @Override + public void onFrameMetricsAvailable(@NonNull String sceneName, long[] avgDurations, int[] dropLevel, int[] dropSum, float avgDroppedFrame, float avgRefreshRate, float avgFps) { + MatrixLog.i(TAG, "[report] FPS:%s %s", avgFps, toString()); try { - activity.getWindow().removeOnFrameMetricsAvailableListener(frameListenerMap.remove(activity.hashCode())); - } catch (Throwable t) { - MatrixLog.e(TAG, "removeOnFrameMetricsAvailableListener error : " + t.getMessage()); + TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class); + if (null == plugin) { + return; + } + JSONObject dropLevelObject = new JSONObject(); + JSONObject dropSumObject = new JSONObject(); + for (DropStatus dropStatus : DropStatus.values()) { + dropLevelObject.put(dropStatus.name(), dropLevel[dropStatus.ordinal()]); + dropSumObject.put(dropStatus.name(), dropSum[dropStatus.ordinal()]); + } + + JSONObject resultObject = new JSONObject(); + DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication()); + + resultObject.put(SharePluginInfo.ISSUE_SCENE, sceneName); + resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject); + resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject); + resultObject.put(SharePluginInfo.ISSUE_FPS, avgFps); + + for (FrameDuration frameDuration : FrameDuration.values()) { + resultObject.put(frameDuration.name(), avgDurations[frameDuration.ordinal()]); + if (frameDuration.equals(FrameDuration.TOTAL_DURATION)) { + break; + } + } + if (sdkInt >= Build.VERSION_CODES.S) { + resultObject.put("GPU_DURATION", avgDurations[FrameDuration.GPU_DURATION.ordinal()]); + } + resultObject.put("DROP_COUNT", Math.round(avgDroppedFrame)); + resultObject.put("REFRESH_RATE", (int) (avgRefreshRate)); + + Issue issue = new Issue(); + issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS); + issue.setContent(resultObject); + plugin.onDetectIssue(issue); + + } catch (JSONException e) { + MatrixLog.e(TAG, "json error", e); } } } -} +} \ No newline at end of file diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/IdleHandlerLagTracer.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/IdleHandlerLagTracer.java index a6137be4e..f339874cc 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/IdleHandlerLagTracer.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/IdleHandlerLagTracer.java @@ -86,7 +86,7 @@ public void run() { String scene = AppActiveMatrixDelegate.INSTANCE.getVisibleScene(); JSONObject jsonObject = new JSONObject(); - jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication()); + DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication()); jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.LAG_IDLE_HANDLER); jsonObject.put(SharePluginInfo.ISSUE_SCENE, scene); jsonObject.put(SharePluginInfo.ISSUE_THREAD_STACK, stackTrace); diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/LooperAnrTracer.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/LooperAnrTracer.java index 9ffa2b345..ef6ffff79 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/LooperAnrTracer.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/LooperAnrTracer.java @@ -29,8 +29,9 @@ import com.tencent.matrix.trace.config.TraceConfig; import com.tencent.matrix.trace.constants.Constants; import com.tencent.matrix.trace.core.AppMethodBeat; -import com.tencent.matrix.trace.core.UIThreadMonitor; +import com.tencent.matrix.trace.core.LooperMonitor; import com.tencent.matrix.trace.items.MethodItem; +import com.tencent.matrix.trace.listeners.ILooperListener; import com.tencent.matrix.trace.util.TraceDataUtils; import com.tencent.matrix.trace.util.Utils; import com.tencent.matrix.util.DeviceUtil; @@ -44,7 +45,7 @@ import java.util.LinkedList; import java.util.List; -public class LooperAnrTracer extends Tracer { +public class LooperAnrTracer extends Tracer implements ILooperListener { private static final String TAG = "Matrix.AnrTracer"; private Handler anrHandler; @@ -52,7 +53,7 @@ public class LooperAnrTracer extends Tracer { private final TraceConfig traceConfig; private final AnrHandleTask anrTask = new AnrHandleTask(); private final LagHandleTask lagTask = new LagHandleTask(); - private boolean isAnrTraceEnable; + private final boolean isAnrTraceEnable; public LooperAnrTracer(TraceConfig traceConfig) { this.traceConfig = traceConfig; @@ -63,7 +64,7 @@ public LooperAnrTracer(TraceConfig traceConfig) { public void onAlive() { super.onAlive(); if (isAnrTraceEnable) { - UIThreadMonitor.getMonitor().addObserver(this); + LooperMonitor.register(this); this.anrHandler = new Handler(MatrixHandlerThread.getDefaultHandler().getLooper()); this.lagHandler = new Handler(MatrixHandlerThread.getDefaultHandler().getLooper()); } @@ -73,7 +74,7 @@ public void onAlive() { public void onDead() { super.onDead(); if (isAnrTraceEnable) { - UIThreadMonitor.getMonitor().removeObserver(this); + LooperMonitor.unregister(this); anrTask.getBeginRecord().release(); anrHandler.removeCallbacksAndMessages(null); lagHandler.removeCallbacksAndMessages(null); @@ -81,29 +82,26 @@ public void onDead() { } @Override - public void dispatchBegin(long beginNs, long cpuBeginMs, long token) { - super.dispatchBegin(beginNs, cpuBeginMs, token); + public boolean isValid() { + return true; + } + @Override + public void onDispatchBegin(String log) { anrTask.beginRecord = AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin"); - anrTask.token = token; if (traceConfig.isDevEnv()) { - MatrixLog.v(TAG, "* [dispatchBegin] token:%s index:%s", token, anrTask.beginRecord.index); + MatrixLog.v(TAG, "* [dispatchBegin] index:%s", anrTask.beginRecord.index); } - long cost = (System.nanoTime() - token) / Constants.TIME_MILLIS_TO_NANO; - anrHandler.postDelayed(anrTask, Constants.DEFAULT_ANR - cost); - lagHandler.postDelayed(lagTask, Constants.DEFAULT_NORMAL_LAG - cost); + anrHandler.postDelayed(anrTask, Constants.DEFAULT_ANR); + lagHandler.postDelayed(lagTask, Constants.DEFAULT_NORMAL_LAG); } - @Override - public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isBelongFrame) { - super.dispatchEnd(beginNs, cpuBeginMs, endNs, cpuEndMs, token, isBelongFrame); + public void onDispatchEnd(String log, long beginNs, long endNs) { if (traceConfig.isDevEnv()) { long cost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO; - MatrixLog.v(TAG, "[dispatchEnd] token:%s cost:%sms cpu:%sms usage:%s", - token, cost, - cpuEndMs - cpuBeginMs, Utils.calculateCpuUsage(cpuEndMs - cpuBeginMs, cost)); + MatrixLog.v(TAG, "[dispatchEnd] beginNs:%s endNs:%s cost:%sms", beginNs, endNs, cost); } anrTask.getBeginRecord().release(); anrHandler.removeCallbacks(anrTask); @@ -126,7 +124,7 @@ public void run() { String dumpStack = Utils.getWholeStack(stackTrace); JSONObject jsonObject = new JSONObject(); - jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication()); + DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication()); jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.LAG); jsonObject.put(SharePluginInfo.ISSUE_SCENE, scene); jsonObject.put(SharePluginInfo.ISSUE_THREAD_STACK, dumpStack); @@ -178,29 +176,27 @@ public void run() { // Thread state Thread.State status = Looper.getMainLooper().getThread().getState(); - StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace(); String dumpStack; - if (traceConfig.getLooperPrinterStackStyle() == TraceConfig.STACK_STYLE_WHOLE) { - dumpStack = Utils.getWholeStack(stackTrace, "|*\t\t"); - } else { - dumpStack = Utils.getStack(stackTrace, "|*\t\t", 12); + switch (traceConfig.getLooperPrinterStackStyle()) { + case TraceConfig.STACK_STYLE_WHOLE: + dumpStack = Utils.getWholeStack(Looper.getMainLooper().getThread().getStackTrace(), "|*\t\t"); + break; + case TraceConfig.STACK_STYLE_RAW: + dumpStack = Utils.getMainThreadJavaStackTrace(); + break; + case TraceConfig.STACK_STYLE_SIMPLE: + default: + dumpStack = Utils.getStack(Looper.getMainLooper().getThread().getStackTrace(), "|*\t\t", 12); } - - // frame - UIThreadMonitor monitor = UIThreadMonitor.getMonitor(); - long inputCost = monitor.getQueueCost(UIThreadMonitor.CALLBACK_INPUT, token); - long animationCost = monitor.getQueueCost(UIThreadMonitor.CALLBACK_ANIMATION, token); - long traversalCost = monitor.getQueueCost(UIThreadMonitor.CALLBACK_TRAVERSAL, token); - // trace - LinkedList stack = new LinkedList(); + LinkedList stack = new LinkedList<>(); if (data.length > 0) { TraceDataUtils.structuredDataToStack(data, stack, true, curTime); TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() { @Override public boolean isFilter(long during, int filterCount) { - return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS; + return during < (long) filterCount * Constants.TIME_UPDATE_CYCLE_MS; } @Override @@ -211,7 +207,7 @@ public int getFilterMaxCount() { @Override public void fallback(List stack, int size) { MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack); - Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK)); + Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK)); while (iterator.hasNext()) { iterator.next(); iterator.remove(); @@ -227,8 +223,8 @@ public void fallback(List stack, int size) { // stackKey String stackKey = TraceDataUtils.getTreeKey(stack, stackCost); MatrixLog.w(TAG, "%s \npostTime:%s curTime:%s", - printAnr(scene, processStat, memoryInfo, status, logcatBuilder, isForeground, stack.size(), - stackKey, dumpStack, inputCost, animationCost, traversalCost, stackCost), + printAnr(scene, processStat, memoryInfo, status, logcatBuilder, + isForeground, stack.size(), stackKey, dumpStack, stackCost), token / Constants.TIME_MILLIS_TO_NANO, curTime); // for logcat if (stackCost >= Constants.DEFAULT_ANR_INVALID) { @@ -243,17 +239,13 @@ public void fallback(List stack, int size) { return; } JSONObject jsonObject = new JSONObject(); - jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication()); + DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication()); jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.ANR); jsonObject.put(SharePluginInfo.ISSUE_COST, stackCost); jsonObject.put(SharePluginInfo.ISSUE_STACK_KEY, stackKey); jsonObject.put(SharePluginInfo.ISSUE_SCENE, scene); jsonObject.put(SharePluginInfo.ISSUE_TRACE_STACK, reportBuilder.toString()); - if (traceConfig.getLooperPrinterStackStyle() == TraceConfig.STACK_STYLE_WHOLE) { - jsonObject.put(SharePluginInfo.ISSUE_THREAD_STACK, Utils.getWholeStack(stackTrace)); - } else { - jsonObject.put(SharePluginInfo.ISSUE_THREAD_STACK, Utils.getStack(stackTrace)); - } + jsonObject.put(SharePluginInfo.ISSUE_THREAD_STACK, dumpStack); jsonObject.put(SharePluginInfo.ISSUE_PROCESS_PRIORITY, processStat[0]); jsonObject.put(SharePluginInfo.ISSUE_PROCESS_NICE, processStat[1]); jsonObject.put(SharePluginInfo.ISSUE_PROCESS_FOREGROUND, isForeground); @@ -277,8 +269,8 @@ public void fallback(List stack, int size) { } - private String printAnr(String scene, int[] processStat, long[] memoryInfo, Thread.State state, StringBuilder stack, boolean isForeground, - long stackSize, String stackKey, String dumpStack, long inputCost, long animationCost, long traversalCost, long stackCost) { + private String printAnr(String scene, int[] processStat, long[] memoryInfo, Thread.State state, StringBuilder stack, + boolean isForeground, long stackSize, String stackKey, String dumpStack, long stackCost) { StringBuilder print = new StringBuilder(); print.append(String.format("-\n>>>>>>>>>>>>>>>>>>>>>>> maybe happens ANR(%s ms)! <<<<<<<<<<<<<<<<<<<<<<<\n", stackCost)); print.append("|* [Status]").append("\n"); @@ -291,9 +283,6 @@ private String printAnr(String scene, int[] processStat, long[] memoryInfo, Thre print.append("|*\t\tDalvikHeap: ").append(memoryInfo[0]).append("kb\n"); print.append("|*\t\tNativeHeap: ").append(memoryInfo[1]).append("kb\n"); print.append("|*\t\tVmSize: ").append(memoryInfo[2]).append("kb\n"); - print.append("|* [doFrame]").append("\n"); - print.append("|*\t\tinputCost:animationCost:traversalCost").append("\n"); - print.append("|*\t\t").append(inputCost).append(":").append(animationCost).append(":").append(traversalCost).append("\n"); print.append("|* [Thread]").append("\n"); print.append(String.format("|*\t\tStack(%s): ", state)).append(dumpStack); print.append("|* [Trace]").append("\n"); diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/SignalAnrTracer.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/SignalAnrTracer.java index d172e5a8b..7c398b30c 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/SignalAnrTracer.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/SignalAnrTracer.java @@ -24,8 +24,10 @@ import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; +import android.util.Pair; import androidx.annotation.Keep; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.tencent.matrix.AppActiveMatrixDelegate; @@ -45,17 +47,28 @@ import org.json.JSONObject; import java.io.BufferedReader; +import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class SignalAnrTracer extends Tracer { private static final String TAG = "SignalAnrTracer"; private static final String CHECK_ANR_STATE_THREAD_NAME = "Check-ANR-State-Thread"; + private static final String ANR_DUMP_THREAD_NAME = "ANR-Dump-Thread"; private static final int CHECK_ERROR_STATE_INTERVAL = 500; private static final int ANR_DUMP_MAX_TIME = 20000; + private static long anrReportTimeout = ANR_DUMP_MAX_TIME; private static final int CHECK_ERROR_STATE_COUNT = ANR_DUMP_MAX_TIME / CHECK_ERROR_STATE_INTERVAL; private static final long FOREGROUND_MSG_THRESHOLD = -2000; @@ -79,6 +92,191 @@ public class SignalAnrTracer extends Tracer { System.loadLibrary("trace-canary"); } + private static class SimpleDeadLockDetector { + static class ThreadNode { + int threadId; + String info; + String lockObjCls; + int peerId = -1; + int visit = 0; // 0 not visited, 1 visiting, 2 visited + } + + private final Pattern threadPattern = Pattern.compile("^\"(.*?)\" .*? tid=(\\d+) \\w+$"); + private final Pattern lockHeldPattern = Pattern.compile("^ - .*?\\(a (.*?)\\) held by thread (\\d+)$"); + private final StringBuilder currentSb = new StringBuilder(); + private final HashMap threadsWaitingForHeldLock = new HashMap<>(); + private LinkedList waitingList = new LinkedList<>(); + private String mainThreadInfo = ""; + private boolean threadInfoBegin = false; + private ThreadNode currentThreadInfo = new ThreadNode(); + + public void parseLine(String line) { + + if (line.isEmpty()) { + // thread info end + threadInfoBegin = false; + + if (currentSb.length() > 0 && currentThreadInfo.peerId >= 0) { + String threadInfo = currentSb.toString(); + if (currentThreadInfo.threadId == 1) { + // "currentThreadId" is a thin lock thread id. This is a small integer used by the + // thin lock implementation. This is not to be confused with the native thread's tid, + // nor is it the value returned by java.lang.Thread.getId --- this is a distinct value, + // used only for locking. usually, 0 is reserved to mean "invalid", 1 for main thread. + mainThreadInfo = threadInfo; + } + + currentThreadInfo.info = threadInfo; + threadsWaitingForHeldLock.put(currentThreadInfo.threadId, currentThreadInfo); + currentThreadInfo = new ThreadNode(); + } + + } else if (!threadInfoBegin) { + Matcher m = threadPattern.matcher(line); + if (m.find()) { + // new thread info begin + threadInfoBegin = true; + + currentSb.setLength(0); + currentSb.append(line).append('\n'); + try { + currentThreadInfo.threadId = Integer.parseInt(Objects.requireNonNull(m.group(2))); + } catch (Exception e) { + MatrixLog.e(TAG, e.toString()); + } + } + } else { + Matcher m = lockHeldPattern.matcher(line); + if (m.find()) { + try { + currentThreadInfo.lockObjCls = m.group(1); + currentThreadInfo.peerId = Integer.parseInt(Objects.requireNonNull(m.group(2))); + } catch (Exception e) { + MatrixLog.e(TAG, e.toString()); + } + } + currentSb.append(line).append('\n'); + } + } + + public boolean hasDeadLock() { + parseLine(""); // ensure thread info parse complete + return checkDeadLock(); + } + + @NonNull + public String getMainThreadInfo() { + return mainThreadInfo; + } + + @NonNull + public String getLockHeldThread1Info() { + if (waitingList == null || waitingList.size() == 0) { + return ""; + } + int threadId = waitingList.get(0).threadId; + ThreadNode node = threadsWaitingForHeldLock.get(threadId); + return node == null ? "" : node.info; + } + + @NonNull + public String getLockHeldThread2Info() { + if (waitingList == null || waitingList.size() == 0) { + return ""; + } + int threadId = waitingList.get(waitingList.size() - 1).threadId; + ThreadNode node = threadsWaitingForHeldLock.get(threadId); + return node == null ? "" : node.info; + } + + private static class Pair implements Map.Entry { + F f; + S s; + + Pair(F f, S s) { + this.f = f; + this.s = s; + } + + @Override + public F getKey() { + return f; + } + + @Override + public S getValue() { + return s; + } + + @Override + public S setValue(S value) { + return s = value; + } + + @Override + public String toString() { + return "Pair{" + "f=" + f + ", s=" + s + '}'; + } + } + + @NonNull + public Map.Entry getWaitingThreadsInfo() { + if (waitingList.size() == 0) { + return new Pair<>(null, null); + } else { + int[] threadsId = new int[waitingList.size()]; + String[] locksType = new String[waitingList.size()]; + int idx = 0; + for (ThreadNode threadNode : waitingList) { + threadsId[idx] = threadNode.threadId; + locksType[idx] = threadNode.lockObjCls; + ++idx; + } + return new Pair<>(threadsId, locksType); + } + } + + private boolean checkDeadLock() { + waitingList.clear(); + for (Map.Entry nodeEntry : threadsWaitingForHeldLock.entrySet()) { + ThreadNode node = nodeEntry.getValue(); + if (node.visit == 0) { + ThreadNode ret; + if ((ret = dfsSearch(node)) != null) { + // retrieve cycle from path and save it in waitingList + while (waitingList.size() > 0 && waitingList.getFirst() != ret) { + waitingList.removeFirst(); + } + return true; + } + } + } + return false; + } + + // Return the entry point if a cycle is found, else return null. + private ThreadNode dfsSearch(ThreadNode node) { + waitingList.addLast(node); + node.visit = 1; + + ThreadNode peerNode = threadsWaitingForHeldLock.get(node.peerId); + if (peerNode != null) { + if (peerNode.visit == 1) { + return peerNode; + } + + ThreadNode ret; + if (peerNode.visit == 0 && (ret = dfsSearch(peerNode)) != null) { + return ret; + } + } + + node.visit = 2; + waitingList.removeLast(); + return null; + } + } + @Override protected void onAlive() { super.onAlive(); @@ -114,6 +312,10 @@ public SignalAnrTracer(Application application, String anrTraceFilePath, String sApplication = application; } + public static void setAnrReportTimeout(long timeout) { + anrReportTimeout = timeout; + } + public void setSignalAnrDetectedListener(SignalAnrDetectedListener listener) { sSignalAnrDetectedListener = listener; } @@ -150,21 +352,52 @@ public void run() { @RequiresApi(api = Build.VERSION_CODES.M) @Keep private synchronized static void onANRDumped() { - onAnrDumpedTimeStamp = System.currentTimeMillis(); - MatrixLog.i(TAG, "onANRDumped"); - stackTrace = Utils.getMainThreadJavaStackTrace(); - MatrixLog.i(TAG, "onANRDumped, stackTrace = %s, duration = %d", stackTrace, (System.currentTimeMillis() - onAnrDumpedTimeStamp)); - cgroup = readCgroup(); - MatrixLog.i(TAG, "onANRDumped, read cgroup duration = %d", (System.currentTimeMillis() - onAnrDumpedTimeStamp)); - currentForeground = AppForegroundUtil.isInterestingToUser(); - MatrixLog.i(TAG, "onANRDumped, isInterestingToUser duration = %d", (System.currentTimeMillis() - onAnrDumpedTimeStamp)); - confirmRealAnr(true); + final CountDownLatch anrDumpLatch = new CountDownLatch(1); + new Thread(new Runnable() { + @Override + public void run() { + onAnrDumpedTimeStamp = System.currentTimeMillis(); + MatrixLog.i(TAG, "onANRDumped"); + stackTrace = Utils.getMainThreadJavaStackTrace(); + MatrixLog.i(TAG, "onANRDumped, stackTrace = %s, duration = %d", stackTrace, (System.currentTimeMillis() - onAnrDumpedTimeStamp)); + cgroup = readCgroup(); + MatrixLog.i(TAG, "onANRDumped, read cgroup duration = %d", (System.currentTimeMillis() - onAnrDumpedTimeStamp)); + currentForeground = AppForegroundUtil.isInterestingToUser(); + MatrixLog.i(TAG, "onANRDumped, isInterestingToUser duration = %d", (System.currentTimeMillis() - onAnrDumpedTimeStamp)); + confirmRealAnr(true); + anrDumpLatch.countDown(); + } + }, ANR_DUMP_THREAD_NAME).start(); + + try { + anrDumpLatch.await(anrReportTimeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + //empty here + } } @Keep private static void onANRDumpTrace() { try { - MatrixUtil.printFileByLine(TAG, sAnrTraceFilePath); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File(sAnrTraceFilePath)), "UTF-8"))) { + String line; + SimpleDeadLockDetector detector = new SimpleDeadLockDetector(); + while ((line = reader.readLine()) != null) { + detector.parseLine(line); + MatrixLog.i(TAG, line); + } + if (sSignalAnrDetectedListener != null) { + if (detector.hasDeadLock()) { + sSignalAnrDetectedListener.onDeadLockAnrDetected( + detector.getMainThreadInfo(), detector.getLockHeldThread1Info(), + detector.getLockHeldThread2Info(), detector.getWaitingThreadsInfo()); + } else if (detector.getMainThreadInfo().contains("android.os.MessageQueue.nativePollOnce")) { + sSignalAnrDetectedListener.onMainThreadStuckAtNativePollOnce(detector.getMainThreadInfo()); + } + } + } catch (Throwable t) { + MatrixLog.e(TAG, "printFileByLine failed e : " + t.getMessage()); + } } catch (Throwable t) { MatrixLog.e(TAG, "onANRDumpTrace error: %s", t.getMessage()); } @@ -347,6 +580,11 @@ public static void printTrace() { public interface SignalAnrDetectedListener { void onAnrDetected(String stackTrace, String mMessageString, long mMessageWhen, boolean fromProcessErrorState, String cpuset); + void onNativeBacktraceDetected(String backtrace, String mMessageString, long mMessageWhen, boolean fromProcessErrorState); + + void onDeadLockAnrDetected(String mainThreadStackTrace, String lockHeldThread1, String lockHeldThread2, Map.Entry waitingList); + + void onMainThreadStuckAtNativePollOnce(String mainThreadStackTrace); } } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/ThreadPriorityTracer.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/ThreadTracer.java similarity index 74% rename from matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/ThreadPriorityTracer.java rename to matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/ThreadTracer.java index a07041629..5b31de460 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/ThreadPriorityTracer.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/ThreadTracer.java @@ -29,10 +29,13 @@ import org.json.JSONObject; -public class ThreadPriorityTracer extends Tracer { +public class ThreadTracer extends Tracer { private static final String TAG = "ThreadPriorityTracer"; private static MainThreadPriorityModifiedListener sMainThreadPriorityModifiedListener; + private static PthreadKeyCallback sPthreadKeyCallback; + private static boolean enableThreadPriorityTracer = false; + private static boolean enablePthreadKeyTracer = false; static { System.loadLibrary("trace-canary"); @@ -41,7 +44,9 @@ public class ThreadPriorityTracer extends Tracer { @Override protected void onAlive() { super.onAlive(); - nativeInitMainThreadPriorityDetective(); + if (enableThreadPriorityTracer || enablePthreadKeyTracer) { + nativeInitThreadHook(enableThreadPriorityTracer ? 1 : 0, enablePthreadKeyTracer ? 1 : 0); + } } @Override @@ -50,10 +55,21 @@ protected void onDead() { } public void setMainThreadPriorityModifiedListener(MainThreadPriorityModifiedListener mainThreadPriorityModifiedListener) { - this.sMainThreadPriorityModifiedListener = mainThreadPriorityModifiedListener; + enableThreadPriorityTracer = true; + sMainThreadPriorityModifiedListener = mainThreadPriorityModifiedListener; + } + + public void setPthreadKeyCallback(PthreadKeyCallback callback) { + enablePthreadKeyTracer = true; + sPthreadKeyCallback = callback; } - private static native void nativeInitMainThreadPriorityDetective(); + public static int getPthreadKeySeq() { + return nativeGetPthreadKeySeq(); + } + + private static native void nativeInitThreadHook(int priority, int phreadKey); + private static native int nativeGetPthreadKeySeq(); @Keep private static void onMainThreadPriorityModified(int priorityBefore, int priorityAfter) { @@ -115,12 +131,27 @@ private static void onMainThreadTimerSlackModified(long timerSlack) { } catch (Throwable t) { MatrixLog.e(TAG, "onMainThreadPriorityModified error: %s", t.getMessage()); } + } + @Keep + private static void pthreadKeyCallback(int type, int ret, int keySeq, String soPath, String backtrace) { + if (sPthreadKeyCallback != null) { + if (type == 0) { + sPthreadKeyCallback.onPthreadCreate(ret, keySeq, soPath, backtrace); + } else if (type == 1) { + sPthreadKeyCallback.onPthreadDelete(ret, keySeq, soPath, backtrace); + } + + } } public interface MainThreadPriorityModifiedListener { void onMainThreadPriorityModified(int priorityBefore, int priorityAfter); - void onMainThreadTimerSlackModified(long timerSlack); } + + public interface PthreadKeyCallback { + void onPthreadCreate(int ret, int keyIndex, String soPath, String backtrace); + void onPthreadDelete(int ret, int keyIndex, String soPath, String backtrace); + } } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/Tracer.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/Tracer.java index 4b2127e95..3d6e1a8f8 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/Tracer.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/tracer/Tracer.java @@ -18,11 +18,10 @@ import androidx.annotation.CallSuper; -import com.tencent.matrix.AppActiveMatrixDelegate; -import com.tencent.matrix.trace.listeners.LooperObserver; +import com.tencent.matrix.lifecycle.owners.ProcessUILifecycleOwner; import com.tencent.matrix.util.MatrixLog; -public abstract class Tracer extends LooperObserver implements ITracer { +public abstract class Tracer implements ITracer { private volatile boolean isAlive = false; private static final String TAG = "Matrix.Tracer"; @@ -65,6 +64,6 @@ public boolean isAlive() { } public boolean isForeground() { - return AppActiveMatrixDelegate.INSTANCE.isAppForeground(); + return ProcessUILifecycleOwner.INSTANCE.isProcessForeground(); } } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/util/TraceDataUtils.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/util/TraceDataUtils.java index 57cb6aa69..aa34ae16a 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/util/TraceDataUtils.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/util/TraceDataUtils.java @@ -58,7 +58,7 @@ public static void structuredDataToStack(long[] buffer, LinkedList r } if (!isBegin) { - MatrixLog.d(TAG, "never begin! pass this method[%s]", getMethodId(trueId)); + // MatrixLog.d(TAG, "never begin! pass this method[%s]", getMethodId(trueId)); continue; } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/util/Utils.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/util/Utils.java index 1852d62b8..39f637172 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/util/Utils.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/util/Utils.java @@ -87,16 +87,12 @@ public static String getMainThreadJavaStackTrace() { return stackTrace.toString(); } - public static String calculateCpuUsage(long threadMs, long ms) { - if (threadMs <= 0) { - return ms > 1000 ? "0%" : "100%"; - } - - if (threadMs >= ms) { - return "100%"; + public static String getJavaStackTrace() { + StringBuilder stackTrace = new StringBuilder(); + for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) { + stackTrace.append(stackTraceElement.toString()).append("\n"); } - - return String.format("%.2f", 1.f * threadMs / ms * 100) + "%"; + return stackTrace.toString(); } public static boolean isEmpty(String str) { diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/view/FloatFrameView.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/view/FloatFrameView.java index 5af2f4dcd..dfac506b1 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/view/FloatFrameView.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/view/FloatFrameView.java @@ -22,6 +22,7 @@ import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; +import android.os.Build; import android.text.TextPaint; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -30,27 +31,40 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.RequiresApi; + import com.tencent.matrix.trace.R; import com.tencent.matrix.trace.constants.Constants; import java.util.LinkedList; +@RequiresApi(Build.VERSION_CODES.N) public class FloatFrameView extends LinearLayout { public TextView fpsView; + public TextView sceneView; public LineChartView chartView; - public TextView levelFrozenView; - public TextView levelHighView; - public TextView levelMiddleView; - public TextView levelNormalView; - public TextView sumLevelFrozenView; - public TextView sumLevelHighView; - public TextView sumLevelMiddleView; - public TextView sumLevelNormalView; + public TextView extraInfoView; - public TextView sceneView; - public TextView qiWangView; - public TextView sumQiWangView; + public TextView unknownDelayDurationView; + public TextView inputHandlingDurationView; + public TextView animationDurationView; + public TextView layoutMeasureDurationView; + public TextView drawDurationView; + public TextView syncDurationView; + public TextView commandIssueDurationView; + public TextView swapBuffersDurationView; + public TextView gpuDurationView; + public TextView totalDurationView; + + public TextView sumNormalView; + public TextView sumMiddleView; + public TextView sumHighView; + public TextView sumFrozenView; + public TextView levelNormalView; + public TextView levelMiddleView; + public TextView levelHighView; + public TextView levelFrozenView; public FloatFrameView(Context context) { super(context); @@ -71,17 +85,26 @@ private void initView(Context context) { sceneView = findViewById(R.id.scene_view); extraInfoView.setText("{other info}"); - qiWangView = findViewById(R.id.qi_wang_tv); - levelFrozenView = findViewById(R.id.level_frozen); - levelHighView = findViewById(R.id.level_high); - levelMiddleView = findViewById(R.id.level_middle); + unknownDelayDurationView = findViewById(R.id.unknown_delay_duration_tv); + inputHandlingDurationView = findViewById(R.id.input_handling_duration_tv); + animationDurationView = findViewById(R.id.animation_duration_tv); + layoutMeasureDurationView = findViewById(R.id.layout_measure_duration_tv); + drawDurationView = findViewById(R.id.draw_duration_tv); + syncDurationView = findViewById(R.id.sync_duration_tv); + commandIssueDurationView = findViewById(R.id.command_issue_duration_tv); + swapBuffersDurationView = findViewById(R.id.swap_buffers_duration_tv); + gpuDurationView = findViewById(R.id.gpu_duration_tv); + totalDurationView = findViewById(R.id.total_duration_tv); + + sumNormalView = findViewById(R.id.sum_normal); + sumMiddleView = findViewById(R.id.sum_middle); + sumHighView = findViewById(R.id.sum_high); + sumFrozenView = findViewById(R.id.sum_frozen); levelNormalView = findViewById(R.id.level_normal); + levelMiddleView = findViewById(R.id.level_middle); + levelHighView = findViewById(R.id.level_high); + levelFrozenView = findViewById(R.id.level_frozen); - sumQiWangView = findViewById(R.id.sum_qi_wang_tv); - sumLevelFrozenView = findViewById(R.id.sum_level_frozen); - sumLevelHighView = findViewById(R.id.sum_level_high); - sumLevelMiddleView = findViewById(R.id.sum_level_middle); - sumLevelNormalView = findViewById(R.id.sum_level_normal); chartView = findViewById(R.id.chart); } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/view/FrameDecorator.java b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/view/FrameDecorator.java index f2dc95f9e..e587f4d09 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/view/FrameDecorator.java +++ b/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/view/FrameDecorator.java @@ -33,58 +33,67 @@ import android.view.animation.AccelerateInterpolator; import android.widget.TextView; -import com.tencent.matrix.AppActiveMatrixDelegate; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + import com.tencent.matrix.Matrix; -import com.tencent.matrix.listeners.IAppForeground; +import com.tencent.matrix.lifecycle.IStateObserver; +import com.tencent.matrix.lifecycle.owners.ProcessUIResumedStateOwner; import com.tencent.matrix.trace.R; import com.tencent.matrix.trace.TracePlugin; import com.tencent.matrix.trace.constants.Constants; -import com.tencent.matrix.trace.core.UIThreadMonitor; -import com.tencent.matrix.trace.listeners.IDoFrameListener; +import com.tencent.matrix.trace.listeners.ISceneFrameListener; import com.tencent.matrix.trace.tracer.FrameTracer; -import com.tencent.matrix.util.MatrixHandlerThread; import com.tencent.matrix.util.MatrixLog; -import java.util.Objects; -import java.util.concurrent.Executor; +import java.util.Arrays; -public class FrameDecorator extends IDoFrameListener implements IAppForeground { +@RequiresApi(Build.VERSION_CODES.N) +public class FrameDecorator implements ISceneFrameListener { private static final String TAG = "Matrix.FrameDecorator"; private WindowManager windowManager; private WindowManager.LayoutParams layoutParam; private boolean isShowing; - private FloatFrameView view; - private static Handler mainHandler = new Handler(Looper.getMainLooper()); - private Handler handler; + private final FloatFrameView view; + private static final Handler mainHandler = new Handler(Looper.getMainLooper()); private static FrameDecorator instance; private static final Object lock = new Object(); private View.OnClickListener clickListener; - private DisplayMetrics displayMetrics = new DisplayMetrics(); + private final DisplayMetrics displayMetrics = new DisplayMetrics(); private boolean isEnable = true; - private float frameIntervalMs; - private float maxFps; + private static final int sdkInt = Build.VERSION.SDK_INT; + private final int bestColor; + private final int normalColor; + private final int middleColor; + private final int highColor; + private final int frozenColor; + private int belongColor; - private int bestColor; - private int normalColor; - private int middleColor; - private int highColor; - private int frozenColor; + private IStateObserver mProcessForegroundListener = new IStateObserver() { + @Override + public void on() { + onForeground(true); + } + @Override + public void off() { + onForeground(false); + } + }; @SuppressLint("ClickableViewAccessibility") private FrameDecorator(Context context, final FloatFrameView view) { - this.frameIntervalMs = 1f * UIThreadMonitor.getMonitor().getFrameIntervalNanos() / Constants.TIME_MILLIS_TO_NANO; - this.maxFps = Math.round(1000f / frameIntervalMs); this.view = view; - view.fpsView.setText(String.format("%.2f FPS", maxFps)); this.bestColor = context.getResources().getColor(R.color.level_best_color); this.normalColor = context.getResources().getColor(R.color.level_normal_color); this.middleColor = context.getResources().getColor(R.color.level_middle_color); this.highColor = context.getResources().getColor(R.color.level_high_color); this.frozenColor = context.getResources().getColor(R.color.level_frozen_color); + belongColor = bestColor; + + ProcessUIResumedStateOwner.INSTANCE.observeForever(mProcessForegroundListener); - AppActiveMatrixDelegate.INSTANCE.addListener(this); view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { @@ -93,7 +102,7 @@ public void onViewAttachedToWindow(View v) { TracePlugin tracePlugin = Matrix.with().getPluginByClass(TracePlugin.class); if (null != tracePlugin) { FrameTracer tracer = tracePlugin.getFrameTracer(); - tracer.addListener(FrameDecorator.this); + tracer.register(FrameDecorator.this); } } } @@ -105,7 +114,7 @@ public void onViewDetachedFromWindow(View v) { TracePlugin tracePlugin = Matrix.with().getPluginByClass(TracePlugin.class); if (null != tracePlugin) { FrameTracer tracer = tracePlugin.getFrameTracer(); - tracer.removeListener(FrameDecorator.this); + tracer.unregister(FrameDecorator.this); } } } @@ -148,8 +157,7 @@ public void onAnimationUpdate(ValueAnimator animation) { if (!isShowing) { return; } - int value = (int) animation.getAnimatedValue("trans"); - layoutParam.x = value; + layoutParam.x = (int) animation.getAnimatedValue("trans"); windowManager.updateViewLayout(v, layoutParam); } }); @@ -185,165 +193,6 @@ public void setExtraInfo(String info) { } - private long sumFrameCost; - private long[] lastCost = new long[1]; - private long sumFrames; - private int belongColor = bestColor; - private long[] lastFrames = new long[1]; - private int[] dropLevel = new int[FrameTracer.DropStatus.values().length]; - private int[] sumDropLevel = new int[FrameTracer.DropStatus.values().length]; - private String lastVisibleScene = "default"; - - private Runnable updateDefaultRunnable = new Runnable() { - @Override - public void run() { - view.fpsView.setText(String.format("%.2f FPS", maxFps)); - view.fpsView.setTextColor(view.getResources().getColor(R.color.level_best_color)); - } - }; - - - @Override - public void doFrameAsync(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) { - super.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); - - if (!Objects.equals(focusedActivity, lastVisibleScene)) { - dropLevel = new int[FrameTracer.DropStatus.values().length]; - lastVisibleScene = focusedActivity; - lastCost[0] = 0; - lastFrames[0] = 0; - } - - sumFrameCost += (dropFrame + 1) * frameIntervalMs; - sumFrames += 1; - float duration = sumFrameCost - lastCost[0]; - - if (dropFrame >= Constants.DEFAULT_DROPPED_FROZEN) { - dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++; - sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++; - belongColor = frozenColor; - } else if (dropFrame >= Constants.DEFAULT_DROPPED_HIGH) { - dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++; - sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++; - if (belongColor != frozenColor) { - belongColor = highColor; - } - } else if (dropFrame >= Constants.DEFAULT_DROPPED_MIDDLE) { - dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++; - sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++; - if (belongColor != frozenColor && belongColor != highColor) { - belongColor = middleColor; - } - } else if (dropFrame >= Constants.DEFAULT_DROPPED_NORMAL) { - dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++; - sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++; - if (belongColor != frozenColor && belongColor != highColor && belongColor != middleColor) { - belongColor = normalColor; - } - } else { - dropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++; - sumDropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++; - if (belongColor != frozenColor && belongColor != highColor && belongColor != middleColor && belongColor != normalColor) { - belongColor = bestColor; - } - } - - - long collectFrame = sumFrames - lastFrames[0]; - if (duration >= 200) { - final float fps = Math.min(maxFps, 1000.f * collectFrame / duration); - updateView(view, fps, belongColor, - dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index], - dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index], - dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index], - dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index], - sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index], - sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index], - sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index], - sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]); - belongColor = bestColor; - lastCost[0] = sumFrameCost; - lastFrames[0] = sumFrames; - mainHandler.removeCallbacks(updateDefaultRunnable); - mainHandler.postDelayed(updateDefaultRunnable, 250); - } - } - - private void updateView(final FloatFrameView view, final float fps, final int belongColor, - final int normal, final int middle, final int high, final int frozen, - final int sumNormal, final int sumMiddle, final int sumHigh, final int sumFrozen) { - int all = normal + middle + high + frozen; - float frozenValue = all <= 0 ? 0 : 1.f * frozen / all * 60; - float highValue = all <= 0 ? 0 : 1.f * high / all * 25; - float middleValue = all <= 0 ? 0 : 1.f * middle / all * 14; - float normaValue = all <= 0 ? 0 : 1.f * normal / all * 1; - float qiWang = frozenValue + highValue + middleValue + normaValue; - - int sumAll = sumNormal + sumMiddle + sumHigh + sumFrozen; - float sumFrozenValue = sumAll <= 0 ? 0 : 1.f * sumFrozen / sumAll * 60; - float sumHighValue = sumAll <= 0 ? 0 : 1.f * sumHigh / sumAll * 25; - float sumMiddleValue = sumAll <= 0 ? 0 : 1.f * sumMiddle / sumAll * 14; - float sumNormaValue = sumAll <= 0 ? 0 : 1.f * sumNormal / sumAll * 1; - float sumQiWang = sumFrozenValue + sumHighValue + sumMiddleValue + sumNormaValue; - - final String radioFrozen = String.format("%.1f", frozenValue); - final String radioHigh = String.format("%.1f", highValue); - final String radioMiddle = String.format("%.1f", middleValue); - final String radioNormal = String.format("%.1f", normaValue); - final String qiWangStr = String.format("current: %.1f", qiWang); - - final String sumRadioFrozen = String.format("%.1f", sumFrozenValue); - final String sumRadioHigh = String.format("%.1f", sumHighValue); - final String sumRadioMiddle = String.format("%.1f", sumMiddleValue); - final String sumRadioNormal = String.format("%.1f", sumNormaValue); - final String sumQiWangStr = String.format("sum: %.1f", sumQiWang); - - final String fpsStr = String.format("%.2f FPS", fps); - - mainHandler.post(new Runnable() { - @Override - public void run() { - view.chartView.addFps((int) fps, belongColor); - view.fpsView.setText(fpsStr); - view.fpsView.setTextColor(belongColor); - - view.qiWangView.setText(qiWangStr); - view.levelFrozenView.setText(radioFrozen); - view.levelHighView.setText(radioHigh); - view.levelMiddleView.setText(radioMiddle); - view.levelNormalView.setText(radioNormal); - - view.sumQiWangView.setText(sumQiWangStr); - view.sumLevelFrozenView.setText(sumRadioFrozen); - view.sumLevelHighView.setText(sumRadioHigh); - view.sumLevelMiddleView.setText(sumRadioMiddle); - view.sumLevelNormalView.setText(sumRadioNormal); - - } - }); - } - - @Override - public Executor getExecutor() { - return executor; - } - - private Executor executor = new Executor() { - @Override - public void execute(Runnable command) { - getHandler().post(command); - } - }; - - private Handler getHandler() { - if (handler == null || !handler.getLooper().getThread().isAlive()) { - if (null != MatrixHandlerThread.getDefaultHandlerThread()) { - handler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper()); - } - } - return handler; - } - public static FrameDecorator get() { return instance; } @@ -418,7 +267,6 @@ public void run() { if (!isShowing) { isShowing = true; windowManager.addView(view, layoutParam); - } } }); @@ -452,8 +300,7 @@ public boolean isShowing() { return isShowing; } - @Override - public void onForeground(final boolean isForeground) { + private void onForeground(final boolean isForeground) { MatrixLog.i(TAG, "[onForeground] isForeground:%s", isForeground); if (!isEnable) { return; @@ -472,4 +319,86 @@ public void run() { } } + @Override + public int getIntervalMs() { + return 200; + } + + @Override + public String getName() { + return null; + } + + @Override + public boolean skipFirstFrame() { + return false; + } + + @Override + public int getThreshold() { + return 0; + } + + @SuppressLint("DefaultLocale") + @Override + public void onFrameMetricsAvailable(@NonNull String sceneName, long[] avgDurations, int[] dropLevel, int[] dropSum, float avgDroppedFrame, float avgRefreshRate, final float fps) { + final String unknownDelay = String.format("unknown delay: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.UNKNOWN_DELAY_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String inputHandling = String.format("input handling: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.INPUT_HANDLING_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String animation = String.format("animation: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.ANIMATION_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String layoutMeasure = String.format("layout measure: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.LAYOUT_MEASURE_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String draw = String.format("draw: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.DRAW_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String sync = String.format("sync: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.SYNC_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String commandIssue = String.format("command issue: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.COMMAND_ISSUE_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String swapBuffers = String.format("swap buffers: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.SWAP_BUFFERS_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String gpu = String.format("gpu: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.GPU_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + final String total = String.format("total: %.1fms", (double) avgDurations[FrameTracer.FrameDuration.TOTAL_DURATION.ordinal()] / Constants.TIME_MILLIS_TO_NANO); + + if (fps <= avgRefreshRate - Constants.DEFAULT_DROPPED_FROZEN) { + belongColor = frozenColor; + } else if (fps <= avgRefreshRate - Constants.DEFAULT_DROPPED_HIGH) { + belongColor = highColor; + } else if (fps <= avgRefreshRate - Constants.DEFAULT_DROPPED_MIDDLE) { + belongColor = middleColor; + } else if (fps <= avgRefreshRate - Constants.DEFAULT_DROPPED_NORMAL) { + belongColor = normalColor; + } else { + belongColor = bestColor; + } + + final int[] level = Arrays.copyOf(dropLevel, dropLevel.length); + final int[] sum = Arrays.copyOf(dropSum, dropSum.length); + + mainHandler.post(new Runnable() { + @Override + public void run() { + view.chartView.addFps((int) fps, belongColor); + view.fpsView.setText(String.format("%.2f FPS", fps)); + view.fpsView.setTextColor(belongColor); + + view.unknownDelayDurationView.setText(unknownDelay); + view.inputHandlingDurationView.setText(inputHandling); + view.animationDurationView.setText(animation); + view.layoutMeasureDurationView.setText(layoutMeasure); + view.drawDurationView.setText(draw); + view.syncDurationView.setText(sync); + view.commandIssueDurationView.setText(commandIssue); + view.swapBuffersDurationView.setText(swapBuffers); + if (sdkInt >= Build.VERSION_CODES.S) { + view.gpuDurationView.setText(gpu); + } else { + view.gpuDurationView.setText("gpu: unusable"); + } + view.totalDurationView.setText(total); + + view.sumNormalView.setText(String.valueOf(sum[FrameTracer.DropStatus.DROPPED_NORMAL.ordinal()])); + view.sumMiddleView.setText(String.valueOf(sum[FrameTracer.DropStatus.DROPPED_MIDDLE.ordinal()])); + view.sumHighView.setText(String.valueOf(sum[FrameTracer.DropStatus.DROPPED_HIGH.ordinal()])); + view.sumFrozenView.setText(String.valueOf(sum[FrameTracer.DropStatus.DROPPED_FROZEN.ordinal()])); + view.levelNormalView.setText(String.valueOf(level[FrameTracer.DropStatus.DROPPED_NORMAL.ordinal()])); + view.levelMiddleView.setText(String.valueOf(level[FrameTracer.DropStatus.DROPPED_MIDDLE.ordinal()])); + view.levelHighView.setText(String.valueOf(level[FrameTracer.DropStatus.DROPPED_HIGH.ordinal()])); + view.levelFrozenView.setText(String.valueOf(level[FrameTracer.DropStatus.DROPPED_FROZEN.ordinal()])); + } + }); + } } diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/res/layout/float_frame_view.xml b/matrix/matrix-android/matrix-trace-canary/src/main/res/layout/float_frame_view.xml index 3e0c4b570..5dc13d0a2 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/res/layout/float_frame_view.xml +++ b/matrix/matrix-android/matrix-trace-canary/src/main/res/layout/float_frame_view.xml @@ -34,14 +34,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" - android:minWidth="76dp" + android:minWidth="100dp" android:orientation="vertical"> @@ -49,7 +49,7 @@ android:id="@+id/scene_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="left" + android:gravity="start" android:textColor="@color/dark_text" android:textSize="8dp" @@ -68,14 +68,86 @@ + + + + + + + + + + + + + + + + + + + - - @@ -165,9 +223,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:layout_gravity="center" android:background="@color/level_middle_color" - android:gravity="center" - android:maxHeight="20dp" android:text="0" android:textColor="@android:color/white" android:textSize="8dp" /> @@ -177,9 +234,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:layout_gravity="center" android:background="@color/level_high_color" - android:gravity="center" - android:maxHeight="20dp" android:text="0" android:textColor="@android:color/white" android:textSize="8dp" /> @@ -189,9 +245,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:layout_gravity="center" android:background="@color/level_frozen_color" - android:gravity="center" - android:maxHeight="20dp" android:text="0" android:textColor="@android:color/white" android:textSize="8dp" /> diff --git a/matrix/matrix-android/matrix-trace-canary/src/main/res/values/colors.xml b/matrix/matrix-android/matrix-trace-canary/src/main/res/values/colors.xml index 8f80c7293..8521b7122 100644 --- a/matrix/matrix-android/matrix-trace-canary/src/main/res/values/colors.xml +++ b/matrix/matrix-android/matrix-trace-canary/src/main/res/values/colors.xml @@ -1,9 +1,9 @@ #999999 - #0eb83a - #FFD700 - #EE7600 - #DC143C - #99FFFF + #0cf300 + #40bf00 + #708f00 + #bb4400 + #f00f00 \ No newline at end of file diff --git a/matrix/matrix-android/matrix-traffic/CMakeLists.txt b/matrix/matrix-android/matrix-traffic/CMakeLists.txt index 7d9466f12..f3fe1379c 100644 --- a/matrix/matrix-android/matrix-traffic/CMakeLists.txt +++ b/matrix/matrix-android/matrix-traffic/CMakeLists.txt @@ -31,6 +31,7 @@ find_library( log-lib target_link_libraries(matrix-traffic PRIVATE ${log-lib} PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libxhook.a + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libsemi_dlfcn.a PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libwechatbacktrace.so ) diff --git a/matrix/matrix-android/matrix-traffic/src/main/cpp/MatrixTraffic.cc b/matrix/matrix-android/matrix-traffic/src/main/cpp/MatrixTraffic.cc index 694dca159..110c59fea 100644 --- a/matrix/matrix-android/matrix-traffic/src/main/cpp/MatrixTraffic.cc +++ b/matrix/matrix-android/matrix-traffic/src/main/cpp/MatrixTraffic.cc @@ -28,9 +28,11 @@ #include #include +#include #include #include #include +#include #include "BacktraceDefine.h" #include "Backtrace.h" @@ -44,9 +46,14 @@ using namespace MatrixTraffic; static bool HOOKED = false; static bool sDumpNativeBackTrace = false; +static map> backtraceMap; +static shared_mutex backtraceMapLock; + static struct StacktraceJNI { jclass TrafficPlugin; jmethodID TrafficPlugin_setFdStackTrace; + jmethodID TrafficPlugin_clearFdInfo; + jmethodID TrafficPlugin_printLog; } gJ; @@ -59,11 +66,6 @@ void makeNativeStack(wechat_backtrace::Backtrace* backtrace, char *&stack) { auto _callback = [&](wechat_backtrace::FrameDetail it) { std::string so_name = it.map_name; - char *demangled_name = nullptr; - int status = 0; - - demangled_name = abi::__cxa_demangle(it.function_name, nullptr, 0, &status); - if (strstr(it.map_name, "libmatrix-traffic.so") || strstr(it.map_name, "libwechatbacktrace.so")) { return; } @@ -72,9 +74,6 @@ void makeNativeStack(wechat_backtrace::Backtrace* backtrace, char *&stack) { << "#" << std::dec << (index++) << " pc " << std::hex << it.rel_pc << " " << it.map_name - << " (" - << (demangled_name ? demangled_name : "null") - << ")" << std::endl; if (last_so_name != it.map_name) { last_so_name = it.map_name; @@ -82,10 +81,6 @@ void makeNativeStack(wechat_backtrace::Backtrace* backtrace, char *&stack) { } brief_stack_builder << std::hex << it.rel_pc << ";"; - - if (demangled_name) { - free(demangled_name); - } }; wechat_backtrace::restore_frame_detail(backtrace->frames.get(), backtrace->frame_size, @@ -95,79 +90,90 @@ void makeNativeStack(wechat_backtrace::Backtrace* backtrace, char *&stack) { strcpy(stack, full_stack_builder.str().c_str()); } - -static char* getNativeBacktrace() { +static void saveNativeBackTrace(const char* key) { wechat_backtrace::Backtrace *backtracePrt; - - wechat_backtrace::Backtrace backtrace_zero = BACKTRACE_INITIALIZER( - 16); - - + int max_frames = 16; backtracePrt = new wechat_backtrace::Backtrace; - backtracePrt->max_frames = backtrace_zero.max_frames; - backtracePrt->frame_size = backtrace_zero.frame_size; - backtracePrt->frames = backtrace_zero.frames; + backtracePrt->max_frames = max_frames; + backtracePrt->frame_size = 0; + backtracePrt->frames = std::shared_ptr( + new wechat_backtrace::Frame[max_frames], std::default_delete()); wechat_backtrace::unwind_adapter(backtracePrt->frames.get(), backtracePrt->max_frames, backtracePrt->frame_size); - char* nativeStack; - makeNativeStack(backtracePrt, nativeStack); - return nativeStack; + string keyString(key); + backtraceMapLock.lock(); + backtraceMap[keyString] = static_cast>(backtracePrt); + backtraceMapLock.unlock(); } - -int (*original_connect)(int fd, const struct sockaddr* addr, socklen_t addr_length); -int my_connect(int fd, sockaddr *addr, socklen_t addr_length) { - TrafficCollector::enQueueConnect(fd, addr, addr_length); - return original_connect(fd, addr, addr_length); +static char* getNativeBacktrace(string keyString) { + if (backtraceMap.count(keyString) > 0) { + backtraceMapLock.lock_shared(); + wechat_backtrace::Backtrace *backtracePrt = backtraceMap[keyString].get(); + backtraceMapLock.unlock_shared(); + char* nativeStack; + makeNativeStack(backtracePrt, nativeStack); + return nativeStack; + } else { + return new char(0); + } } ssize_t (*original_read)(int fd, void *buf, size_t count); ssize_t my_read(int fd, void *buf, size_t count) { ssize_t ret = original_read(fd, buf, count); - TrafficCollector::enQueueRx(MSG_TYPE_READ, fd, ret); + if (ret > 0) { + TrafficCollector::enQueueRx(MSG_TYPE_READ, fd, ret); + } return ret; } - ssize_t (*original_recv)(int sockfd, void *buf, size_t len, int flags); ssize_t my_recv(int sockfd, void *buf, size_t len, int flags) { ssize_t ret = original_recv(sockfd, buf, len, flags); - TrafficCollector::enQueueRx(MSG_TYPE_RECV, sockfd, ret); + if (ret > 0) { + TrafficCollector::enQueueRx(MSG_TYPE_RECV, sockfd, ret); + } return ret; } - ssize_t (*original_recvfrom)(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t my_recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) { ssize_t ret = original_recvfrom(sockfd, buf, len, flags, src_addr, addrlen); - TrafficCollector::enQueueRx(MSG_TYPE_RECVFROM, sockfd, ret); + if (ret > 0) { + TrafficCollector::enQueueRx(MSG_TYPE_RECVFROM, sockfd, ret); + } return ret; } - ssize_t (*original_recvmsg)(int sockfd, struct msghdr *msg, int flags); ssize_t my_recvmsg(int sockfd, struct msghdr *msg, int flags) { ssize_t ret = original_recvmsg(sockfd, msg, flags); - TrafficCollector::enQueueRx(MSG_TYPE_RECVMSG, sockfd, ret); + if (ret > 0) { + TrafficCollector::enQueueRx(MSG_TYPE_RECVMSG, sockfd, ret); + } return ret; } - ssize_t (*original_write)(int fd, const void *buf, size_t count); ssize_t my_write(int fd, const void *buf, size_t count) { ssize_t ret = original_write(fd, buf, count); - TrafficCollector::enQueueTx(MSG_TYPE_WRITE, fd, ret); + if (ret > 0) { + TrafficCollector::enQueueTx(MSG_TYPE_WRITE, fd, ret); + } return ret; } ssize_t (*original_send)(int sockfd, const void *buf, size_t len, int flags); ssize_t my_send(int sockfd, const void *buf, size_t len, int flags) { ssize_t ret = original_send(sockfd, buf, len, flags); - TrafficCollector::enQueueTx(MSG_TYPE_SEND, sockfd, ret); + if (ret > 0) { + TrafficCollector::enQueueTx(MSG_TYPE_SEND, sockfd, ret); + } return ret; } @@ -176,25 +182,42 @@ ssize_t (*original_sendto)(int sockfd, const void *buf, size_t len, int flags, ssize_t my_sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) { ssize_t ret = original_sendto(sockfd, buf, len, flags, dest_addr, addrlen); - TrafficCollector::enQueueTx(MSG_TYPE_SENDTO, sockfd, ret); + if (ret > 0) { + TrafficCollector::enQueueTx(MSG_TYPE_SENDTO, sockfd, ret); + } return ret; } ssize_t (*original_sendmsg)(int sockfd, const struct msghdr *msg, int flags); ssize_t my_sendmsg(int sockfd, const struct msghdr *msg, int flags) { ssize_t ret = original_sendmsg(sockfd, msg, flags); - TrafficCollector::enQueueTx(MSG_TYPE_SENDMSG, sockfd, ret); + if (ret > 0) { + TrafficCollector::enQueueTx(MSG_TYPE_SENDMSG, sockfd, ret); + } + return ret; +} + +int (*original_fclose)(FILE *stream); +int my_fclose(FILE *stream) { + int fd = fileno(stream); + int ret = original_fclose(stream); + if (ret == 0) { + TrafficCollector::enQueueClose(fd); + } return ret; } int (*original_close)(int fd); int my_close(int fd) { - TrafficCollector::enQueueClose(fd); - return original_close(fd); + int ret = original_close(fd); + if (ret == 0) { + TrafficCollector::enQueueClose(fd); + } + return ret; } static jobject nativeGetTrafficInfoMap(JNIEnv *env, jclass, jint type) { - return TrafficCollector::getTrafficInfoMap(type); + return TrafficCollector::getFdTrafficInfoMap(type); } static void nativeReleaseMatrixTraffic(JNIEnv *env, jclass) { @@ -202,42 +225,52 @@ static void nativeReleaseMatrixTraffic(JNIEnv *env, jclass) { TrafficCollector::clearTrafficInfo(); } +static jstring nativeGetNativeBackTraceByKey(JNIEnv *env, jclass, jstring key) { + const char* cKey = env->GetStringUTFChars(key, JNI_FALSE); + string keyString(cKey); + char* ret = getNativeBacktrace(keyString); + jstring jRet = env->NewStringUTF(ret); + delete[] ret; + return jRet; +} + static void nativeClearTrafficInfo(JNIEnv *env, jclass) { + backtraceMapLock.lock(); + backtraceMap.clear(); + backtraceMapLock.unlock(); TrafficCollector::clearTrafficInfo(); } -void setStackTrace(char* threadName) { +void setFdStackTraceCall(const char* key) { JNIEnv *env = JniInvocation::getEnv(); if (!env) return; - - jstring jThreadName = env->NewStringUTF(threadName); - jstring nativeBacktrace; + jstring jKey = env->NewStringUTF(key); if (sDumpNativeBackTrace) { - nativeBacktrace = env->NewStringUTF(getNativeBacktrace()); - } else { - nativeBacktrace = env->NewStringUTF(""); + saveNativeBackTrace(key); } - - env->CallStaticVoidMethod(gJ.TrafficPlugin, gJ.TrafficPlugin_setFdStackTrace, jThreadName, nativeBacktrace); - env->DeleteLocalRef(jThreadName); - env->DeleteLocalRef(nativeBacktrace); + env->CallStaticVoidMethod(gJ.TrafficPlugin, gJ.TrafficPlugin_setFdStackTrace, jKey); + env->DeleteLocalRef(jKey); } -static void hookSocket(bool rxHook, bool txHook) { +static void hookSocket(bool rxHook, bool txHook, bool willHookAllSoReadWrite) { if (HOOKED) { return; } - xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "connect", - (void *) my_connect, (void **) (&original_connect)); - xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "close", (void *) my_close, (void **) (&original_close)); + xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "fclose", + (void *) my_fclose, (void **) (&original_fclose)); if (rxHook) { - xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "read", - (void *) my_read, (void **) (&original_read)); + if (willHookAllSoReadWrite) { + xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "read", + (void *) my_read, (void **) (&original_read)); + } else { + xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, "/data/.*\\.so$", "read", + (void *) my_read, (void **) (&original_read)); + } xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "recv", (void *) my_recv, (void **) (&original_recv)); xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "recvfrom", @@ -247,8 +280,13 @@ static void hookSocket(bool rxHook, bool txHook) { } if (txHook) { - xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "write", - (void *) my_write, (void **) (&original_write)); + if (willHookAllSoReadWrite) { + xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "write", + (void *) my_write, (void **) (&original_write)); + } else { + xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, "/data/.*\\.so$", "write", + (void *) my_write, (void **) (&original_write)); + } xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "send", (void *) my_send, (void **) (&original_send)); xhook_grouped_register(HOOK_REQUEST_GROUPID_TRAFFIC, ".*\\.so$", "sendto", @@ -257,18 +295,30 @@ static void hookSocket(bool rxHook, bool txHook) { (void *) my_sendmsg, (void **) (&original_sendmsg)); } + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", "read"); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", "recv"); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", "recvfrom"); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", "recvmsg"); + + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", "write"); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", "send"); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", "sendto"); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", "sendmsg"); + + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, "/vendor/lib.*", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libinput\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libmeminfo\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libgui\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libsensor\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libutils\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libcutils\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libtrace-canary\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libgsl\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libadbconnection_client\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libadbconnection\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libandroid_runtime\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libnetd_client\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libstatssocket\\.so$", nullptr); - xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libc\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libprofile\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libbinder\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libGLES_mali\\.so$", nullptr); @@ -277,7 +327,16 @@ static void hookSocket(bool rxHook, bool txHook) { xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libsqlite\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libbase\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libartbase\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libart\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libtombstoned_client\\.so$", nullptr); xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*liblog\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libcodec2_vndk\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libandroidfw\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libaudioclient\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libjavacrypto\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libwechatbacktrace\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libmatrix-memoryhook\\.so$", nullptr); + xhook_grouped_ignore(HOOK_REQUEST_GROUPID_TRAFFIC, ".*libmatrix-traffic\\.so$", nullptr); xhook_refresh(true); HOOKED = true; @@ -291,11 +350,11 @@ static void ignoreSo(JNIEnv *env, jobjectArray ignoreSoFiles) { } } -static void nativeInitMatrixTraffic(JNIEnv *env, jclass, jboolean rxEnable, jboolean txEnable, jboolean dumpStackTrace, jboolean dumpNativeBackTrace, jboolean lookupIpAddress, jobjectArray ignoreSoFiles) { - TrafficCollector::startLoop(dumpStackTrace == JNI_TRUE, lookupIpAddress == JNI_TRUE); +static void nativeInitMatrixTraffic(JNIEnv *env, jclass, jboolean rxEnable, jboolean txEnable, jboolean dumpStackTrace, jboolean dumpNativeBackTrace, jboolean willHookAllSoReadWrite, jobjectArray ignoreSoFiles) { + TrafficCollector::startLoop(dumpStackTrace == JNI_TRUE); sDumpNativeBackTrace = (dumpNativeBackTrace == JNI_TRUE); ignoreSo(env, ignoreSoFiles); - hookSocket(rxEnable == JNI_TRUE, txEnable == JNI_TRUE); + hookSocket(rxEnable == JNI_TRUE, txEnable == JNI_TRUE, willHookAllSoReadWrite == JNI_TRUE); } template @@ -306,6 +365,7 @@ static const JNINativeMethod TRAFFIC_METHODS[] = { {"nativeGetTrafficInfoMap", "(I)Ljava/util/HashMap;", (void *) nativeGetTrafficInfoMap}, {"nativeClearTrafficInfo", "()V", (void *) nativeClearTrafficInfo}, {"nativeReleaseMatrixTraffic", "()V", (void *) nativeReleaseMatrixTraffic}, + {"nativeGetNativeBackTraceByKey", "(Ljava/lang/String;)Ljava/lang/String;", (void *) nativeGetNativeBackTraceByKey}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { @@ -319,8 +379,9 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { if (!trafficCollectorCls) return -1; gJ.TrafficPlugin = static_cast(env->NewGlobalRef(trafficCollectorCls)); + gJ.TrafficPlugin_setFdStackTrace = - env->GetStaticMethodID(trafficCollectorCls, "setStackTrace", "(Ljava/lang/String;Ljava/lang/String;)V"); + env->GetStaticMethodID(trafficCollectorCls, "setFdStackTrace", "(Ljava/lang/String;)V"); if (env->RegisterNatives( trafficCollectorCls, TRAFFIC_METHODS, static_cast(NELEM(TRAFFIC_METHODS))) != 0) @@ -328,4 +389,4 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { env->DeleteLocalRef(trafficCollectorCls); return JNI_VERSION_1_6; -} // namespace MatrixTraffic +} // namespace MatrixTraffic \ No newline at end of file diff --git a/matrix/matrix-android/matrix-traffic/src/main/cpp/MatrixTraffic.h b/matrix/matrix-android/matrix-traffic/src/main/cpp/MatrixTraffic.h index dd83748c2..c87fc2c7a 100644 --- a/matrix/matrix-android/matrix-traffic/src/main/cpp/MatrixTraffic.h +++ b/matrix/matrix-android/matrix-traffic/src/main/cpp/MatrixTraffic.h @@ -21,6 +21,8 @@ #ifndef MATRIX_ANDROID_MATRIXTRAFFIC_H #define MATRIX_ANDROID_MATRIXTRAFFIC_H -void setStackTrace(char* threadName); +void setFdStackTraceCall(const char* key); +void clearFdInfoCall(int fd); +void printLog(const char* log); #endif //MATRIX_ANDROID_MATRIXTRAFFIC_H diff --git a/matrix/matrix-android/matrix-traffic/src/main/cpp/TrafficCollector.cc b/matrix/matrix-android/matrix-traffic/src/main/cpp/TrafficCollector.cc index d26d8a9b0..c691746c6 100644 --- a/matrix/matrix-android/matrix-traffic/src/main/cpp/TrafficCollector.cc +++ b/matrix/matrix-android/matrix-traffic/src/main/cpp/TrafficCollector.cc @@ -29,124 +29,81 @@ #include #include "MatrixTraffic.h" +#include #include #include +#include using namespace std; namespace MatrixTraffic { static mutex queueMutex; -static lock_guard lock(queueMutex); +static condition_variable cv; + static bool loopRunning = false; static bool sDumpStackTrace = false; -static bool sLookupIpAddress = false; -static map fdFamilyMap; + +static unordered_set activeFdSet; +static shared_mutex activeFdSetMutex; + +static unordered_set invalidFdSet; + static blocking_queue> msgQueue; -static map rxTrafficInfoMap; -static map txTrafficInfoMap; -static mutex rxTrafficInfoMapLock; -static mutex txTrafficInfoMapLock; + +static map rxFdTrafficInfoMap; +static map txFdTrafficInfoMap; +static shared_mutex rxTrafficInfoMapLock; +static shared_mutex txTrafficInfoMapLock; static map fdThreadNameMap; -static map fdIpAddressMap; -static mutex fdThreadNameMapLock; -static mutex fdIpAddressMapLock; - -string getIpAddressFromAddr(sockaddr* _addr) { - string ipAddress; - if (_addr != nullptr) { - if ((int)_addr->sa_family == AF_LOCAL) { - ipAddress = _addr->sa_data; - ipAddress.append(":LOCAL"); - } else if ((int)_addr->sa_family == AF_INET) { - auto *sin4 = reinterpret_cast(_addr); - char ipv4str[INET_ADDRSTRLEN]; - if (inet_ntop(AF_INET, &(sin4->sin_addr), ipv4str, INET6_ADDRSTRLEN) != nullptr) { - ipAddress = ipv4str; - int port = ntohs(sin4->sin_port); - if (port != -1) { - ipAddress.append(":"); - ipAddress.append(to_string(port)); - } - } - } else if ((int)_addr->sa_family == AF_INET6) { - auto *sin6 = reinterpret_cast(_addr); - char ipv6str[INET6_ADDRSTRLEN]; - if (inet_ntop(AF_INET6, &(sin6->sin6_addr), ipv6str, INET6_ADDRSTRLEN) != nullptr) { - ipAddress = ipv6str; - int port = ntohs(sin6->sin6_port); - if (port != -1) { - ipAddress.append(":"); - ipAddress.append(to_string(port)); - } - } +static shared_mutex fdThreadNameMapSharedLock; + +int isNetworkSocketFd(int fd) { + struct sockaddr c; + socklen_t cLen = sizeof(c); + int getSockNameRet = getsockname(fd, (struct sockaddr*) &c, &cLen); + if (getSockNameRet == 0) { + if (c.sa_family != AF_LOCAL && c.sa_family != AF_NETLINK) { + return 1; } } - return ipAddress; -} - -void saveIpAddress(int fd, sockaddr* addr) { - fdIpAddressMapLock.lock(); - if (fdIpAddressMap.count(fd) == 0) { - fdIpAddressMap[fd] = getIpAddressFromAddr(addr); - } - fdIpAddressMapLock.unlock(); + return 0; } -string getIpAddress(int fd) { - fdIpAddressMapLock.lock(); - string ipAddress = fdIpAddressMap[fd]; - fdIpAddressMapLock.unlock(); - return ipAddress; -} - - -string getKeyAndSaveStack(int fd) { - fdThreadNameMapLock.lock(); +string saveFdInfo(int fd) { + fdThreadNameMapSharedLock.lock_shared(); if (fdThreadNameMap.count(fd) == 0) { - string key; - if (sLookupIpAddress) { - key = getIpAddress(fd); - key.append(";"); - } + fdThreadNameMapSharedLock.unlock_shared(); - auto threadName = new char[15]; + char threadName[15]; prctl(PR_GET_NAME, threadName); - key.append(threadName); - fdThreadNameMap[fd] = key; - fdThreadNameMapLock.unlock(); + char key[32]; + snprintf(key, sizeof(key), "%d-%s", fd, threadName); + if (sDumpStackTrace) { - setStackTrace(key.data()); + setFdStackTraceCall(key); } + + fdThreadNameMapSharedLock.lock(); + fdThreadNameMap[fd] = key; + fdThreadNameMapSharedLock.unlock(); + return key; } else { auto key = fdThreadNameMap[fd]; - fdThreadNameMapLock.unlock(); + fdThreadNameMapSharedLock.unlock_shared(); return key; } } -void TrafficCollector::enQueueConnect(int fd, sockaddr *addr, socklen_t addr_length) { - if (!loopRunning) { - return; - } - if (sLookupIpAddress) { - saveIpAddress(fd, addr); - } - shared_ptr msg = make_shared(MSG_TYPE_CONNECT, fd, addr->sa_family, getKeyAndSaveStack(fd), 0); - - msgQueue.push(msg); - queueMutex.unlock(); -} - void TrafficCollector::enQueueClose(int fd) { if (!loopRunning) { return; } - shared_ptr msg = make_shared(MSG_TYPE_CLOSE, fd, 0, "", 0); + shared_ptr msg = make_shared(MSG_TYPE_CLOSE, fd, 0); msgQueue.push(msg); - queueMutex.unlock(); + cv.notify_one(); } void enQueueMsg(int type, int fd, size_t len) { @@ -154,9 +111,18 @@ void enQueueMsg(int type, int fd, size_t len) { return; } - shared_ptr msg = make_shared(type, fd, 0, getKeyAndSaveStack(fd), len); + activeFdSetMutex.lock_shared(); + if (activeFdSet.count(fd) > 0) { + activeFdSetMutex.unlock_shared(); + saveFdInfo(fd); + } else { + activeFdSetMutex.unlock_shared(); + } + + shared_ptr msg = make_shared(type, fd, len); msgQueue.push(msg); - queueMutex.unlock(); + + cv.notify_one(); } void TrafficCollector::enQueueTx(int type, int fd, size_t len) { @@ -173,58 +139,72 @@ void TrafficCollector::enQueueRx(int type, int fd, size_t len) { enQueueMsg(type, fd, len); } -void appendRxTraffic(const string& threadName, long len) { +void appendRxTraffic(int fd, long len) { rxTrafficInfoMapLock.lock(); - rxTrafficInfoMap[threadName] += len; + rxFdTrafficInfoMap[fd] += len; rxTrafficInfoMapLock.unlock(); } -void appendTxTraffic(const string& threadName, long len) { +void appendTxTraffic(int fd, long len) { txTrafficInfoMapLock.lock(); - txTrafficInfoMap[threadName] += len; + txFdTrafficInfoMap[fd] += len; txTrafficInfoMapLock.unlock(); } void loop() { + unique_lock lk(queueMutex); while (loopRunning) { if (msgQueue.empty()) { - queueMutex.lock(); + cv.wait(lk); } else { shared_ptr msg = msgQueue.front(); - if (msg->type == MSG_TYPE_CONNECT) { - fdFamilyMap[msg->fd] = msg->sa_family; - } else if (msg->type == MSG_TYPE_READ) { - if (fdFamilyMap.count(msg->fd) > 0) { - appendRxTraffic(msg->threadName, msg->len); - } - } else if (msg->type >= MSG_TYPE_RECV && msg->type <= MSG_TYPE_RECVMSG) { - if (fdFamilyMap[msg->fd] != AF_LOCAL) { - appendRxTraffic(msg->threadName, msg->len); + int fd = msg->fd; + int type = msg->type; + if (type == MSG_TYPE_CLOSE) { + if (activeFdSet.count(fd) > 0) { + fdThreadNameMapSharedLock.lock(); + fdThreadNameMap.erase(fd); + fdThreadNameMapSharedLock.unlock(); + + activeFdSetMutex.lock(); + activeFdSet.erase(fd); + activeFdSetMutex.unlock(); } - } else if (msg->type == MSG_TYPE_WRITE) { - if (fdFamilyMap.count(msg->fd) > 0) { - appendTxTraffic(msg->threadName, msg->len); + invalidFdSet.erase(fd); + } else { + if (activeFdSet.count(fd) == 0 && invalidFdSet.count(fd) == 0) { + if (!isNetworkSocketFd(fd)) { + invalidFdSet.insert(fd); + fdThreadNameMapSharedLock.lock(); + fdThreadNameMap.erase(fd); + fdThreadNameMapSharedLock.unlock(); + } else { + activeFdSetMutex.lock(); + activeFdSet.insert(fd); + activeFdSetMutex.unlock(); + } } - } else if (msg->type >= MSG_TYPE_SEND && msg->type <= MSG_TYPE_SENDMSG) { - if (fdFamilyMap[msg->fd] != AF_LOCAL) { - appendTxTraffic(msg->threadName, msg->len); + if (type >= MSG_TYPE_READ && type <= MSG_TYPE_RECVMSG) { + if (activeFdSet.count(fd) > 0 && invalidFdSet.count(fd) == 0) { + appendRxTraffic(fd, msg->len); + } + } else if (type >= MSG_TYPE_WRITE && type <= MSG_TYPE_SENDMSG) { + if (activeFdSet.count(fd) > 0 && invalidFdSet.count(fd) == 0) { + appendTxTraffic(fd, msg->len); + } } - } else if (msg->type == MSG_TYPE_CLOSE) { - fdThreadNameMapLock.lock(); - fdThreadNameMap.erase(msg->fd); - fdThreadNameMapLock.unlock(); - fdFamilyMap.erase(msg->fd); } msgQueue.pop(); } } } - -void TrafficCollector::startLoop(bool dumpStackTrace, bool lookupIpAddress) { - sDumpStackTrace = dumpStackTrace; - sLookupIpAddress = lookupIpAddress; +void TrafficCollector::startLoop(bool dumpStackTrace) { + if (loopRunning) { + return; + } loopRunning = true; + sDumpStackTrace = dumpStackTrace; thread loopThread(loop); loopThread.detach(); } @@ -233,7 +213,7 @@ void TrafficCollector::stopLoop() { loopRunning = false; } -jobject TrafficCollector::getTrafficInfoMap(int type) { +jobject TrafficCollector::getFdTrafficInfoMap(int type) { JNIEnv *env = JniInvocation::getEnv(); jclass mapClass = env->FindClass("java/util/HashMap"); if(mapClass == nullptr) { @@ -245,25 +225,54 @@ jobject TrafficCollector::getTrafficInfoMap(int type) { "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); if (type == TYPE_GET_TRAFFIC_RX) { - rxTrafficInfoMapLock.lock(); - for (auto & it : rxTrafficInfoMap) { - jstring threadName = env->NewStringUTF(it.first.c_str()); + rxTrafficInfoMapLock.lock_shared(); + for (auto & it : rxFdTrafficInfoMap) { + int fd = it.first; + if (fdThreadNameMap.count(fd) == 0) { + continue; + } + fdThreadNameMapSharedLock.lock_shared(); + const char* key = fdThreadNameMap[fd].c_str(); + fdThreadNameMapSharedLock.unlock_shared(); + if (key == nullptr || strlen(key) == 0) { + continue; + } + + if (it.second <= 0) { + continue; + } + jstring jKey = env->NewStringUTF(key); jstring traffic = env->NewStringUTF(to_string(it.second).c_str()); - env->CallObjectMethod(jHashMap, mapPut, threadName, traffic); - env->DeleteLocalRef(threadName); + env->CallObjectMethod(jHashMap, mapPut, jKey, traffic); + env->DeleteLocalRef(jKey); env->DeleteLocalRef(traffic); + } - rxTrafficInfoMapLock.unlock(); + rxTrafficInfoMapLock.unlock_shared(); } else if (type == TYPE_GET_TRAFFIC_TX) { - txTrafficInfoMapLock.lock(); - for (auto & it : txTrafficInfoMap) { - jstring threadName = env->NewStringUTF(it.first.c_str()); + txTrafficInfoMapLock.lock_shared(); + for (auto & it : txFdTrafficInfoMap) { + int fd = it.first; + if (fdThreadNameMap.count(fd) == 0) { + continue; + } + fdThreadNameMapSharedLock.lock_shared(); + const char* key = fdThreadNameMap[fd].c_str(); + fdThreadNameMapSharedLock.unlock_shared(); + if (key == nullptr || strlen(key) == 0) { + continue; + } + + if (it.second <= 0) { + continue; + } + jstring jKey = env->NewStringUTF(key); jstring traffic = env->NewStringUTF(to_string(it.second).c_str()); - env->CallObjectMethod(jHashMap, mapPut, threadName, traffic); - env->DeleteLocalRef(threadName); + env->CallObjectMethod(jHashMap, mapPut, jKey, traffic); + env->DeleteLocalRef(jKey); env->DeleteLocalRef(traffic); } - txTrafficInfoMapLock.unlock(); + txTrafficInfoMapLock.unlock_shared(); } env->DeleteLocalRef(mapClass); return jHashMap; @@ -271,16 +280,18 @@ jobject TrafficCollector::getTrafficInfoMap(int type) { void TrafficCollector::clearTrafficInfo() { rxTrafficInfoMapLock.lock(); - rxTrafficInfoMap.clear(); + rxFdTrafficInfoMap.clear(); rxTrafficInfoMapLock.unlock(); txTrafficInfoMapLock.lock(); - txTrafficInfoMap.clear(); + txFdTrafficInfoMap.clear(); txTrafficInfoMapLock.unlock(); - fdThreadNameMapLock.lock(); + fdThreadNameMapSharedLock.lock(); fdThreadNameMap.clear(); - fdThreadNameMapLock.unlock(); + fdThreadNameMapSharedLock.unlock(); + + } TrafficCollector::~TrafficCollector() { diff --git a/matrix/matrix-android/matrix-traffic/src/main/cpp/TrafficCollector.h b/matrix/matrix-android/matrix-traffic/src/main/cpp/TrafficCollector.h index 276cf0572..e94dd7797 100644 --- a/matrix/matrix-android/matrix-traffic/src/main/cpp/TrafficCollector.h +++ b/matrix/matrix-android/matrix-traffic/src/main/cpp/TrafficCollector.h @@ -31,8 +31,6 @@ #include #include -#define MSG_TYPE_CONNECT 1 - #define MSG_TYPE_READ 10 #define MSG_TYPE_RECV 11 #define MSG_TYPE_RECVFROM 12 @@ -53,25 +51,23 @@ using namespace std; namespace MatrixTraffic { class TrafficMsg { public: - TrafficMsg(const int _type, const int _fd, sa_family_t _sa_family, string _threadName, long _len) - : type(_type), fd(_fd), sa_family(_sa_family), threadName(_threadName), len(_len) { + TrafficMsg(const int _type, const int _fd, long _len) + : type(_type), fd(_fd), len(_len) { } int type; int fd; - sa_family_t sa_family; - string threadName; +// sa_family_t sa_family; +// string threadName; long len; }; class TrafficCollector { public : - static void startLoop(bool dumpStackTrace, bool lookupIpAddress); + static void startLoop(bool dumpStackTrace); static void stopLoop(); - static void enQueueConnect(int fd, sockaddr *addr, socklen_t __addr_length); - static void enQueueClose(int fd); static void enQueueTx(int type, int fd, size_t len); @@ -80,7 +76,7 @@ public : static void clearTrafficInfo(); - static jobject getTrafficInfoMap(int type); + static jobject getFdTrafficInfoMap(int type); virtual ~TrafficCollector(); }; diff --git a/matrix/matrix-android/matrix-traffic/src/main/java/com/tencent/matrix/traffic/TrafficConfig.java b/matrix/matrix-android/matrix-traffic/src/main/java/com/tencent/matrix/traffic/TrafficConfig.java index fcf375d7c..5f5c5960f 100644 --- a/matrix/matrix-android/matrix-traffic/src/main/java/com/tencent/matrix/traffic/TrafficConfig.java +++ b/matrix/matrix-android/matrix-traffic/src/main/java/com/tencent/matrix/traffic/TrafficConfig.java @@ -4,11 +4,18 @@ import java.util.List; public class TrafficConfig { + public static final int STACK_TRACE_FILTER_MODE_FULL = 0; + public static final int STACK_TRACE_FILTER_MODE_STARTS_WITH = 1; + public static final int STACK_TRACE_FILTER_MODE_PATTERN = 2; private boolean rxCollectorEnable; private boolean txCollectorEnable; private boolean dumpStackTraceEnable; private boolean dumpNativeBackTraceEnable; - private boolean lookupIpAddressEnable; + private boolean hookAllSoReadWrite = true; + //TODO + //private boolean lookupIpAddressEnable; + private int stackTraceFilterMode = 0; + private String stackTraceFilterCore; private List ignoreSoList = new ArrayList<>(); public TrafficConfig() { @@ -20,15 +27,13 @@ public TrafficConfig(boolean rxCollectorEnable, boolean txCollectorEnable, boole this.txCollectorEnable = txCollectorEnable; this.dumpStackTraceEnable = dumpStackTraceEnable; this.dumpNativeBackTraceEnable = false; - this.lookupIpAddressEnable = false; } - public TrafficConfig(boolean rxCollectorEnable, boolean txCollectorEnable, boolean dumpStackTraceEnable, boolean dumpNativeBackTraceEnable, boolean lookupIpAddressEnable) { + public TrafficConfig(boolean rxCollectorEnable, boolean txCollectorEnable, boolean dumpStackTraceEnable, boolean dumpNativeBackTraceEnable) { this.rxCollectorEnable = rxCollectorEnable; this.txCollectorEnable = txCollectorEnable; this.dumpStackTraceEnable = dumpStackTraceEnable; this.dumpNativeBackTraceEnable = dumpNativeBackTraceEnable; - this.lookupIpAddressEnable = lookupIpAddressEnable; } public boolean isRxCollectorEnable() { @@ -61,14 +66,6 @@ public void setDumpNativeBackTrace(boolean dumpNativeBackTraceEnable) { this.dumpNativeBackTraceEnable = dumpNativeBackTraceEnable; } - public boolean willLookupIpAddress() { - return lookupIpAddressEnable; - } - - public void setLookupIpAddressEnable(boolean lookupIpAddressEnable) { - this.lookupIpAddressEnable = lookupIpAddressEnable; - } - public void addIgnoreSoFile(String soName) { ignoreSoList.add(soName); } @@ -76,4 +73,26 @@ public void addIgnoreSoFile(String soName) { public String[] getIgnoreSoFiles() { return ignoreSoList.toArray(new String[ignoreSoList.size()]); } + + public void setStackTraceFilterMode(int mode, String filterCore) { + this.stackTraceFilterMode = mode; + this.stackTraceFilterCore = filterCore; + } + + public int getStackTraceFilterMode() { + return this.stackTraceFilterMode; + } + + public String getStackTraceFilterCore() { + return this.stackTraceFilterCore; + } + + public boolean willHookAllSoReadWrite() { + return hookAllSoReadWrite; + } + + public void setHookAllSoReadWrite(boolean hookAllSoReadWrite) { + this.hookAllSoReadWrite = hookAllSoReadWrite; + } + } diff --git a/matrix/matrix-android/matrix-traffic/src/main/java/com/tencent/matrix/traffic/TrafficPlugin.java b/matrix/matrix-android/matrix-traffic/src/main/java/com/tencent/matrix/traffic/TrafficPlugin.java index c6926159f..9791731ef 100644 --- a/matrix/matrix-android/matrix-traffic/src/main/java/com/tencent/matrix/traffic/TrafficPlugin.java +++ b/matrix/matrix-android/matrix-traffic/src/main/java/com/tencent/matrix/traffic/TrafficPlugin.java @@ -4,8 +4,10 @@ import com.tencent.matrix.plugin.Plugin; import com.tencent.matrix.util.MatrixLog; +import com.tencent.matrix.util.MatrixUtil; import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class TrafficPlugin extends Plugin { @@ -15,9 +17,12 @@ public class TrafficPlugin extends Plugin { public static final int TYPE_GET_TRAFFIC_RX = 0; public static final int TYPE_GET_TRAFFIC_TX = 1; - private static final ConcurrentHashMap stackTraceMap = new ConcurrentHashMap<>(); + private static int stackTraceFilterMode = 0; + private static String stackTraceFilterCore = ""; + private static final Map hashStackTraceMap = new ConcurrentHashMap<>(); + private static final Map keyHashMap = new ConcurrentHashMap<>(); - //TODO will be done in next upgrade + //TODO //public static final int TYPE_GET_TRAFFIC_ALL = 2; static { @@ -36,7 +41,9 @@ public void start() { super.start(); MatrixLog.i(TAG, "start"); String[] ignoreSoFiles = trafficConfig.getIgnoreSoFiles(); - nativeInitMatrixTraffic(trafficConfig.isRxCollectorEnable(), trafficConfig.isTxCollectorEnable(), trafficConfig.willDumpStackTrace(), trafficConfig.willDumpNativeBackTrace(), trafficConfig.willLookupIpAddress(), ignoreSoFiles); + stackTraceFilterMode = trafficConfig.getStackTraceFilterMode(); + stackTraceFilterCore = trafficConfig.getStackTraceFilterCore(); + nativeInitMatrixTraffic(trafficConfig.isRxCollectorEnable(), trafficConfig.isTxCollectorEnable(), trafficConfig.willDumpStackTrace(), trafficConfig.willDumpNativeBackTrace(), trafficConfig.willHookAllSoReadWrite(), ignoreSoFiles); } @@ -54,35 +61,68 @@ public HashMap getTrafficInfoMap(int type) { return nativeGetTrafficInfoMap(type); } - public ConcurrentHashMap getStackTraceMap() { - return stackTraceMap; + public String getStackTraceByMd5(String md5) { + return hashStackTraceMap.get(md5); + } + + public String getJavaStackTraceByKey(String key) { + if (!trafficConfig.willDumpStackTrace()) { + return ""; + } + String md5 = keyHashMap.get(key); + if (md5 == null || md5.isEmpty()) { + return ""; + } + return hashStackTraceMap.get(md5); + } + public String getNativeBackTraceByKey(String key) { + if (!trafficConfig.willDumpNativeBackTrace()) { + return ""; + } + return nativeGetNativeBackTraceByKey(key); } public void clearTrafficInfo() { - stackTraceMap.clear(); + keyHashMap.clear(); + hashStackTraceMap.clear(); nativeClearTrafficInfo(); } - private static native void nativeInitMatrixTraffic(boolean rxEnable, boolean txEnable, boolean dumpStackTrace, boolean dumpNativeBackTrace, boolean lookupIpAddress, String[] ignoreSoFiles); + private static native void nativeInitMatrixTraffic(boolean rxEnable, boolean txEnable, boolean dumpStackTrace, boolean dumpNativeBackTrace, boolean willHookAllSoReadWrite, String[] ignoreSoFiles); private static native String nativeGetTrafficInfo(); private static native String nativeGetAllStackTraceTrafficInfo(); private static native void nativeReleaseMatrixTraffic(); private static native void nativeClearTrafficInfo(); private static native HashMap nativeGetTrafficInfoMap(int type); + private static native String nativeGetNativeBackTraceByKey(String key); @Keep - private static void setStackTrace(String threadName, String nativeBackTrace) { - MatrixLog.i(TAG, "setStackTrace, threadName = " + threadName, "nativeBackTrace = " + nativeBackTrace); - StringBuilder stackTrace = new StringBuilder(nativeBackTrace); + private static void setFdStackTrace(String key) { + StringBuilder stackTrace = new StringBuilder(); StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - for (int line = 3; line < stackTraceElements.length; line++) { - stackTrace.append(stackTraceElements[line]).append("\n"); - } - - stackTraceMap.put(threadName, stackTrace.toString()); - } + for (int line = 0; line < stackTraceElements.length; line++) { + String stackTraceLine = stackTraceElements[line].toString(); + boolean willAppend = false; + if (stackTraceFilterMode == TrafficConfig.STACK_TRACE_FILTER_MODE_FULL) { + willAppend = true; + } else if (stackTraceFilterMode == TrafficConfig.STACK_TRACE_FILTER_MODE_STARTS_WITH) { + if (stackTraceLine.startsWith(stackTraceFilterCore)) { + willAppend = true; + } + } else if (stackTraceFilterMode == TrafficConfig.STACK_TRACE_FILTER_MODE_PATTERN) { + if (stackTraceLine.matches(stackTraceFilterCore)) { + willAppend = true; + } + } + if (willAppend) { + stackTrace.append(stackTraceLine).append("\n"); + } - public void clearStackTrace() { - stackTraceMap.clear(); + } + String md5 = MatrixUtil.getMD5String(stackTrace.toString()); + if (!hashStackTraceMap.containsKey(md5)) { + hashStackTraceMap.put(md5, stackTrace.toString()); + } + keyHashMap.put(key, md5); } } diff --git a/matrix/matrix-android/settings.gradle b/matrix/matrix-android/settings.gradle index e86b26a33..9303c91b3 100644 --- a/matrix/matrix-android/settings.gradle +++ b/matrix/matrix-android/settings.gradle @@ -18,21 +18,23 @@ include ':matrix-sqlite-lint:matrix-sqlite-lint-android-sdk' include ':matrix-battery-canary' include ':matrix-arscutil' include ':matrix-opengl-leak' -include ':matrix-memory-dump' include ':matrix-traffic' // Components for memory hook include ':matrix-backtrace' include ':matrix-hooks' -include ':matrix-jectl' +include ':matrix-memguard' +include ':matrix-mallctl' include ':matrix-fd' -// Benchmark -include ':test:matrix-backtrace-benchmark' - -// TEST -include ':test:test-backtrace' -include ':test:test-memoryhook' +//// Benchmark +//include ':test:matrix-backtrace-benchmark' +// +//// TEST +//include ':test:test-backtrace' +//include ':test:test-memoryhook' +//include ':test:test-openglhook' +include ':matrix-memory-canary' if (gradle.staticLinkCXX()) { include ':matrix-hooks:cxx-static' diff --git a/matrix/matrix-android/test/test-openglhook/CMakeLists.txt b/matrix/matrix-android/test/test-openglhook/CMakeLists.txt new file mode 100644 index 000000000..456465168 --- /dev/null +++ b/matrix/matrix-android/test/test-openglhook/CMakeLists.txt @@ -0,0 +1,80 @@ +# Sets the minimum version of CMake required to build your native library. +# This ensures that a certain set of CMake features is available to +# your build. + +CMAKE_MINIMUM_REQUIRED(VERSION 3.4.1) + +# Specifies a library name, specifies whether the library is STATIC or +# SHARED, and provides relative paths to the source code. You can +# define multiple libraries by adding multiple add.library() commands, +# and CMake builds them for you. When you build your app, Gradle +# automatically packages shared libraries with your APK. + +SET(TARGET test-openglhook) + +SET(SOURCE_DIR src/main/cpp) + +OPTION(EnableLOG "Enable QUT Logs" ON) +IF(EnableLOG) + ADD_DEFINITIONS(-DEnableLOG) +ENDIF() + +SET( + SOURCE_FILES + ${SOURCE_DIR}/test/OpenglHookTest.cpp +) + +ADD_LIBRARY( # Specifies the name of the library. + ${TARGET} + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + ${SOURCE_FILES} + ) + +TARGET_INCLUDE_DIRECTORIES( + ${TARGET} + PUBLIC ${SOURCE_DIR}/test + PRIVATE ${EXT_DEP}/include + PRIVATE ${EXT_DEP}/include/backtrace + PRIVATE ${EXT_DEP}/include/backtrace/common +) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +FIND_LIBRARY( # Sets the name of the path variable. + log-lib + log +) + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in this +# build script, prebuilt third-party libraries, or system libraries. + +TARGET_LINK_LIBRARIES( # Specifies the target library. + ${TARGET} + # Links the target library to the log library + # included in the NDK. + PRIVATE ${log-lib} + GLESv2 + GLESv3 + EGL + PRIVATE -Wl,--gc-sections + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libwechatbacktrace.so + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libmatrix-hookcommon.so + PRIVATE ${EXT_DEP}/lib/${ANDROID_ABI}/libmatrix-opengl-leak.so +) + +TARGET_COMPILE_OPTIONS( + ${TARGET} + PRIVATE $<$:-std=c99 -O0> + PRIVATE $<$:-std=c++17 -fno-exceptions -frtti -O0> + PRIVATE -fstack-protector +) + diff --git a/matrix/matrix-android/test/test-openglhook/build.gradle b/matrix/matrix-android/test/test-openglhook/build.gradle new file mode 100644 index 000000000..ae2d5a0ef --- /dev/null +++ b/matrix/matrix-android/test/test-openglhook/build.gradle @@ -0,0 +1,80 @@ +apply plugin: 'com.android.application' + +apply from: rootProject.file('gradle/WeChatNativeDepend.gradle') + +android { + compileSdkVersion rootProject.compileSdkVersion + defaultConfig { + minSdkVersion rootProject.MIN_SDK_VERSION_FOR_HOOK + targetSdkVersion rootProject.targetSdkVersion + versionCode rootProject.VERSION_CODE + versionName rootProject.VERSION_NAME + + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + consumerProguardFiles "consumer-rules.pro" + + ndk { + abiFilters rootProject.ABI_FILTERS as String[] + } + } + + signingConfigs { + release { + keyAlias signingConfigs.debug.keyAlias + keyPassword signingConfigs.debug.keyPassword + storeFile signingConfigs.debug.storeFile + storePassword signingConfigs.debug.storePassword + v2SigningEnabled false + } + } + + buildTypes { + debug { + debuggable true + signingConfig signingConfigs.debug + minifyEnabled false + } + release { + signingConfig signingConfigs.release + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + main { + jniLibs { + setSrcDirs (['./libs'] as Set) + } + } + + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + packagingOptions { + pickFirst 'lib/armeabi-v7a/libc++_shared.so' + pickFirst 'lib/arm64-v8a/libc++_shared.so' + pickFirst 'lib/armeabi-v7a/libwechatbacktrace.so' + pickFirst 'lib/arm64-v8a/libwechatbacktrace.so' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.0' + implementation project(path: ':matrix-backtrace') + implementation project(path: ':matrix-opengl-leak') + implementation project(path: ':matrix-android-lib') + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + + implementation project(':matrix-hooks') +} \ No newline at end of file diff --git a/matrix/matrix-android/test/test-openglhook/src/androidTest/java/com/example/test_openglleak/ExampleInstrumentedTest.java b/matrix/matrix-android/test/test-openglhook/src/androidTest/java/com/example/test_openglleak/ExampleInstrumentedTest.java new file mode 100644 index 000000000..b8901e364 --- /dev/null +++ b/matrix/matrix-android/test/test-openglhook/src/androidTest/java/com/example/test_openglleak/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.test_openglleak; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.test_openglleak", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/test/test-openglhook/src/main/AndroidManifest.xml b/matrix/matrix-android/test/test-openglhook/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e0038504a --- /dev/null +++ b/matrix/matrix-android/test/test-openglhook/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matrix/matrix-android/test/test-openglhook/src/main/cpp/test/OpenglHookTest.cpp b/matrix/matrix-android/test/test-openglhook/src/main/cpp/test/OpenglHookTest.cpp new file mode 100644 index 000000000..42367478e --- /dev/null +++ b/matrix/matrix-android/test/test-openglhook/src/main/cpp/test/OpenglHookTest.cpp @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#include +#include +#include + +#define TAG "matrix.openglHook" + +int egl_init() { + EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (eglDisplay == EGL_NO_DISPLAY) { + __android_log_print(ANDROID_LOG_ERROR, TAG, "egl_init fail: eglDisplay == EGL_NO_DISPLAY"); + return -1; + } + + EGLint *version = new EGLint[2]; + if (!eglInitialize(eglDisplay, &version[0], &version[1])) { + __android_log_print(ANDROID_LOG_ERROR, TAG, "egl_init fail: eglInitialize fail"); + return -1; + } + + const EGLint attrib_config_list[] = { + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 8, + EGL_STENCIL_SIZE, 8, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_NONE + }; + + EGLint num_config; + if (!eglChooseConfig(eglDisplay, attrib_config_list, NULL, 1, &num_config)) { + __android_log_print(ANDROID_LOG_ERROR, TAG, "egl_init fail: eglChooseConfig fail"); + return -1; + } + + + EGLConfig eglConfig; + if (!eglChooseConfig(eglDisplay, attrib_config_list, &eglConfig, num_config, &num_config)) { + __android_log_print(ANDROID_LOG_ERROR, TAG, "egl_init fail: eglChooseConfig fail"); + return -1; + } + + const EGLint attrib_ctx_list[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + + EGLContext eglContext = eglCreateContext(eglDisplay, eglConfig, NULL, attrib_ctx_list); + if (eglContext == EGL_NO_CONTEXT) { + __android_log_print(ANDROID_LOG_ERROR, TAG, "egl_init fail: eglCreateContext fail"); + return -1; + } + + EGLSurface eglSurface = eglCreatePbufferSurface(eglDisplay, eglConfig, NULL); + if (eglSurface == EGL_NO_SURFACE) { + __android_log_print(ANDROID_LOG_ERROR, TAG, "egl_init fail: eglCreatePbufferSurface fail"); + return -1; + } + + if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { + __android_log_print(ANDROID_LOG_ERROR, TAG, "egl_init fail: eglMakeCurrent fail"); + return -1; + } + + return 0; +} + +void native_gl_profiler(const std::string& thread_name, GLuint *textures, GLuint *buffers, GLuint *renderbuffers, int total_count) { + egl_init(); + + struct timeval tv{}; + gettimeofday(&tv, nullptr); + long start = tv.tv_sec * 1000 + tv.tv_usec / 1000; + for (int i = 0; i < total_count; i++) { + glGenRenderbuffers(1, renderbuffers + i); + glGenTextures(1, textures + i); + glGenBuffers(1, buffers + i); + + glBindBuffer(GL_ARRAY_BUFFER, buffers[i]); + glBufferData(GL_ARRAY_BUFFER, 1, nullptr, GL_STATIC_DRAW); + + glBindTexture(GL_TEXTURE_2D, textures[i]); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[i]); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 0, 0); + } + + gettimeofday(&tv, nullptr); + long end = tv.tv_sec * 1000 + tv.tv_usec / 1000; + + long cost = end - start; + + __android_log_print(ANDROID_LOG_ERROR, TAG, "native_gl_profiler thread = %s finish, cost = %ld, start = %ld, end = %ld", thread_name.c_str(), cost, start, end); +} +extern "C" +JNIEXPORT void JNICALL +Java_com_example_openglhook_OpenglHookTestActivity_openglNativeProfiler(JNIEnv *env, + jobject thiz, + jint thread_count) { + + for (int i = 0; i < thread_count; ++i) { + int total_count = 5000; + auto *textures = new GLuint[total_count]; + auto *buffers = new GLuint[total_count]; + auto *renderbuffers = new GLuint[total_count]; + + std::thread test_thread = std::thread([i, textures, buffers, renderbuffers, total_count]() { + std::string thread_name = "opengl_test_thread_" + std::to_string(i); + native_gl_profiler(thread_name, textures, buffers, renderbuffers, total_count); + }); + test_thread.detach(); + } +} \ No newline at end of file diff --git a/matrix/matrix-android/test/test-openglhook/src/main/java/com/example/openglhook/OpenglHookTestActivity.java b/matrix/matrix-android/test/test-openglhook/src/main/java/com/example/openglhook/OpenglHookTestActivity.java new file mode 100644 index 000000000..6a003cce0 --- /dev/null +++ b/matrix/matrix-android/test/test-openglhook/src/main/java/com/example/openglhook/OpenglHookTestActivity.java @@ -0,0 +1,427 @@ +package com.example.openglhook; + +import static android.opengl.GLES20.GL_RENDERBUFFER; +import static android.opengl.GLES30.GL_DEPTH24_STENCIL8; +import static android.opengl.GLES30.GL_DEPTH_COMPONENT32F; + +import androidx.appcompat.app.AppCompatActivity; + +import android.app.Application; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.opengl.EGLContext; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.util.Log; +import android.view.View; + +import com.example.test_openglleak.R; +import com.tencent.matrix.Matrix; +import com.tencent.matrix.openglleak.OpenglLeakPlugin; +import com.tencent.matrix.openglleak.hook.OpenGLHook; +import com.tencent.matrix.openglleak.statistics.LeakMonitorForBackstage; +import com.tencent.matrix.openglleak.statistics.resource.OpenGLInfo; +import com.tencent.matrix.openglleak.statistics.resource.ResRecordManager; +import com.tencent.matrix.openglleak.utils.EGLHelper; +import com.tencent.matrix.util.MatrixLog; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +public class OpenglHookTestActivity extends AppCompatActivity { + + private static final int JAVA_THREAD_COUNT = 4; + private static final int NATIVE_THREAD_COUNT = 4; + + private static final String TAG = "matrix.openglHook"; + + private final Handler[] mHandlers = new Handler[JAVA_THREAD_COUNT]; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_opengl_leak_test); + + System.loadLibrary("test-openglhook"); + + for (int i = 0; i < JAVA_THREAD_COUNT; i++) { + HandlerThread handlerThread = new HandlerThread("Java_thread_" + i); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + mHandlers[i] = handler; + } + + + findViewById(R.id.install_opengl_plugin).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Log.e(TAG, "current threadId = " + Thread.currentThread().getName()); + installOpenGLPlugin(); + } + }); + + findViewById(R.id.init_egl_env).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + initEGLContext(); + } + }); + + findViewById(R.id.use_textures).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + useTextures(); + } + }); + + findViewById(R.id.use_buffer).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + useBuffers(); + } + }); + + findViewById(R.id.use_render_buffer).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + useRenderbuffer(); + } + }); + + findViewById(R.id.dump_to_string).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int queueSize = OpenGLHook.getInstance().getResidualQueueSize(); + Log.e(TAG, "queueSize = " + queueSize); + + if (queueSize != 0) { + return; + } + + String content = ResRecordManager.getInstance().dumpGLToString(); + Log.e(TAG, "dump content = " + content); + } + }); + + findViewById(R.id.dump_to_file).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int queueSize = OpenGLHook.getInstance().getResidualQueueSize(); + Log.e(TAG, "queueSize = " + queueSize); + + if (queueSize != 0) { + return; + } + + openglDump(); + } + }); + + findViewById(R.id.gl_profiler).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + openglNativeProfiler(NATIVE_THREAD_COUNT); + openglJavaProfiler(); + } + }); + + } + + private native void openglNativeProfiler(int threadCnt); + + private void openglJavaProfiler() { + for (int i = 0; i < JAVA_THREAD_COUNT; i++) { + + mHandlers[i].post(new Runnable() { + @Override + public void run() { + EGLHelper.initOpenGL(); + long start = System.currentTimeMillis(); + + int totalCount = 5000; + int[] textures = new int[totalCount]; + int[] buffers = new int[totalCount]; + int[] renderbuffers = new int[totalCount]; + + for (int i = 0; i < totalCount; i++) { + + GLES20.glGenRenderbuffers(1, renderbuffers, i); + GLES20.glGenTextures(1, textures, i); + GLES20.glGenBuffers(1, buffers, i); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[i]); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, 1, null, GLES20.GL_STATIC_DRAW); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]); + + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, 1, 1, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); + GLES20.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[i]); + GLES20.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 0, 0); + } + long end = System.currentTimeMillis(); + long cost = end - start; + Log.e(TAG, Thread.currentThread().getName() + ": openglProfiler finish, cost = " + cost); + } + }); + } + } + + private void openglDump() { + final String dumpPath = getExternalCacheDir() + File.separator + Process.myPid() + "_opengl_hook.log"; + + File dumpFile = new File(dumpPath); + if (dumpFile.exists()) { + boolean result = dumpFile.delete(); + Log.e(TAG, "delete dumpFile result = " + result); + } + + try { + dumpFile.createNewFile(); + } catch (IOException e) { + Log.e(TAG, "dumpFile create fail"); + } + + ResRecordManager.getInstance().dumpGLToFile(dumpPath); + Log.e(TAG, "openGLHook dump, dumpPath = " + dumpPath); + } + + private void useRenderbuffer() { + int[] renderbuffers = new int[1]; + GLES20.glGenRenderbuffers(1, IntBuffer.wrap(renderbuffers)); + GLES20.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]); + GLES20.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32F, 3000, 3000); + GLES20.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 10480, 10480); + GLES20.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 10480, 10480); + GLES20.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 10250, 10250); + } + + private void useBuffers() { + int[] buffers = new int[1]; + + ByteBuffer vertexBuffer = ByteBuffer.allocateDirect(40 * 15); + vertexBuffer.putInt(1); + vertexBuffer.putInt(2); + vertexBuffer.putInt(3); + vertexBuffer.putInt(4); + + GLES20.glGenBuffers(1, IntBuffer.wrap(buffers)); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, 40, vertexBuffer, GLES20.GL_STATIC_DRAW); + } + + private void useTextures() { + Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); + + int[] textures = new int[1]; + GLES20.glGenTextures(1, IntBuffer.wrap(textures)); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + } + + private void initEGLContext() { + EGLHelper.initOpenGL(); + } + + + private void installOpenGLPlugin() { + OpenglLeakPlugin plugin = new OpenglLeakPlugin(getApplication()); + new Matrix.Builder(((Application) getApplicationContext())).plugin(plugin).build().startAllPlugins(); + LeakMonitorForBackstage monitor = new LeakMonitorForBackstage(3 * 1000L); + monitor.setLeakListener(new LeakMonitorForBackstage.LeakListener() { + @Override + public void onLeak(OpenGLInfo info) { + Log.e("onLeak", info + ""); + } + }); + monitor.start((Application) getApplicationContext()); + +// OpenGLHook.getInstance().setResourceListener(new OpenGLHook.ResourceListener() { +// @Override +// public void onGlGenTextures(OpenGLInfo info) { +// Log.e(TAG, "onGlGenTextures = " + info); +// } +// +// @Override +// public void onGlDeleteTextures(OpenGLInfo info) { +// Log.e(TAG, "onGlDeleteTextures = " + info); +// } +// +// @Override +// public void onGlGenBuffers(OpenGLInfo info) { +// Log.e(TAG, "onGlGenBuffers = " + info); +// } +// +// @Override +// public void onGlDeleteBuffers(OpenGLInfo info) { +// Log.e(TAG, "onGlDeleteBuffers = " + info); +// } +// +// @Override +// public void onGlGenFramebuffers(OpenGLInfo info) { +// Log.e(TAG, "onGlGenFramebuffers = " + info); +// } +// +// @Override +// public void onGlDeleteFramebuffers(OpenGLInfo info) { +// Log.e(TAG, "onGlDeleteFramebuffers = " + info); +// } +// +// @Override +// public void onGlGenRenderbuffers(OpenGLInfo info) { +// Log.e(TAG, "onGlGenRenderbuffers = " + info); +// } +// +// @Override +// public void onGlDeleteRenderbuffers(OpenGLInfo info) { +// Log.e(TAG, "onGlDeleteRenderbuffers = " + info); +// } +// }); +// +// OpenGLHook.getInstance().setMemoryListener(new OpenGLHook.MemoryListener() { +// @Override +// public void onGlTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, int id, long eglContextId, long size) { +// Log.e(TAG, "onGlTexImage2D , target = " + target + ", level = " + level +// + ", internalFormat = " + internalFormat + ", width = " + width + ", height = " + height +// + ", border = " + border + ", format = " + format + ", type = " + type + ", id = " + id +// + ". eglContextId = " + eglContextId + ", size = " + size); +// } +// +// @Override +// public void onGlTexImage3D(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, int id, long eglContextId, long size) { +// Log.e(TAG, "onGlTexImage3D , target = " + target + ", level = " + level +// + ", internalFormat = " + internalFormat + ", width = " + width + ", height = " + height +// + ", depth = " + depth + ", border = " + border + ", format = " + format +// + ", type = " + type + ", id = " + id + ". eglContextId = " + eglContextId + ", size = " + size); +// } +// +// @Override +// public void onGlBufferData(int target, int usage, int id, long eglContextId, long size) { +// Log.e(TAG, "onGlBufferData, target = " + target + ", usage = " + usage + ", id = " + id +// + ", eglContextId = " + eglContextId + ", size = " + size); +// } +// +// @Override +// public void onGlRenderbufferStorage(int target, int width, int height, int internalFormat, int id, long eglContextId, long size) { +// Log.e(TAG, "onGlRenderbufferStorage, target = " + target + ", width = " + width +// + ", height = " + height + ", internalFormat = " + internalFormat + ", id = " + id +// + " , eglContextId = " + eglContextId + ", size = " + size); +// } +// }); +// +// OpenGLHook.getInstance().setBindListener(new OpenGLHook.BindListener() { +// @Override +// public void onGlBindTexture(int target, long eglContextId, int id) { +// Log.e(TAG, "onGlBindTexture, target = " + target + ", eglContextId = " + eglContextId + ", id = " + id); +// } +// +// @Override +// public void onGlBindBuffer(int target, long eglContextId, int id) { +// Log.e(TAG, "onGlBindBuffer, target = " + target + ", eglContextId = " + eglContextId + ", id = " + id); +// } +// +// @Override +// public void onGlBindRenderbuffer(int target, long eglContextId, int id) { +// Log.e(TAG, "onGlBindRenderbuffer, target = " + target + ", eglContextId = " + eglContextId + ", id = " + id); +// } +// +// @Override +// public void onGlBindFramebuffer(int target, long eglContextId, int id) { +// Log.e(TAG, "onGlBindFramebuffer, target = " + target + ", eglContextId = " + eglContextId + ", id = " + id); +// } +// }); + } + + EGLContext initContext = null; + + public void testGLSharedContext(View view) { + + HandlerThread thread1 = new HandlerThread("thread1"); + thread1.start(); + Handler handler1 = new Handler(thread1.getLooper()); + + HandlerThread thread2 = new HandlerThread("thread2"); + thread2.start(); + final Handler handler2 = new Handler(thread2.getLooper()); + + + final int totalCount = 1; + final int[] textures = new int[totalCount]; + final int[] buffers = new int[totalCount]; + final int[] renderBuffers = new int[totalCount]; + final int[] frameBuffers = new int[totalCount]; + + handler1.post(new Runnable() { + @Override + public void run() { + initContext = EGLHelper.initOpenGL(); + MatrixLog.i(TAG, "init Context = %s", initContext.getNativeHandle()); + for (int i = 0; i < totalCount; i++) { + GLES20.glGenRenderbuffers(1, renderBuffers, i); + GLES20.glGenTextures(1, textures, i); + GLES20.glGenBuffers(1, buffers, i); + GLES20.glGenFramebuffers(1, frameBuffers, i); + } + + handler2.post(new Runnable() { + @Override + public void run() { + EGLContext sharedContext = EGLHelper.initOpenGLSharedContext(initContext); + MatrixLog.i(TAG, "shared Context = %s", sharedContext.getNativeHandle()); + for (int i = 0; i < totalCount; i++) { + GLES20.glDeleteRenderbuffers(1, renderBuffers, i); + GLES20.glDeleteTextures(1, textures, i); + GLES20.glDeleteBuffers(1, buffers, i); + GLES20.glDeleteFramebuffers(1, frameBuffers, i); + } + } + }); + } + }); + + handler1.postDelayed(new Runnable() { + @Override + public void run() { + MatrixLog.i(TAG, "dump..."); + dump(); + } + }, 5000); + } + + private void dump() { + final String dirPath = getApplication().getExternalCacheDir() + "/OpenGLHook"; + final String filePath = dirPath + "/" + Process.myPid() + "_opengl_dump.txt"; + File dirFile = new File(dirPath); + if (!dirFile.exists()) { + dirFile.mkdirs(); + } + File dumpFile = new File(filePath); + if (dumpFile.exists()) { + dumpFile.delete(); + } + try { + dumpFile.createNewFile(); + } catch (IOException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + + ResRecordManager.getInstance().dumpGLToFile(filePath); + + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)))){ + String line; + while ((line = br.readLine()) != null) { + MatrixLog.i(TAG, line); + } + } catch (IOException e) { + MatrixLog.printErrStackTrace(TAG, e, ""); + } + } + +} \ No newline at end of file diff --git a/matrix/matrix-android/test/test-openglhook/src/main/res/drawable/ic_launcher_background.xml b/matrix/matrix-android/test/test-openglhook/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/matrix/matrix-android/test/test-openglhook/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/matrix/matrix-android/test/test-openglhook/src/main/res/layout/activity_opengl_leak_test.xml b/matrix/matrix-android/test/test-openglhook/src/main/res/layout/activity_opengl_leak_test.xml new file mode 100644 index 000000000..6cba47ea4 --- /dev/null +++ b/matrix/matrix-android/test/test-openglhook/src/main/res/layout/activity_opengl_leak_test.xml @@ -0,0 +1,75 @@ + + + +