diff --git a/README.md b/README.md index d0e9644..bd8365e 100644 --- a/README.md +++ b/README.md @@ -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` | `Lkotlin/jvm/functions/Function0;` | - | `Function1` | `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. 返回主界面 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 170562f..a67d5fc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro index e5a4d9e..d9735b2 100644 --- a/core/proguard-rules.pro +++ b/core/proguard-rules.pro @@ -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 (...); + 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 *; +} diff --git a/core/src/main/baseline-prof.txt b/core/src/main/baseline-prof.txt index 96526fd..5a358aa 100644 --- a/core/src/main/baseline-prof.txt +++ b/core/src/main/baseline-prof.txt @@ -1,7 +1,9 @@ # YaYa 日历应用 Baseline Profile -# 覆盖冷启动热点路径,消除 JIT 编译导致的启动掉帧 +# 全量 AOT 覆盖:冷启动 + 所有用户交互路径 +# 生成方式:./gradlew :macrobenchmark:updateBaselineProfile +# 手动运行(不自动复制):./gradlew :macrobenchmark:connectedBenchmarkAndroidTest -# ========== 业务层热点方法 ========== +# ========== 启动热点路径(必须 AOT)========== # 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 @@ -15,6 +17,7 @@ HSPLplus/rua/project/DayCellInfo;->(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;->(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;->(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;->()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;->(IFLandroidx/compose/fo HSPLandroidx/compose/foundation/pager/PagerState;->getCurrentPage()I HSPLandroidx/compose/foundation/pager/PagerState;->getCurrentPageOffsetFraction()F +# LazyListState +HSPLandroidx/compose/foundation/lazy/LazyListState;->()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; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9cd30b5..84a71e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/macrobenchmark/build.gradle.kts b/macrobenchmark/build.gradle.kts new file mode 100644 index 0000000..3418b65 --- /dev/null +++ b/macrobenchmark/build.gradle.kts @@ -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/ 下是否有输出" + ) + } + } +} diff --git a/macrobenchmark/src/main/AndroidManifest.xml b/macrobenchmark/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0ed2e18 --- /dev/null +++ b/macrobenchmark/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt b/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt new file mode 100644 index 0000000..032702d --- /dev/null +++ b/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt @@ -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() + } + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e0e286e..a0677c0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,4 +29,5 @@ dependencyResolutionManagement { } include(":core") -include(":app") \ No newline at end of file +include(":app") +include(":macrobenchmark") \ No newline at end of file