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:
parent
6582d5970e
commit
2771e3733b
110
README.md
110
README.md
@ -32,103 +32,31 @@
|
|||||||
|
|
||||||
# 编译 release APK(含 Baseline Profiles)
|
# 编译 release APK(含 Baseline Profiles)
|
||||||
./gradlew :app:assembleRelease
|
./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:
|
仅运行基准测试(不自动复制):
|
||||||
|
|
||||||
| 检查项 | 是否需要更新 | 说明 |
|
```
|
||||||
|--------|-------------|------|
|
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
|
||||||
| 新增/修改了首帧渲染的 composable | 必须 | `MainActivity` → `CalendarMonthView` 启动路径上的任何 composable |
|
```
|
||||||
| 修改了 `DayCell.kt` 的方法签名 | 必须 | DayCell 是启动最热点(35 次调用) |
|
|
||||||
| 修改了 `CalendarMonthPage.kt` 的方法签名 | 必须 | 月度网格页面在首帧渲染 |
|
|
||||||
| 修改了 `CalendarMonthView.kt` 的方法签名 | 必须 | 根 composable |
|
|
||||||
| 修改了 `LunarCache.kt` 的计算逻辑 | 建议 | 缓存 miss 时会走 compute() 路径 |
|
|
||||||
| 新增/删除了 `tyme4kt` 的调用 | 建议 | 农历计算是 CPU 密集型 |
|
|
||||||
| 仅修改 UI 颜色、文字、布局 | 不需要 | 不涉及方法签名变化 |
|
|
||||||
| 新增设置页、关于页等非首屏页面 | 不需要 | 不在冷启动路径 |
|
|
||||||
|
|
||||||
### 如何更新
|
手动复制路径:
|
||||||
|
`macrobenchmark/build/outputs/connected_android_test_additional_output/`
|
||||||
|
|
||||||
1. **定位新增/变更的热点方法**
|
测试覆盖全部用户交互路径,实现全量 AOT:
|
||||||
|
|
||||||
通过 logcat 抓取启动时的 JIT 编译日志:
|
1. 冷启动 → 首帧渲染
|
||||||
```bash
|
2. FAB 展开 → 年视图 → 月视图
|
||||||
adb shell setprop dalvik.vm.extra-opts -verbose:compiler
|
3. 日期选择 → 周视图折叠/展开
|
||||||
adb logcat -s "JIT" | grep "plus/rua/project"
|
4. 关于页 → 开源许可页
|
||||||
```
|
5. 返回主界面
|
||||||
|
|
||||||
或抓取带 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 进行深度性能分析。
|
|
||||||
|
|||||||
@ -45,8 +45,16 @@ android {
|
|||||||
)
|
)
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
|
// benchmark 构建类型供 macrobenchmark 模块使用
|
||||||
|
create("benchmark") {
|
||||||
|
initWith(buildTypes.getByName("release"))
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
matchingFallbacks += listOf("release")
|
||||||
|
isDebuggable = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = false
|
buildConfig = false
|
||||||
|
|||||||
108
core/proguard-rules.pro
vendored
108
core/proguard-rules.pro
vendored
@ -1,17 +1,123 @@
|
|||||||
# Baseline Profiles 保留规则:确保方法名不被 R8 混淆,使 profile 规则匹配正确
|
# Baseline Profiles 保留规则:确保方法名不被 R8 混淆,使 profile 规则匹配正确
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
# 保留 YaYa 业务类方法名(profile 中引用的类)
|
# ========== 启动热点路径保留 ==========
|
||||||
|
|
||||||
|
# DayCell — 启动最热点
|
||||||
-keepclassmembers class plus.rua.project.ui.DayCellKt {
|
-keepclassmembers class plus.rua.project.ui.DayCellKt {
|
||||||
public static void DayCell(...);
|
public static void DayCell(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# LunarCache — 日期计算缓存
|
||||||
-keepclassmembers class plus.rua.project.LunarCache {
|
-keepclassmembers class plus.rua.project.LunarCache {
|
||||||
public static plus.rua.project.DayCellInfo getOrCompute(kotlinx.datetime.LocalDate);
|
public static plus.rua.project.DayCellInfo getOrCompute(kotlinx.datetime.LocalDate);
|
||||||
public static java.lang.String formatLunarDate(kotlinx.datetime.LocalDate);
|
public static java.lang.String formatLunarDate(kotlinx.datetime.LocalDate);
|
||||||
|
public static void precompute(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# DayCellInfo 数据类
|
||||||
-keepclassmembers class plus.rua.project.DayCellInfo {
|
-keepclassmembers class plus.rua.project.DayCellInfo {
|
||||||
public java.lang.String getAnnotationText();
|
public java.lang.String getAnnotationText();
|
||||||
public boolean getIsAnnotationHighlight();
|
public boolean getIsAnnotationHighlight();
|
||||||
public java.lang.String getHolidayBadge();
|
public java.lang.String getHolidayBadge();
|
||||||
public java.lang.String getLunarMonthName();
|
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 *;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
# YaYa 日历应用 Baseline Profile
|
# YaYa 日历应用 Baseline Profile
|
||||||
# 覆盖冷启动热点路径,消除 JIT 编译导致的启动掉帧
|
# 全量 AOT 覆盖:冷启动 + 所有用户交互路径
|
||||||
|
# 生成方式:./gradlew :macrobenchmark:updateBaselineProfile
|
||||||
|
# 手动运行(不自动复制):./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
|
||||||
|
|
||||||
# ========== 业务层热点方法 ==========
|
# ========== 启动热点路径(必须 AOT)==========
|
||||||
|
|
||||||
# DayCell — 启动时最热的 composable(35 次调用)
|
# DayCell — 启动时最热的 composable(35 次调用)
|
||||||
HSPLplus/rua/project/ui/DayCellKt;->DayCell(Lkotlinx/datetime/LocalDate;ZZZLplus/rua/project/ShiftKind;ZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;)V
|
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;->getAnnotationText()Ljava/lang/String;
|
||||||
HSPLplus/rua/project/DayCellInfo;->getIsAnnotationHighlight()Z
|
HSPLplus/rua/project/DayCellInfo;->getIsAnnotationHighlight()Z
|
||||||
HSPLplus/rua/project/DayCellInfo;->getHolidayBadge()Ljava/lang/String;
|
HSPLplus/rua/project/DayCellInfo;->getHolidayBadge()Ljava/lang/String;
|
||||||
|
HSPLplus/rua/project/DayCellInfo;->getLunarMonthName()Ljava/lang/String;
|
||||||
|
|
||||||
# CalendarMonthPage — 月度网格页面
|
# 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
|
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;->pageToYearMonth(IILkotlinx/datetime/LocalDate;)Lkotlin/Pair;
|
||||||
HSPLplus/rua/project/ui/CalendarUtilsKt;->yearMonthToPage(IIII)I
|
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 框架关键路径 ==========
|
# ========== Compose 框架关键路径 ==========
|
||||||
|
|
||||||
# Composer — Compose 运行时核心
|
# Composer — Compose 运行时核心
|
||||||
@ -80,10 +130,10 @@ HSPLandroidx/compose/runtime/EffectsKt;->LaunchedEffect(Ljava/lang/Object;Lkotli
|
|||||||
# Box / Column / Row
|
# 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/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/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
|
# 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
|
# 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
|
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
|
# 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;
|
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 相关 ==========
|
# ========== Pager 相关 ==========
|
||||||
|
|
||||||
# HorizontalPager
|
# 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;->getCurrentPage()I
|
||||||
HSPLandroidx/compose/foundation/pager/PagerState;->getCurrentPageOffsetFraction()F
|
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 图片加载 ==========
|
# ========== Sketch 图片加载 ==========
|
||||||
|
|
||||||
# AsyncImage
|
# AsyncImage
|
||||||
@ -140,3 +199,7 @@ HSPLcom/tyme/solar/SolarTermDay;->getSolarTerm()Lcom/tyme/solar/SolarTerm;
|
|||||||
|
|
||||||
# SolarTerm
|
# SolarTerm
|
||||||
HSPLcom/tyme/solar/SolarTerm;->getName()Ljava/lang/String;
|
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;
|
||||||
|
|||||||
@ -17,6 +17,8 @@ kotlinx-datetime = "0.8.0"
|
|||||||
tyme4kt = "1.4.5"
|
tyme4kt = "1.4.5"
|
||||||
sketch = "4.4.0"
|
sketch = "4.4.0"
|
||||||
profileinstaller = "1.4.0"
|
profileinstaller = "1.4.0"
|
||||||
|
benchmarkMacro = "1.3.4"
|
||||||
|
androidx-uiautomator = "2.3.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
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-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" }
|
sketch-animated-gif = { module = "io.github.panpf.sketch4:sketch-animated-gif", version.ref = "sketch" }
|
||||||
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" }
|
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]
|
[plugins]
|
||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
androidLibrary = { id = "com.android.library", version.ref = "agp" }
|
||||||
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
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" }
|
||||||
|
|||||||
87
macrobenchmark/build.gradle.kts
Normal file
87
macrobenchmark/build.gradle.kts
Normal 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/ 下是否有输出"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
macrobenchmark/src/main/AndroidManifest.xml
Normal file
10
macrobenchmark/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,4 +29,5 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
include(":core")
|
include(":core")
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":macrobenchmark")
|
||||||
Loading…
x
Reference in New Issue
Block a user