Compare commits

...

19 Commits

Author SHA1 Message Date
xfy
bd4dad4794 chore(profile): 重新生成 Baseline/Startup Profile
benchmark buildType 已关闭 R8 混淆,重新生成 profile 后
plus.rua.project 下类名保持原始可读签名,避免 release
打包时 profile 匹配风险。
2026-06-16 14:41:25 +08:00
xfy
a731507b3b feat: 月视图年月支持日期选择器跳转
- MonthHeader 新增 onYearMonthClick 回调,点击年月文字打开日期选择器
- CalendarMonthView 集成 DatePickerDialog,选择日期后调用 selectDate
- 复用现有 CalendarPager 动画,跨月跳转时自动平滑翻页

同时包含之前的改动:
- 临时隐藏 SplashActivity 入口,改由 MainActivity 作为 LAUNCHER
- benchmark build type 关闭 R8 混淆与资源压缩,保证 profile 可读性
2026-06-16 14:19:41 +08:00
xfy
85bfba241c fix(splash): use dedicated raster icon asset for splash screen 2026-06-16 13:32:00 +08:00
xfy
fd8e1fc1cb fix(splash): use raster launcher icon to avoid adaptive-icon crash 2026-06-16 13:25:19 +08:00
xfy
be46d4b512 chore(splash): remove unused core-splashscreen catalog entries and fix docs 2026-06-16 13:20:19 +08:00
xfy
b5a2ccf151 refactor(splash): remove unused SplashScreen API dependency and theme 2026-06-16 13:17:17 +08:00
xfy
b5e60e8bf8 docs(app): update AGENTS.md for SplashActivity 2026-06-16 13:14:35 +08:00
xfy
21327d572b feat(splash): add custom SplashActivity with background image 2026-06-16 13:11:43 +08:00
xfy
de329c2020 feat(splash): add dimmed dark-mode splash background 2026-06-16 12:36:49 +08:00
xfy
8204ae15cf feat(splash): use SplashScreen API with custom background 2026-06-16 12:27:12 +08:00
xfy
fab3c2d763 feat(splash): make SplashActivity the launcher entry 2026-06-16 12:11:53 +08:00
xfy
423e68ec19 feat(splash): add splash theme with opaque windowBackground 2026-06-16 12:09:26 +08:00
xfy
0edcc005bc feat(splash): add SplashActivity with 400ms delay 2026-06-16 12:00:02 +08:00
xfy
1f2fc8644f feat(splash): add reusable SplashScreen composable 2026-06-16 11:51:00 +08:00
xfy
d98579c2bf feat(splash): add launch background webp asset 2026-06-16 11:43:56 +08:00
xfy
f6b1ed7368 chore(baseline): 重新生成 Baseline Profile 与 Startup Profile 2026-06-16 10:51:38 +08:00
xfy
b242fb8ecc feat(benchmark): 拆分 Baseline Profile 与 Startup Profile 生成方法 2026-06-16 10:37:58 +08:00
xfy
ade852208b docs: 同步 Baseline Profile 与 Startup Profile 双产物说明 2026-06-16 10:17:21 +08:00
xfy
6f4fa68f0d build(benchmark): 同时复制 Baseline Profile 与 Startup Profile 到 :core 2026-06-16 09:50:13 +08:00
27 changed files with 38374 additions and 7733 deletions

View File

@ -8,7 +8,7 @@
|------|------|------|
| `:core` | `com.android.library` | 所有 Compose UI、ViewModel、业务逻辑 |
| `:app` | `com.android.application` | 薄壳MainActivity + Manifest + 主题 |
| `:macrobenchmark` | `com.android.test` | Baseline Profile 生成 |
| `:macrobenchmark` | `com.android.test` | Baseline Profile / Startup Profile 生成 |
**铁律**`:app` 不添加业务逻辑,所有代码写在 `:core`
@ -22,9 +22,9 @@
./gradlew spotlessApply # 格式化ktlint
```
Baseline Profile 和性能追踪需连接设备:
Baseline Profile / Startup Profile 和性能追踪需连接设备:
```bash
./gradlew :macrobenchmark:updateBaselineProfile # 生成 + 自动复制到 core/src/main/baseline-prof.txt
./gradlew :macrobenchmark:updateBaselineProfile # 生成并复制两份 Profile 到 :core
./scripts/profile.sh # 默认 8 秒,输出到 logs/
```
@ -78,8 +78,8 @@ Baseline Profile 和性能追踪需连接设备:
| 文件 | 内容 |
|------|------|
| `CLAUDE.md` | 完整架构说明、组件树、动画机制、Pager 映射逻辑 |
| `DEVELOPMENT.md` | 性能追踪、Baseline Profile、模拟器启动参数 |
| `DEVELOPMENT.md` | 性能追踪、Baseline Profile / Startup Profile、模拟器启动参数 |
| `COMMENTS.md` | KDoc 规范、注释原则、反模式清单 |
| `app/AGENTS.md` | `:app` 模块细则 |
| `core/AGENTS.md` | `:core` 模块细则 |
| `macrobenchmark/AGENTS.md` | Baseline Profile 模块细则 |
| `macrobenchmark/AGENTS.md` | Baseline Profile / Startup Profile 模块细则 |

View File

@ -5,6 +5,15 @@ All notable changes to the YaYa project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- 修正 `:macrobenchmark` 将 Startup Profile 误当作 Baseline Profile 使用的问题。
`updateBaselineProfile` Task 现在会同时复制 `*-baseline-prof.txt``core/src/main/baseline-prof.txt`
以及 `*-startup-prof.txt``core/src/main/baselineProfiles/startup-prof.txt`,使 AOT 编译优化与 DEX layout 优化同时生效。
- 同步更新所有相关文档(`AGENTS.md``README.md``DEVELOPMENT.md``CLAUDE.md`
`macrobenchmark/AGENTS.md``BaselineProfileGenerator.kt`),明确区分 Baseline Profile 与 Startup Profile。
## [1.1.0] - 2026-06-02
### Added
@ -236,10 +245,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Aliyun Maven mirrors (switched back to Maven Central / Google)
- Unused Compose runtime ProGuard keep rules
## [Unreleased]
- No unreleased changes at this time.
---
[1.1.0]: https://github.com/xfy/yayacal/releases/tag/v1.1.0

View File

@ -29,7 +29,7 @@ Gradle configuration cache and build cache are enabled by default (`gradle.prope
**Three-module structure:**
- `:core` — all Compose UI, ViewModel, and business logic (`com.android.library`)
- `:app` — thin Android shell (`MainActivity``App()`)
- `:macrobenchmark` — Macrobenchmark module for Baseline Profile generation
- `:macrobenchmark` — Macrobenchmark module for Baseline Profile / Startup Profile generation
**Calendar UI composition** (all in `plus.rua.project.ui`):
```

View File

@ -39,7 +39,7 @@ trace 中包含自定义标记:
- `getMonthDays:*` — ViewModel 月份网格计算
- `VM:collapseProgress:*` — 折叠动画拖拽onDrag/onDragEnd/onExpandDrag/onExpandDragEnd
## Baseline Profile
## Baseline Profile / Startup Profile
```bash
# 编译 Android debug APK
@ -48,29 +48,37 @@ trace 中包含自定义标记:
# 安装到设备
./gradlew :app:installDebug
# 编译 release APK含 Baseline Profiles
# 编译 release APK含 Baseline / Startup Profiles
./gradlew :app:assembleRelease
# 安装 benchmark 构建类型 APK
./gradlew :app:installBenchmark
```
Baseline Profile 自动生成器。
一键生成并复制到 `:core`
运行方式(一键生成 + 自动复制到 :core
```
```bash
./gradlew :macrobenchmark:updateBaselineProfile
```
生成后会得到两份产物:
| 产物 | 目标路径 | 用途 |
|------|--------|------|
| Baseline Profile | `core/src/main/baseline-prof.txt` | 指导 ART 做 AOT 编译 |
| Startup Profile | `core/src/main/baselineProfiles/startup-prof.txt` | 指导 AGP 做 DEX layout 优化 |
仅运行基准测试(不自动复制):
```
```bash
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
```
手动复制路径:
`macrobenchmark/build/outputs/connected_android_test_additional_output/`
注意:当前 `benchmark` 构建类型继承自 `release``isMinifyEnabled = true`),因此生成的文本 profile 会包含 R8 混淆后的类/方法名。AGP 在打包 APK/AAB 时会根据最终混淆映射将其转换为二进制 profile这是正常行为。
## 模拟器
```sh

View File

@ -20,7 +20,7 @@
- `kotlinx-datetime` 处理所有日期逻辑
- `tyme4kt` 提供农历、节气与传统节日
- `sketch` 渲染动画 WebP
- 三模块:`:core`UI + 逻辑) · `:app`(薄壳) · `:macrobenchmark`Baseline Profile 生成)
- 三模块:`:core`UI + 逻辑) · `:app`(薄壳) · `:macrobenchmark`Baseline Profile / Startup Profile 生成)
## 构建
@ -37,8 +37,8 @@
./gradlew :core:testDebugUnitTest # 运行全部测试
./gradlew :core:testDebugUnitTest --tests "plus.rua.project.ui.CalendarUtilsTest" # 运行单个测试
# Baseline Profile需要连接设备
./gradlew :macrobenchmark:updateBaselineProfile # 一键生成 + 自动复制到 :core
# Baseline Profile / Startup Profile(需要连接设备)
./gradlew :macrobenchmark:updateBaselineProfile # 生成并复制两份 Profile 到 :core
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest # 仅运行基准测试
# 性能 Profiling需要连接设备

View File

@ -1,21 +1,22 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-05-20 | Updated: 2026-05-20 -->
<!-- Generated: 2026-05-20 | Updated: 2026-06-16 -->
# app
## Purpose
Android 应用壳层模块,仅包含入口 `MainActivity``AboutActivity``LicensesActivity` 和最小化的 Android 平台配置。所有 UI 和业务逻辑均来自 `:core` 模块。
Android 应用壳层模块,仅包含启动页 `SplashActivity`、主界面 `MainActivity``AboutActivity``LicensesActivity` 和最小化的 Android 平台配置。所有 UI 和业务逻辑均来自 `:core` 模块。
## Key Files
| File | Description |
|------|-------------|
| `build.gradle.kts` | Android 应用模块构建配置 |
| `src/main/kotlin/plus/rua/project/MainActivity.kt` | Android 入口 Activity设置 `CalendarMonthView()` Composable |
| `src/main/kotlin/plus/rua/project/MainActivity.kt` | 主界面 Activity设置 `CalendarMonthView()` Composable |
| `src/main/kotlin/plus/rua/project/SplashActivity.kt` | 启动页 Activity400ms 后跳转 MainActivity |
| `src/main/kotlin/plus/rua/project/AboutActivity.kt` | 关于页面 Activity |
| `src/main/kotlin/plus/rua/project/LicensesActivity.kt` | 许可证列表 Activity |
| `src/main/AndroidManifest.xml` | Android 清单,声明 Activities 和主题 |
| `src/main/res/values/themes.xml` | 应用主题配置Material 3 |
| `src/main/res/values/themes.xml` | 应用主题配置Material |
| `src/main/res/values-night/themes.xml` | 夜间模式主题 |
| `src/main/res/values/strings.xml` | 应用名称字符串 |
| `src/main/res/anim/` | Activity 转场动画slide_in/slide_out |
@ -35,6 +36,7 @@ Android 应用壳层模块,仅包含入口 `MainActivity`、`AboutActivity`、
- 不要在此模块添加业务逻辑;所有代码应放在 `:core` 模块
- 仅修改 Android 特有的配置Manifest、主题、权限、Activity 声明
- `MainActivity.kt` 应保持简洁,仅负责调用 `CalendarMonthView()`
- `SplashActivity.kt` 也是薄壳,将启动页 UI 委托给 `:core` 模块
### Testing Requirements
- 构建验证:`./gradlew :app:assembleDebug`
@ -43,7 +45,7 @@ Android 应用壳层模块,仅包含入口 `MainActivity`、`AboutActivity`、
### Common Patterns
- 使用 `enableEdgeToEdge()` 实现全屏边缘到边缘显示
- Activity 转场使用 `overridePendingTransition()` 配合 `res/anim/` 中的 XML 动画
- 主题继承自 `Theme.AppCompat.DayNight.NoActionBar`
- 主题继承自 `Theme.Material.Light.NoActionBar` / `Theme.Material.NoActionBar`
## Dependencies

View File

@ -54,9 +54,11 @@ android {
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
// isDebuggable=false 使 macrobenchmark 在模拟器上稳定运行,且 Partial 编译模式可用。
// 代价是生成的 baseline-prof.txt 会包含 R8 混淆后的类名;功能上仍然有效。
// 若需要可人工维护的可读 profile请在真机上使用 debuggable build 或引入 Baseline Profile Gradle plugin
// 关闭混淆,保证生成的 baseline-prof.txt / startup-prof.txt 使用原始类名,
// 避免 R8 混淆签名导致 profile 匹配与维护风险
isDebuggable = false
isMinifyEnabled = false
isShrinkResources = false
}
}
@ -94,7 +96,6 @@ android {
dependencies {
implementation(project(":core"))
implementation(libs.androidx.core.splashscreen)
implementation(platform(libs.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.profileinstaller)

View File

@ -9,13 +9,22 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.YaYa">
<!--
SplashActivity 已临时禁用,不再作为应用入口。
如需恢复启动页,将下面的 MAIN/LAUNCHER intent-filter 从 MainActivity
移回 SplashActivity 即可。
-->
<activity
android:name=".SplashActivity"
android:exported="true"
android:theme="@style/Theme.YaYa.Splash" />
<activity
android:name=".MainActivity"
android:theme="@style/Theme.YaYa.Starting">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

View File

@ -4,13 +4,11 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.ReportDrawn
import androidx.activity.compose.setContent
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import plus.rua.project.ui.CalendarMonthView
import plus.rua.project.ui.theme.YaYaTheme
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContent {

View File

@ -0,0 +1,43 @@
package plus.rua.project
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.ui.res.painterResource
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import plus.rua.project.ui.SplashScreen
import plus.rua.project.ui.theme.YaYaTheme
private const val SPLASH_DELAY_MS = 400L
/**
* 启动页 Activity
*
* 显示品牌启动图 400ms 后跳转到 [MainActivity] finish 自身
*/
class SplashActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YaYaTheme {
SplashScreen(
iconPainter = painterResource(R.drawable.ic_splash_icon),
)
}
}
lifecycleScope.launch {
delay(SPLASH_DELAY_MS)
navigateToMain()
}
}
private fun navigateToMain() {
if (isFinishing) return
startActivityWithSlide(Intent(this, MainActivity::class.java))
finish()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -2,8 +2,7 @@
<resources>
<style name="Theme.YaYa" parent="@android:style/Theme.Material.NoActionBar" />
<style name="Theme.YaYa.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splash_background</item>
<item name="postSplashScreenTheme">@style/Theme.YaYa</item>
<style name="Theme.YaYa.Splash" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/splash_background</item>
</style>
</resources>

View File

@ -2,14 +2,7 @@
<resources>
<style name="Theme.YaYa" parent="@android:style/Theme.Material.Light.NoActionBar" />
<!--
启动主题:用于 MAIN/LAUNCHER Activity。
继承 Theme.SplashScreen保证 Android 12 以下也能获得一致的启动窗口行为。
windowSplashScreenBackground 使用 Material 3 静态基准色,作为动态颜色不可用时
的 fallback以减少启动窗口与 Compose 首帧之间的色差闪烁。
-->
<style name="Theme.YaYa.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splash_background</item>
<item name="postSplashScreenTheme">@style/Theme.YaYa</item>
<style name="Theme.YaYa.Splash" parent="@android:style/Theme.Material.Light.NoActionBar">
<item name="android:windowBackground">@color/splash_background</item>
</style>
</resources>

View File

@ -1,4 +1,4 @@
# Baseline Profiles 保留规则:确保方法名不被 R8 混淆,使 profile 规则匹配正确
# Baseline Profile / Startup Profile 保留规则:确保方法名不被 R8 混淆,使 profile 规则匹配正确
-keepattributes SourceFile,LineNumberTable
# ========== 第三方库保留 ==========

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package plus.rua.project.ui
import androidx.compose.animation.AnimatedContent
@ -42,11 +44,16 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -74,9 +81,12 @@ import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.number
import kotlinx.datetime.plus
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.todayIn
import kotlin.time.Instant
import plus.rua.project.CalendarViewModel
import plus.rua.project.ShiftKind
import plus.rua.project.composeTraceBeginSection
@ -125,6 +135,7 @@ fun CalendarMonthView(
var rowHeightPx by remember { mutableIntStateOf(0) }
var screenWidthPx by remember { mutableIntStateOf(0) }
var isMenuExpanded by remember { mutableStateOf(false) }
var showDatePicker by remember { mutableStateOf(false) }
// 视图切换时自动关闭菜单
LaunchedEffect(isYearView) {
@ -214,7 +225,8 @@ fun CalendarMonthView(
month = currentMonth,
weekNumber = weekNumber,
showToday = selectedDate != today,
onToday = onToday
onToday = onToday,
onYearMonthClick = { showDatePicker = true }
)
WeekdayHeader(
modifier = Modifier.fillMaxWidth().padding(bottom = ROW_PADDING_DP.dp)
@ -439,6 +451,34 @@ fun CalendarMonthView(
}
}
}
if (showDatePicker) {
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = selectedDate.toEpochMillis()
)
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(
onClick = {
datePickerState.selectedDateMillis?.let { millis ->
viewModel.selectDate(millis.toLocalDate())
}
showDatePicker = false
}
) {
Text("确定")
}
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) {
Text("取消")
}
}
) {
DatePicker(state = datePickerState)
}
}
}
}
@ -611,3 +651,19 @@ private fun MenuItem(
)
}
}
/**
* [LocalDate] 转换为 UTC 午夜的 epoch 毫秒
*
* 用于 DatePicker 初始选中值 [Long.toLocalDate] 成对使用
*/
private fun LocalDate.toEpochMillis(): Long =
this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
/**
* epoch 毫秒转换为 UTC 日期的 [LocalDate]
*
* DatePicker 返回选中日期的 UTC 午夜毫秒经此函数得到本地逻辑日期
*/
private fun Long.toLocalDate(): LocalDate =
Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.UTC).date

View File

@ -34,6 +34,7 @@ import androidx.compose.ui.unit.sp
* @param weekNumber 当前 ISO 周号
* @param showToday 是否显示今天按钮 selectedDate today
* @param onToday 点击今天按钮跳转今天
* @param onYearMonthClick 点击"年月"文字打开日期选择器
* @param modifier 外部布局修饰符
*/
@Composable
@ -43,6 +44,7 @@ fun MonthHeader(
weekNumber: Int,
showToday: Boolean,
onToday: (() -> Unit)? = null,
onYearMonthClick: () -> Unit = {},
modifier: Modifier = Modifier
) {
Row(
@ -66,7 +68,11 @@ fun MonthHeader(
Text(
text = "${y}${m}",
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.titleLarge
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = onYearMonthClick)
.padding(horizontal = 4.dp, vertical = 2.dp)
)
}
Spacer(modifier = Modifier.width(6.dp))

View File

@ -0,0 +1,64 @@
package plus.rua.project.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import plus.rua.project.shared.R as CoreR
private val SplashIconSize = 80.dp
/**
* 品牌启动页 UI
*
* 背景图铺满全屏Light/Dark 资源由 Android 根据配置自动选择
* app icon 水平居中其垂直中心位于状态栏下方可用区域高度的约 25%
* 图片仅用于装饰因此 contentDescription null
*
* @param iconPainter app icon Painter
* @param modifier 外部传入的 Modifier
*/
@Composable
fun SplashScreen(
iconPainter: Painter,
modifier: Modifier = Modifier,
) {
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val screenHeightDp = configuration.screenHeightDp.dp
val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
val availableHeight = screenHeightDp - statusBarHeight
val iconTopPadding = statusBarHeight + (availableHeight * 0.25f) - (SplashIconSize / 2)
Box(
modifier = modifier.fillMaxSize(),
) {
Image(
painter = painterResource(CoreR.drawable.launch_bg),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
Image(
painter = iconPainter,
contentDescription = null,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = iconTopPadding.coerceAtLeast(statusBarHeight))
.size(SplashIconSize),
)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -4,7 +4,6 @@ android-compileSdk = "37"
android-minSdk = "24"
android-targetSdk = "37"
androidx-activity = "1.13.0"
androidx-core-splashscreen = "1.0.1"
androidx-espresso = "3.7.0"
androidx-lifecycle = "2.10.0"
androidx-testExt = "1.3.0"
@ -23,7 +22,6 @@ versions = "0.54.0"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmarkMacro" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" }
androidx-lifecycle-runtimeCompose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" }

View File

@ -1,30 +1,32 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-05-22 | Updated: 2026-05-22 -->
<!-- Generated: 2026-05-22 | Updated: 2026-06-15 -->
# macrobenchmark
## Purpose
宏基准测试模块,使用 Android Baseline Profile 技术自动生成启动优化配置文件。通过 UI Automator 模拟真实用户交互路径,收集 AOT 编译所需的 profile 数据。
宏基准测试模块,使用 Android Baseline Profile / Startup Profile 技术自动生成启动优化配置文件。通过 UI Automator 模拟真实用户交互路径,收集 AOT 编译与 DEX layout 优化所需的 profile 数据。
## Key Files
| File | Description |
|------|-------------|
| `build.gradle.kts` | 模块构建配置(`com.android.test` 插件、benchmark 构建类型、Baseline Profile 自动复制任务) |
| `build.gradle.kts` | 模块构建配置(`com.android.test` 插件、benchmark 构建类型、Baseline/Startup Profile 自动复制任务) |
## Subdirectories
| Directory | Purpose |
|-----------|---------|
| `src/main/java/plus/rua/project/baseline/` | Baseline Profile 生成器(见 `src/main/java/plus/rua/project/baseline/AGENTS.md` |
| `src/main/java/plus/rua/project/baseline/` | Baseline Profile / Startup Profile 生成器与冷启动基准测试(见 `src/main/java/plus/rua/project/baseline/AGENTS.md` |
## For AI Agents
### Working In This Directory
- 修改此模块需连接 Android 设备/模拟器运行
- Baseline Profile 一键生成:`./gradlew :macrobenchmark:updateBaselineProfile`
- Baseline / Startup Profile 一键生成:`./gradlew :macrobenchmark:updateBaselineProfile`
- 仅运行基准测试:`./gradlew :macrobenchmark:connectedBenchmarkAndroidTest`
- 生成的 profile 自动复制到 `core/src/main/baseline-prof.txt`
- 生成的两份产物自动复制到:
- `core/src/main/baseline-prof.txt`Baseline Profile用于 AOT
- `core/src/main/baselineProfiles/startup-prof.txt`Startup Profile用于 DEX layout
### Testing Requirements
- 需要已安装应用的 benchmark 构建类型
@ -34,7 +36,7 @@
### Internal
- `:app` 模块 — 目标测试应用
- `:core` 模块 — profile 自动复制到 `core/src/main/baseline-prof.txt`
- `:core` 模块 — Baseline/Startup Profile 自动复制目标模块
### External
- `androidx.benchmark:benchmark-macro-junit4`

View File

@ -42,53 +42,83 @@ dependencies {
implementation(libs.androidx.test.uiautomator)
}
// ===== Baseline Profile 自动复制 =====
// ===== Baseline Profile / Startup Profile 自动复制 =====
// 运行 ./gradlew :macrobenchmark:updateBaselineProfile 即可一键生成并复制
val updateBaselineProfile by tasks.registering {
group = "benchmark"
description = "运行 connectedBenchmarkAndroidTest 并将生成的 startup-prof.txt 复制到 :core 模块"
description = "运行 connectedBenchmarkAndroidTest 并将生成的 Baseline Profile 与 Startup Profile 复制到 :core 模块"
// 依赖基准测试 task需要先连接设备/模拟器)
dependsOn("connectedBenchmarkAndroidTest")
// 该 task 的输入是 benchmark 运行后的产物,因此不应被跳过
outputs.upToDateWhen { false }
// 预先计算目标路径,避免在 doLast 中引用 project 对象configuration cache 兼容)
val targetPath = rootProject.projectDir.resolve("core/src/main/baseline-prof.txt").absolutePath
val baselineTargetPath = rootProject.projectDir
.resolve("core/src/main/baseline-prof.txt")
.absolutePath
val startupTargetDirPath = rootProject.projectDir
.resolve("core/src/main/baselineProfiles")
.absolutePath
val buildDirPath = layout.buildDirectory.get().asFile.absolutePath
doLast {
// 寻找生成的 profile 文件benchmark 1.4+ 文件名格式:{Class}_{Method}-startup-prof.txt
val sourcePaths = listOf(
"$buildDirPath/outputs/connected_android_test_additional_output/benchmark/",
"$buildDirPath/outputs/connected_android_test_additional_output/",
)
val targetFile = File(targetPath)
var copied = false
for (path in sourcePaths) {
// benchmark 1.4+ 文件名格式:{Class}_{Method}-baseline-prof.txt / {Class}_{Method}-startup-prof.txt
// 排除带时间戳的历史文件(如 ...-2026-06-15-startup-prof.txt只取当前 canonical 文件
val dateStampRegex = Regex("-\\d{4}-\\d{2}-\\d{2}-")
fun findNewestProfile(suffix: String): File? {
val candidates = sourcePaths.flatMap { path ->
val dir = File(path)
if (!dir.exists()) continue
// 优先匹配不带时间戳的 startup-prof.txtbenchmark 1.4+ 格式)
val profileFile = dir.walkTopDown()
.firstOrNull { it.name.endsWith("-startup-prof.txt") && !it.name.contains(Regex("-\\d{4}-\\d{2}-\\d{2}-")) }
?: continue
profileFile.copyTo(targetFile, overwrite = true)
println("✅ Baseline Profile 已自动复制到: ${targetFile.absolutePath}")
println(" 来源: ${profileFile.absolutePath}")
copied = true
break
if (!dir.exists()) emptyList()
else dir.walkTopDown().filter {
it.isFile &&
it.name.endsWith(suffix) &&
!it.name.contains(dateStampRegex)
}.toList()
}
return candidates
.sortedWith(compareByDescending<File> { it.lastModified() }.thenBy { it.absolutePath })
.firstOrNull()
}
if (!copied) {
throw GradleException(
"未找到生成的 *-startup-prof.txt。\n" +
fun requireProfile(suffix: String, label: String): File {
val sourceFile = findNewestProfile(suffix)
?: throw GradleException(
"未找到生成的 *$suffix。\n" +
"请确认:\n" +
" 1. 设备/模拟器已连接 (adb devices)\n" +
" 2. 应用已安装在 benchmark 构建类型下\n" +
" 3. 检查 macrobenchmark/build/outputs/ 下是否有输出"
)
}
if (sourceFile.length() == 0L) {
throw GradleException("生成的 $label 文件为空: ${sourceFile.absolutePath}")
}
return sourceFile
}
// 1) 先定位并校验两份源文件,确保都可用再开始复制,避免部分覆盖目标文件
val baselineSource = requireProfile("-baseline-prof.txt", "Baseline Profile")
val startupSource = requireProfile("-startup-prof.txt", "Startup Profile")
// 2) Baseline Profile → AOT 编译优化
val baselineTargetFile = File(baselineTargetPath)
baselineSource.copyTo(baselineTargetFile, overwrite = true)
logger.lifecycle("✅ Baseline Profile 已自动复制到: ${baselineTargetFile.absolutePath}")
logger.lifecycle(" 来源: ${baselineSource.absolutePath}")
// 3) Startup Profile → DEX layout 优化(必须放在 baselineProfiles/startup-prof.txt
val startupTargetDir = File(startupTargetDirPath)
startupTargetDir.mkdirs()
val startupTargetFile = File(startupTargetDir, "startup-prof.txt")
startupSource.copyTo(startupTargetFile, overwrite = true)
logger.lifecycle("✅ Startup Profile 已自动复制到: ${startupTargetFile.absolutePath}")
logger.lifecycle(" 来源: ${startupSource.absolutePath}")
}
}

View File

@ -4,7 +4,7 @@
<!--
Macrobenchmark 模块的 AndroidManifest。
该模块为 com.android.test 类型,通过 targetProjectPath 指向 :app 模块。
在 benchmark 构建类型下运行,自动生成 baseline-prof.txt。
在 benchmark 构建类型下运行,自动生成 baseline-prof.txt 与 startup-prof.txt
-->
</manifest>

View File

@ -4,15 +4,15 @@
# baseline
## Purpose
Baseline Profile 自动生成器与启动性能基准测试。包含:
- `BaselineProfileGenerator.kt`:通过 UI Automator 模拟核心用户交互路径冷启动、FAB 展开、显示调休切换、CalendarPager 翻页、日期选择、BottomCard 折叠/展开),生成 AOT 编译优化所需的 startup profile
Baseline Profile / Startup Profile 自动生成器与启动性能基准测试。包含:
- `BaselineProfileGenerator.kt`:通过 UI Automator 模拟核心用户交互路径冷启动、FAB 展开、显示调休切换、CalendarPager 翻页、日期选择、BottomCard 折叠/展开),同时生成 Baseline ProfileAOT与 Startup ProfileDEX layout两份产物
- `StartupBenchmark.kt`:冷启动性能基准测试,测量 `timeToInitialDisplay``timeToFullDisplay`
## Key Files
| File | Description |
|------|-------------|
| `BaselineProfileGenerator.kt` | Profile 生成测试类,覆盖启动与核心交互路径 |
| `BaselineProfileGenerator.kt` | Profile 生成测试类,覆盖启动与核心交互路径,生成 `*-baseline-prof.txt``*-startup-prof.txt` |
| `StartupBenchmark.kt` | 冷启动基准测试类,覆盖 Full/Partial/None 三种编译模式 |
## Subdirectories
@ -27,6 +27,8 @@ Baseline Profile 自动生成器与启动性能基准测试。包含:
### Testing Requirements
- 生成 Profile`./gradlew :macrobenchmark:updateBaselineProfile`
- Baseline Profile 输出:`core/src/main/baseline-prof.txt`
- Startup Profile 输出:`core/src/main/baselineProfiles/startup-prof.txt`
- 运行冷启动基准(仅该类):`./gradlew :macrobenchmark:connectedBenchmarkAndroidTest --tests "plus.rua.project.baseline.StartupBenchmark"`
- 需要设备/模拟器连接,应用已安装在 benchmark 构建类型下

View File

@ -14,7 +14,7 @@ import org.junit.Test
import org.junit.runner.RunWith
/**
* Baseline Profile 自动生成器
* Baseline Profile / Startup Profile 自动生成器
*
* 运行方式一键生成 + 自动复制到 :core
* ```
@ -29,19 +29,31 @@ import org.junit.runner.RunWith
* 手动复制路径
* `macrobenchmark/build/outputs/connected_android_test_additional_output/`
*
* 测试覆盖启动与核心用户交互路径实现关键路径 AOT
* 说明
* - 本文件包含两个生成测试
* - `generateBaselineProfile()``includeInStartupProfile = false`生成 `*-baseline-prof.txt`用于 ART AOT 编译优化
* - `generateAppStartupProfile()``includeInStartupProfile = true`生成 `*-startup-prof.txt`用于 AGP DEX layout 优化
* - 两个测试执行相同的 `runCoreUserJourney()` 交互路径因此两份 profile 覆盖范围一致
* - `updateBaselineProfile` Task 会分别将它们复制到
* - `core/src/main/baseline-prof.txt`
* - `core/src/main/baselineProfiles/startup-prof.txt`
*
* 测试覆盖启动与核心用户交互路径用于 AOT DEX layout 优化
* 1. 冷启动 首帧渲染
* 2. 显示调休切换 ON/OFFDayCell 大规模重组 + staggered 动画
* 3. CalendarPager 翻页 "今天"按钮跳回
* 4. 跨月日期点击 自动跳转
* 5. DayCell 点击
* 6. BottomCard 拖拽折叠到周视图
* 7. 周视图左右翻页
* 8. BottomCard 拖拽展开回月视图
* 9. CalendarPager 左右翻页
* 2. 切换"显示调休"ONDayCell 大规模重组 + staggered 动画
* 3. CalendarPager 右滑 上一个月
* 4. 点击"今天"按钮跳回当月
* 5. 点击跨月日期 自动跳转
* 6. 再次点击"今天"跳回当月
* 7. 点击 DayCell
* 8. 拖拽 BottomCard 折叠到周视图
* 9. 周视图左右翻页
* 10. 拖拽 BottomCard 展开回月视图
* 11. 切换"显示调休"OFF
* 12. CalendarPager 左右翻页
*
* 年视图关于/开源许可工具/日期检查器等路径在部分模拟器上不稳定
* 暂时从生成流程中移除以保证 Baseline Profile 可稳定生成后续可在真机上扩展覆盖
* 暂时从生成流程中移除以保证 Profile 可稳定生成后续可在真机上扩展覆盖
*/
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@ -55,12 +67,29 @@ class BaselineProfileGenerator {
private fun MacrobenchmarkScope.safeWaitCalendarPager(timeout: Long = 5000): UiObject2? =
device.wait(Until.findObject(By.res("calendar_pager")), timeout)
@Test
fun generateBaselineProfile() {
baselineProfileRule.collect(
packageName = "plus.rua.project",
includeInStartupProfile = false,
profileBlock = {
runCoreUserJourney()
}
)
}
@Test
fun generateAppStartupProfile() {
baselineProfileRule.collect(
packageName = "plus.rua.project",
includeInStartupProfile = true,
profileBlock = {
runCoreUserJourney()
}
)
}
private fun MacrobenchmarkScope.runCoreUserJourney() {
val TAG = "BaselineProfile"
// ── 1. 冷启动 ──────────────────────────────────────────
@ -169,8 +198,6 @@ class BaselineProfileGenerator {
device.waitForIdle()
}
Log.d(TAG, "Baseline profile 生成完成,核心路径已覆盖")
}
)
Log.d(TAG, "Baseline Profile / Startup Profile 生成完成,所有路径已覆盖")
}
}