perf: 集成 Macrobenchmark 模块,自动生成全量 Baseline Profile

- 新增 :macrobenchmark 模块,提供自动 Baseline Profile 生成
- 扩展 proguard-rules.pro,保留所有业务类方法名确保 profile 匹配
- 全量 AOT 覆盖:冷启动 + 交互路径(年视图、周折叠、关于页等)
- app/build.gradle.kts 新增 benchmark 构建类型
- README 简化 Baseline Profiles 维护指南

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-21 10:50:01 +08:00
parent 6582d5970e
commit 2771e3733b
9 changed files with 444 additions and 97 deletions

110
README.md
View File

@ -32,103 +32,31 @@
# 编译 release APK含 Baseline Profiles
./gradlew :app:assembleRelease
./gradlew :app:installBenchmark
```
## Baseline Profiles 维护指南
Baseline Profile 自动生成器。
本项目已集成 [Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles),用于消除冷启动时的 JIT 编译开销。Release APK 构建时会自动将 `core/src/main/baseline-prof.txt` 打包进 `assets/dexopt/baseline.prof`
运行方式(一键生成 + 自动复制到 :core
### 何时需要更新 `baseline-prof.txt`
```
./gradlew :macrobenchmark:updateBaselineProfile
```
每次发版前,检查以下清单。任一条件命中,就需要更新 profile
仅运行基准测试(不自动复制)
| 检查项 | 是否需要更新 | 说明 |
|--------|-------------|------|
| 新增/修改了首帧渲染的 composable | 必须 | `MainActivity``CalendarMonthView` 启动路径上的任何 composable |
| 修改了 `DayCell.kt` 的方法签名 | 必须 | DayCell 是启动最热点35 次调用) |
| 修改了 `CalendarMonthPage.kt` 的方法签名 | 必须 | 月度网格页面在首帧渲染 |
| 修改了 `CalendarMonthView.kt` 的方法签名 | 必须 | 根 composable |
| 修改了 `LunarCache.kt` 的计算逻辑 | 建议 | 缓存 miss 时会走 compute() 路径 |
| 新增/删除了 `tyme4kt` 的调用 | 建议 | 农历计算是 CPU 密集型 |
| 仅修改 UI 颜色、文字、布局 | 不需要 | 不涉及方法签名变化 |
| 新增设置页、关于页等非首屏页面 | 不需要 | 不在冷启动路径 |
```
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
```
### 如何更新
手动复制路径:
`macrobenchmark/build/outputs/connected_android_test_additional_output/`
1. **定位新增/变更的热点方法**
测试覆盖全部用户交互路径,实现全量 AOT
通过 logcat 抓取启动时的 JIT 编译日志:
```bash
adb shell setprop dalvik.vm.extra-opts -verbose:compiler
adb logcat -s "JIT" | grep "plus/rua/project"
```
或抓取带 ART 详细日志的启动 trace
```bash
adb shell am start -W -n plus.rua.project/.MainActivity
adb logcat -d | grep -E "JIT|dex2oat|BaselineProfile"
```
2. **编辑 `core/src/main/baseline-prof.txt`**
新增规则格式:
```
HSPL<类全路径;-><方法名>(<参数类型签名>)<返回类型签名>
```
例如新增一个 composable `NewWidget`
```
HSPLplus/rua/project/ui/NewWidgetKt;->NewWidget(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V
```
常用签名速查:
| Kotlin 类型 | Profile 签名 |
|------------|-------------|
| `Int` | `I` |
| `Float` | `F` |
| `Boolean` | `Z` |
| `String` | `Ljava/lang/String;` |
| `LocalDate` | `Lkotlinx/datetime/LocalDate;` |
| `Modifier` | `Landroidx/compose/ui/Modifier;` |
| `Composer` | `Landroidx/compose/runtime/Composer;` |
| `Function0<Unit>` | `Lkotlin/jvm/functions/Function0;` |
| `Function1<T, R>` | `Lkotlin/jvm/functions/Function1;` |
| `ShiftKind?` | `Lplus/rua/project/ShiftKind;` |
| `Unit` / 无返回值 | `V` |
3. **验证方法名不被混淆**
如果新增的方法在 `core/proguard-rules.pro` 中没有保留规则,添加:
```proguard
-keepclassmembers class plus.rua.project.ui.NewWidgetKt {
public static void NewWidget(...);
}
```
4. **编译验证**
```bash
./gradlew :core:compileDebugKotlin
./gradlew :app:assembleRelease
```
5. **确认 profile 打包成功**
```bash
unzip -l app/build/outputs/apk/release/app-release-unsigned.apk | grep baseline
# 应看到 assets/dexopt/baseline.prof
```
### 自动化替代方案(推荐后期迁移)
手动维护容易遗漏,长期建议迁移到 **Macrobenchmark 自动生成**
1. 创建 `:macrobenchmark` 模块
2. 编写启动基准测试(自动遍历冷启动路径)
3. `./gradlew :macrobenchmark:connectedBenchmarkAndroidTest`
4. 自动输出 `baseline-prof.txt`,直接替换即可
参考:[Android Baseline Profiles 官方文档](https://developer.android.com/topic/performance/baselineprofiles/create-profile)
## 性能监控
项目内置了 `LunarCache`(农历/节气/节假日缓存)和性能追踪(`ComposeTrace.kt`)。查看 `DEVELOPMENT.md` 了解如何使用 Perfetto/Systrace 进行深度性能分析。
1. 冷启动 → 首帧渲染
2. FAB 展开 → 年视图 → 月视图
3. 日期选择 → 周视图折叠/展开
4. 关于页 → 开源许可页
5. 返回主界面

View File

@ -45,8 +45,16 @@ android {
)
signingConfig = signingConfigs.getByName("debug")
}
// benchmark 构建类型供 macrobenchmark 模块使用
create("benchmark") {
initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
isDebuggable = false
}
}
buildFeatures {
compose = true
buildConfig = false

View File

@ -1,17 +1,123 @@
# Baseline Profiles 保留规则:确保方法名不被 R8 混淆,使 profile 规则匹配正确
-keepattributes SourceFile,LineNumberTable
# 保留 YaYa 业务类方法名profile 中引用的类)
# ========== 启动热点路径保留 ==========
# DayCell 启动最热点
-keepclassmembers class plus.rua.project.ui.DayCellKt {
public static void DayCell(...);
}
# LunarCache 日期计算缓存
-keepclassmembers class plus.rua.project.LunarCache {
public static plus.rua.project.DayCellInfo getOrCompute(kotlinx.datetime.LocalDate);
public static java.lang.String formatLunarDate(kotlinx.datetime.LocalDate);
public static void precompute(...);
}
# DayCellInfo 数据类
-keepclassmembers class plus.rua.project.DayCellInfo {
public java.lang.String getAnnotationText();
public boolean getIsAnnotationHighlight();
public java.lang.String getHolidayBadge();
public java.lang.String getLunarMonthName();
}
# CalendarMonthPage
-keepclassmembers class plus.rua.project.ui.CalendarMonthPageKt {
public static void CalendarMonthPage(...);
public static java.util.List generateMonthDays(...);
}
# CalendarMonthView
-keepclassmembers class plus.rua.project.ui.CalendarMonthViewKt {
public static void CalendarMonthView(...);
}
# CalendarPager
-keepclassmembers class plus.rua.project.ui.CalendarPagerKt {
public static void CalendarPager(...);
}
# WeekPager
-keepclassmembers class plus.rua.project.ui.WeekPagerKt {
public static void WeekPager(...);
}
# BottomCard
-keepclassmembers class plus.rua.project.ui.BottomCardKt {
public static void BottomCard(...);
}
# CalendarViewModel
-keepclassmembers class plus.rua.project.CalendarViewModel {
public <init>(...);
public kotlinx.datetime.LocalDate getSelectedDate();
public java.util.List getMonthDays(...);
}
# ========== 全量业务类保留 ==========
# 保留所有业务类名和方法名,确保 profile 通配规则始终匹配
# CalendarUtils 所有方法
-keepclassmembers class plus.rua.project.ui.CalendarUtilsKt {
public static *;
}
# MonthHeader
-keepclassmembers class plus.rua.project.ui.MonthHeaderKt {
public static *;
}
# WeekdayHeader
-keepclassmembers class plus.rua.project.ui.WeekdayHeaderKt {
public static *;
}
# YearGridView / YearHeader / MiniMonth
-keepclassmembers class plus.rua.project.ui.YearGridViewKt {
public static *;
}
# AboutScreen
-keepclassmembers class plus.rua.project.ui.AboutScreenKt {
public static *;
}
# LicensesScreen / LicenseItem
-keepclassmembers class plus.rua.project.ui.LicensesScreenKt {
public static *;
}
-keepclassmembers class plus.rua.project.ui.LicensesKt {
public static *;
}
# AnimatedGif
-keepclassmembers class plus.rua.project.ui.AnimatedGifKt {
public static *;
}
# CalendarViewModel$CalendarDay
-keepclassmembers class plus.rua.project.CalendarViewModel$CalendarDay {
public *;
}
# ShiftPattern
-keepclassmembers class plus.rua.project.ShiftPattern {
public *;
}
# AppInfo
-keepclassmembers class plus.rua.project.AppInfo {
public static *;
}
# ComposeTrace
-keepclassmembers class plus.rua.project.ComposeTraceKt {
public static *;
}
# Platform
-keepclassmembers class plus.rua.project.PlatformKt {
public static *;
}

View File

@ -1,7 +1,9 @@
# YaYa 日历应用 Baseline Profile
# 覆盖冷启动热点路径,消除 JIT 编译导致的启动掉帧
# 全量 AOT 覆盖:冷启动 + 所有用户交互路径
# 生成方式:./gradlew :macrobenchmark:updateBaselineProfile
# 手动运行(不自动复制):./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
# ========== 业务层热点方法 ==========
# ========== 启动热点路径(必须 AOT==========
# DayCell — 启动时最热的 composable35 次调用)
HSPLplus/rua/project/ui/DayCellKt;->DayCell(Lkotlinx/datetime/LocalDate;ZZZLplus/rua/project/ShiftKind;ZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;)V
@ -15,6 +17,7 @@ HSPLplus/rua/project/DayCellInfo;-><init>(Ljava/lang/String;ZLjava/lang/String;L
HSPLplus/rua/project/DayCellInfo;->getAnnotationText()Ljava/lang/String;
HSPLplus/rua/project/DayCellInfo;->getIsAnnotationHighlight()Z
HSPLplus/rua/project/DayCellInfo;->getHolidayBadge()Ljava/lang/String;
HSPLplus/rua/project/DayCellInfo;->getLunarMonthName()Ljava/lang/String;
# CalendarMonthPage — 月度网格页面
HSPLplus/rua/project/ui/CalendarMonthPageKt;->CalendarMonthPage(ILkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlin/jvm/functions/Function1;FIFLkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;)V
@ -45,6 +48,53 @@ HSPLplus/rua/project/ui/CalendarUtilsKt;->calculateWeeksCountForPage(ILkotlinx/d
HSPLplus/rua/project/ui/CalendarUtilsKt;->pageToYearMonth(IILkotlinx/datetime/LocalDate;)Lkotlin/Pair;
HSPLplus/rua/project/ui/CalendarUtilsKt;->yearMonthToPage(IIII)I
# ========== 全量业务组件覆盖 ==========
# CalendarUtils 剩余方法lerp、toWeekMonday、pageToWeekMonday、relativeDayDescription、formatLunarDate
HSPLplus/rua/project/ui/CalendarUtilsKt;->lerp(FFF)F
HSPLplus/rua/project/ui/CalendarUtilsKt;->pageToWeekMonday(ILkotlinx/datetime/LocalDate;)Lkotlinx/datetime/LocalDate;
HSPLplus/rua/project/ui/CalendarUtilsKt;->formatLunarDate(Lkotlinx/datetime/LocalDate;)Ljava/lang/String;
HSPLplus/rua/project/ui/CalendarUtilsKt;->relativeDayDescription(Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)Ljava/lang/String;
# MonthHeader
HSPLplus/rua/project/ui/MonthHeaderKt;->MonthHeader(IILandroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
# WeekdayHeader
HSPLplus/rua/project/ui/WeekdayHeaderKt;->WeekdayHeader(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
# YearGridView / YearHeader / MiniMonth
HSPLplus/rua/project/ui/YearGridViewKt;->YearGridView(Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;)V
HSPLplus/rua/project/ui/YearGridViewKt;->YearHeader(ILkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
# AboutScreen
HSPLplus/rua/project/ui/AboutScreenKt;->AboutScreen(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;)V
# LicensesScreen / LicenseItem
HSPLplus/rua/project/ui/LicensesScreenKt;->LicensesScreen(Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;)V
# AnimatedGif
HSPLplus/rua/project/ui/AnimatedGifKt;->AnimatedGif(Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;Landroidx/compose/runtime/Composer;II)V
# CalendarViewModel 内部数据类
HSPLplus/rua/project/CalendarViewModel$CalendarDay;-><init>(Lkotlinx/datetime/LocalDate;Z)V
HSPLplus/rua/project/CalendarViewModel$CalendarDay;->getDate()Lkotlinx/datetime/LocalDate;
HSPLplus/rua/project/CalendarViewModel$CalendarDay;->getIsToday()Z
# ShiftPattern
HSPLplus/rua/project/ShiftPattern;-><init>(Lkotlinx/datetime/LocalDate;[Lplus/rua/project/ShiftKind;)V
HSPLplus/rua/project/ShiftPattern;->getShift(Lkotlinx/datetime/LocalDate;)Lplus/rua/project/ShiftKind;
# AppInfo
HSPLplus/rua/project/AppInfo;-><clinit>()V
# ComposeTrace
HSPLplus/rua/project/ComposeTraceKt;->composeTraceBeginSection(Ljava/lang/String;)V
HSPLplus/rua/project/ComposeTraceKt;->composeTraceEndSection()V
# Platform
HSPLplus/rua/project/PlatformKt;->getAppVersion(Landroid/content/Context;)Ljava/lang/String;
HSPLplus/rua/project/PlatformKt;->PredictiveBackHandler(ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
# ========== Compose 框架关键路径 ==========
# Composer — Compose 运行时核心
@ -80,10 +130,10 @@ HSPLandroidx/compose/runtime/EffectsKt;->LaunchedEffect(Ljava/lang/Object;Lkotli
# Box / Column / Row
HSPLandroidx/compose/foundation/layout/BoxKt;->Box(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
HSPLandroidx/compose/foundation/layout/ColumnKt;->Column(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Alignment$Horizontal;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
HSPLandroidx/compose/foundation/layout/RowKt;->Row(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/Arrangement$Horizontal;Landroidx/compose/ui/Alignment$Vertical;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
HSPLandroidx/compose/foundation/layout/RowKt;->Row(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment$Vertical;Landroidx/compose/foundation/layout/Arrangement$Horizontal;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
# Text
HSPLandroidx/compose/material3/TextKt;->Text(Ljava/lang/String;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontSynthesis;Landroidx/compose/ui/text/style/TextAlign;Landroidx/compose/ui/text/style/TextDirection;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextGeometricTransform;Landroidx/compose/ui/text/style/TextIndent;Landroidx/compose/ui/text/PlatformTextStyle;Landroidx/compose/ui/text/style/LineHeightStyle;Landroidx/compose/ui/text/style/LineBreak;Landroidx/compose/ui/text/style/Hyphens;Landroidx/compose/ui/text/IntLocale;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/text/SpanStyle;Landroidx/compose/runtime/Composer;IIII)V
HSPLandroidx/compose/material3/TextKt;->Text(Ljava/lang/String;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontSynthesis;Landroidx/compose/ui/text/style/TextAlign;Landroidx/compose/ui/text/style/TextDirection;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextGeometricTransform;Landroidx/compose/ui/text/style/TextIndent;Landroidx/compose/text/PlatformTextStyle;Landroidx/compose/ui/text/style/LineHeightStyle;Landroidx/compose/ui/text/style/LineBreak;Landroidx/compose/ui/text/style/Hyphens;Landroidx/compose/ui/text/IntLocale;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/text/SpanStyle;Landroidx/compose/runtime/Composer;IIII)V
# Surface / MaterialTheme
HSPLandroidx/compose/material3/SurfaceKt;->Surface(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;JJFFLandroidx/compose/foundation/BorderStroke;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
@ -104,6 +154,10 @@ HSPLandroidx/compose/animation/core/Transition$TransitionAnimationState;->update
# animateFloatAsState
HSPLandroidx/compose/animation/core/AnimateAsStateKt;->animateFloatAsState(FLandroidx/compose/animation/core/AnimationSpec;FLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State;
# AnimatedContent / fadeIn / fadeOut
HSPLandroidx/compose/animation/AnimatedContentKt;->AnimatedContent(Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;III)V
HSPLandroidx/compose/animation/AnimatedVisibilityKt;->AnimatedVisibility(ZLandroidx/compose/ui/Modifier;Landroidx/compose/animation/EnterTransition;Landroidx/compose/animation/ExitTransition;Ljava/lang/String;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
# ========== Pager 相关 ==========
# HorizontalPager
@ -112,6 +166,11 @@ HSPLandroidx/compose/foundation/pager/PagerState;-><init>(IFLandroidx/compose/fo
HSPLandroidx/compose/foundation/pager/PagerState;->getCurrentPage()I
HSPLandroidx/compose/foundation/pager/PagerState;->getCurrentPageOffsetFraction()F
# LazyListState
HSPLandroidx/compose/foundation/lazy/LazyListState;-><init>()V
HSPLandroidx/compose/foundation/lazy/LazyListState;->getFirstVisibleItemIndex()I
HSPLandroidx/compose/foundation/lazy/LazyListState;->getFirstVisibleItemScrollOffset()I
# ========== Sketch 图片加载 ==========
# AsyncImage
@ -140,3 +199,7 @@ HSPLcom/tyme/solar/SolarTermDay;->getSolarTerm()Lcom/tyme/solar/SolarTerm;
# SolarTerm
HSPLcom/tyme/solar/SolarTerm;->getName()Ljava/lang/String;
# SolarFestival / LegalHoliday
HSPLcom/tyme/solar/SolarFestival;->getName()Ljava/lang/String;
HSPLcom/tyme/solar/LegalHoliday;->getName()Ljava/lang/String;

View File

@ -17,6 +17,8 @@ kotlinx-datetime = "0.8.0"
tyme4kt = "1.4.5"
sketch = "4.4.0"
profileinstaller = "1.4.0"
benchmarkMacro = "1.3.4"
androidx-uiautomator = "2.3.0"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@ -37,8 +39,14 @@ tyme4kt = { module = "cn.6tail:tyme4kt", version.ref = "tyme4kt" }
sketch-compose = { module = "io.github.panpf.sketch4:sketch-compose", version.ref = "sketch" }
sketch-animated-gif = { module = "io.github.panpf.sketch4:sketch-animated-gif", version.ref = "sketch" }
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" }
androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmarkMacro" }
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-uiautomator" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
androidTest = { id = "com.android.test", version.ref = "agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

View File

@ -0,0 +1,87 @@
plugins {
id("com.android.test")
}
android {
namespace = "plus.rua.project.macrobenchmark"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildTypes {
// benchmark 构建类型必须与 app 模块的 release 类型签名一致
create("benchmark") {
isDebuggable = true
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
}
}
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
dependencies {
implementation(libs.androidx.benchmark.macro)
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.test.espresso)
implementation(libs.androidx.test.uiautomator)
}
// ===== Baseline Profile 自动复制 =====
// 运行 ./gradlew :macrobenchmark:updateBaselineProfile 即可一键生成并复制
val updateBaselineProfile by tasks.registering {
group = "benchmark"
description = "运行 connectedBenchmarkAndroidTest 并将生成的 baseline-prof.txt 复制到 :core 模块"
// 依赖基准测试 task需要先连接设备/模拟器)
dependsOn("connectedBenchmarkAndroidTest")
doLast {
// 寻找生成的 profile 文件(路径格式因设备/Gradle 版本略有不同)
val sourcePaths = listOf(
// AGP 8.x+ 标准输出路径
"build/outputs/connected_android_test_additional_output/benchmark/",
"build/outputs/connected_android_test_additional_output/",
)
val targetFile = rootProject.projectDir.resolve("core/src/main/baseline-prof.txt")
var copied = false
for (path in sourcePaths) {
val dir = file(path)
if (!dir.exists()) continue
val profileFile = dir.walkTopDown()
.firstOrNull { it.name == "baseline-prof.txt" }
?: continue
profileFile.copyTo(targetFile, overwrite = true)
println("✅ Baseline Profile 已自动复制到: ${targetFile.absolutePath}")
println(" 来源: ${profileFile.absolutePath}")
copied = true
break
}
if (!copied) {
throw GradleException(
"未找到生成的 baseline-prof.txt。\n" +
"请确认:\n" +
" 1. 设备/模拟器已连接 (adb devices)\n" +
" 2. 应用已安装在 benchmark 构建类型下\n" +
" 3. 检查 macrobenchmark/build/outputs/ 下是否有输出"
)
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--
Macrobenchmark 模块的 AndroidManifest。
该模块为 com.android.test 类型,通过 targetProjectPath 指向 :app 模块。
在 benchmark 构建类型下运行,自动生成 baseline-prof.txt。
-->
</manifest>

View File

@ -0,0 +1,136 @@
package plus.rua.project.baseline
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Baseline Profile 自动生成器
*
* 运行方式一键生成 + 自动复制到 :core
* ```
* ./gradlew :macrobenchmark:updateBaselineProfile
* ```
*
* 仅运行基准测试不自动复制
* ```
* ./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
* ```
*
* 手动复制路径
* `macrobenchmark/build/outputs/connected_android_test_additional_output/`
*
* 测试覆盖全部用户交互路径实现全量 AOT
* 1. 冷启动 首帧渲染
* 2. FAB 展开 年视图 月视图
* 3. 日期选择 周视图折叠/展开
* 4. 关于页 开源许可页
* 5. 返回主界面
*/
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun generateAppStartupProfile() {
baselineProfileRule.collect(
packageName = "plus.rua.project",
includeInStartupProfile = true,
profileBlock = {
// 1. 冷启动:从 launcher 启动应用
pressHome()
startActivityAndWait()
// 2. 等待首帧渲染完成
device.waitForIdle()
// 3. 模拟用户交互:展开 FAB 菜单
val fab = device.findObject(By.res("plus.rua.project:id/fab_menu"))
if (fab != null) {
fab.click()
device.waitForIdle()
}
// 4. 切换到年视图(覆盖 YearGridView、YearHeader、MiniMonth 路径)
val yearViewButton = device.findObject(By.text("年视图"))
if (yearViewButton != null) {
yearViewButton.click()
device.waitForIdle()
}
// 5. 在年视图中滑动到不同年份(覆盖动画和分页路径)
val yearGrid = device.findObject(By.res("plus.rua.project:id/year_grid"))
if (yearGrid != null) {
yearGrid.swipe(300, 600, 300, 1200, 10)
device.waitForIdle()
yearGrid.swipe(300, 1200, 300, 600, 10)
device.waitForIdle()
}
// 6. 切换回月视图
val monthViewButton = device.findObject(By.text("月视图"))
if (monthViewButton != null) {
monthViewButton.click()
device.waitForIdle()
}
// 7. 点击某一天(覆盖 DayCell 点击路径 + 底部卡片展开)
val todayCell = device.findObject(By.descContains("今天"))
?: device.findObject(By.text("21"))
if (todayCell != null) {
todayCell.click()
device.waitForIdle()
}
// 8. 上下滑动触发月视图↔周视图切换(覆盖 BottomCard 拖拽 + collapse 动画)
val calendarArea = device.findObject(By.res("plus.rua.project:id/calendar_pager"))
?: device.findObject(By.textContains("2026"))
if (calendarArea != null) {
calendarArea.swipe(300, 600, 300, 1200, 10)
device.waitForIdle()
calendarArea.swipe(300, 1200, 300, 600, 10)
device.waitForIdle()
}
// 9. 左右滑动切换月份(覆盖 CalendarPager 翻页)
if (calendarArea != null) {
calendarArea.swipe(600, 800, 100, 800, 10)
device.waitForIdle()
calendarArea.swipe(100, 800, 600, 800, 10)
device.waitForIdle()
}
// 10. 进入关于页面(覆盖 AboutScreen + AnimatedGif
val aboutButton = device.findObject(By.text("关于"))
if (aboutButton != null) {
aboutButton.click()
device.waitForIdle()
}
// 11. 进入开源许可页面(覆盖 LicensesScreen
val licensesButton = device.findObject(By.text("开源许可"))
if (licensesButton != null) {
licensesButton.click()
device.waitForIdle()
}
// 12. 等待许可列表加载
device.wait(Until.findObject(By.textContains("Apache")), 2000)
// 13. 返回关于页
device.pressBack()
device.waitForIdle()
// 14. 返回主界面
device.pressBack()
device.waitForIdle()
}
)
}
}

View File

@ -29,4 +29,5 @@ dependencyResolutionManagement {
}
include(":core")
include(":app")
include(":app")
include(":macrobenchmark")