perf: 动画性能优化 + 性能追踪工具

- CalendarMonthPage: 提取 WeekRow 子 Composable,graphicsLayer → offset + alpha
- DayCell: updateTransition → 独立 animateFloatAsState/animateColorAsState,
  共享 MutableInteractionSource 减少重组
- CalendarMonthView/WeekPager: graphicsLayer → offset + alpha,共享 interactionSource
- DEVELOPMENT.md: 补充性能追踪使用说明
- scripts/profile.sh: 新增 Perfetto trace 一键抓取脚本

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-22 10:29:58 +08:00
parent fb7e19ddc9
commit baedf878b4
6 changed files with 434 additions and 132 deletions

View File

@ -1,5 +1,35 @@
# 开发指南 # 开发指南
## 性能追踪
使用 `scripts/profile.sh` 一键抓取 Perfetto trace、帧统计和内存快照。
```bash
# 默认抓取 8 秒
./scripts/profile.sh
# 抓取 15 秒
./scripts/profile.sh 15
# 应用已在运行时,不自动启动
./scripts/profile.sh --no-launch
```
输出文件保存在 `logs/` 目录:
| 文件 | 说明 |
|------|------|
| `trace_*.perfetto-trace` | Perfetto trace在 https://ui.perfetto.dev 打开 |
| `framestats_*.txt` | GPU 帧统计 |
| `meminfo_*.txt` | 内存快照 |
| `report_*.md` | 追踪报告摘要 |
trace 中包含自定义标记:
- `MonthView:Compose` — 月视图重组
- `YearView:Compose` — 年视图重组
- `VM:collapseProgress` — 折叠动画
- `getMonthDays:*` — 月份网格计算
## Baseline Profile ## Baseline Profile
```bash ```bash

View File

@ -1,6 +1,7 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -8,10 +9,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.draw.alpha
import androidx.compose.foundation.layout.offset
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -62,32 +65,14 @@ fun CalendarMonthPage(
generateMonthDays(year, month) generateMonthDays(year, month)
} }
val density = LocalDensity.current val density = LocalDensity.current
val interactionSource = remember { MutableInteractionSource() }
val weeks = remember(days) { days.chunked(7) } val weeks = remember(days) { days.chunked(7) }
val anchorIndex = remember(weeks, selectedDate) { val anchorIndex = remember(weeks, selectedDate) {
weeks.indexOfFirst { week -> week.any { it.date == selectedDate } } weeks.indexOfFirst { week -> week.any { it.date == selectedDate } }
} }
val hasAnchor = anchorIndex >= 0
val h = rowHeightPx.toFloat()
// Phase 1 结束点:选中行到顶部所需的比例
val phase1End = if (hasAnchor && anchorIndex > 0 && weeks.size > 1) {
anchorIndex.toFloat() / (weeks.size - 1)
} else 0f
val phase1 = if (phase1End > 0f) {
(collapseProgress / phase1End).coerceIn(0f, 1f)
} else if (collapseProgress > 0f) 1f else 0f
val phase2 = if (phase1End < 1f && collapseProgress > phase1End) {
((collapseProgress - phase1End) / (1f - phase1End)).coerceIn(0f, 1f)
} else 0f
val belowRowsHeight = if (hasAnchor) {
(weeks.size - 1 - anchorIndex) * h
} else 0f
val totalHeightDp = if (rowHeightPx > 0) { val totalHeightDp = if (rowHeightPx > 0) {
val h = rowHeightPx.toFloat()
val totalPx = h * (1 + (effectiveWeeks - 1) * (1f - collapseProgress)) val totalPx = h * (1 + (effectiveWeeks - 1) * (1f - collapseProgress))
with(density) { totalPx.toDp() } with(density) { totalPx.toDp() }
} else null } else null
@ -99,69 +84,122 @@ fun CalendarMonthPage(
) )
) { ) {
weeks.forEachIndexed { weekIndex, week -> weeks.forEachIndexed { weekIndex, week ->
val isAnchor = hasAnchor && weekIndex == anchorIndex key(weekIndex) {
val isAbove = hasAnchor && weekIndex < anchorIndex WeekRow(
val isBelow = hasAnchor && weekIndex > anchorIndex weekIndex = weekIndex,
week = week,
val yOffsetPx = if (rowHeightPx > 0) { anchorIndex = anchorIndex,
when { weeksSize = weeks.size,
!hasAnchor -> weekIndex * h - collapseProgress * weeks.size * h collapseProgress = collapseProgress,
isAnchor -> anchorIndex * h * (1f - phase1) rowHeightPx = rowHeightPx,
isAbove -> weekIndex * h - phase1 * anchorIndex * h selectedDate = selectedDate,
isBelow -> weekIndex * h - phase1 * anchorIndex * h - phase2 * belowRowsHeight today = today,
else -> weekIndex * h shiftKindAt = shiftKindAt,
} showLegalHoliday = showLegalHoliday,
} else 0f onDateClick = onDateClick,
onRowHeightMeasured = onRowHeightMeasured,
val rowAlpha = when { interactionSource = interactionSource
!hasAnchor -> (1f - collapseProgress).coerceIn(0f, 1f) )
isAnchor -> 1f
isAbove -> (1f - phase1).coerceIn(0f, 1f)
isBelow -> (1f - phase2).coerceIn(0f, 1f)
else -> 1f
} }
}
}
}
if (rowAlpha > 0.01f) { @Composable
Row( private fun WeekRow(
modifier = Modifier weekIndex: Int,
.fillMaxWidth() week: List<DayData>,
.zIndex(if (isAnchor) 1f else 0f) anchorIndex: Int,
.then( weeksSize: Int,
if (rowHeightPx > 0) Modifier.height(with(density) { h.toDp() }) collapseProgress: Float,
else Modifier rowHeightPx: Int,
) selectedDate: LocalDate,
.then( today: LocalDate,
if (isAnchor && phase1 >= 1f) Modifier.background(MaterialTheme.colorScheme.surface) shiftKindAt: (LocalDate) -> ShiftKind?,
else Modifier showLegalHoliday: Boolean,
) onDateClick: (LocalDate) -> Unit,
.graphicsLayer { onRowHeightMeasured: ((Int) -> Unit)?,
translationY = yOffsetPx interactionSource: MutableInteractionSource,
alpha = rowAlpha ) {
val density = LocalDensity.current
val hasAnchor = anchorIndex >= 0
val h = rowHeightPx.toFloat()
val isAnchor = hasAnchor && weekIndex == anchorIndex
val isAbove = hasAnchor && weekIndex < anchorIndex
val isBelow = hasAnchor && weekIndex > anchorIndex
val phase1End = if (hasAnchor && anchorIndex > 0 && weeksSize > 1) {
anchorIndex.toFloat() / (weeksSize - 1)
} else 0f
val phase1 = if (phase1End > 0f) {
(collapseProgress / phase1End).coerceIn(0f, 1f)
} else if (collapseProgress > 0f) 1f else 0f
val phase2 = if (phase1End < 1f && collapseProgress > phase1End) {
((collapseProgress - phase1End) / (1f - phase1End)).coerceIn(0f, 1f)
} else 0f
val belowRowsHeight = if (hasAnchor) {
(weeksSize - 1 - anchorIndex) * h
} else 0f
val yOffsetPx = if (rowHeightPx > 0) {
when {
!hasAnchor -> weekIndex * h - collapseProgress * weeksSize * h
isAnchor -> anchorIndex * h * (1f - phase1)
isAbove -> weekIndex * h - phase1 * anchorIndex * h
isBelow -> weekIndex * h - phase1 * anchorIndex * h - phase2 * belowRowsHeight
else -> weekIndex * h
}
} else 0f
val rowAlpha = when {
!hasAnchor -> (1f - collapseProgress).coerceIn(0f, 1f)
isAnchor -> 1f
isAbove -> (1f - phase1).coerceIn(0f, 1f)
isBelow -> (1f - phase2).coerceIn(0f, 1f)
else -> 1f
}
if (rowAlpha > 0.01f) {
Row(
modifier = Modifier
.fillMaxWidth()
.zIndex(if (isAnchor) 1f else 0f)
.then(
if (rowHeightPx > 0) Modifier.height(with(density) { h.toDp() })
else Modifier
)
.then(
if (isAnchor && phase1 >= 1f) Modifier.background(MaterialTheme.colorScheme.surface)
else Modifier
)
.offset(y = with(density) { yOffsetPx.toDp() })
.alpha(rowAlpha)
.then(
if (weekIndex == 0 && rowHeightPx == 0) {
Modifier.onSizeChanged { size ->
if (size.height > 0) {
onRowHeightMeasured?.invoke(size.height)
}
} }
.then( } else Modifier
if (weekIndex == 0 && rowHeightPx == 0) { )
Modifier.onSizeChanged { size -> .padding(vertical = ROW_PADDING_DP.dp)
if (size.height > 0) { ) {
onRowHeightMeasured?.invoke(size.height) week.forEach { dayData ->
} DayCell(
} date = dayData.date,
} else Modifier isCurrentMonth = dayData.isCurrentMonth,
) isSelected = dayData.date == selectedDate,
.padding(vertical = ROW_PADDING_DP.dp) isToday = dayData.date == today,
) { shiftKind = shiftKindAt(dayData.date),
week.forEach { dayData -> showLegalHoliday = showLegalHoliday,
DayCell( onClick = { onDateClick(dayData.date) },
date = dayData.date, modifier = Modifier.weight(1f),
isCurrentMonth = dayData.isCurrentMonth, interactionSource = interactionSource
isSelected = dayData.date == selectedDate, )
isToday = dayData.date == today,
shiftKind = shiftKindAt(dayData.date),
showLegalHoliday = showLegalHoliday,
onClick = { onDateClick(dayData.date) },
modifier = Modifier.weight(1f)
)
}
}
} }
} }
} }

View File

@ -60,7 +60,6 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@ -558,10 +557,9 @@ private fun BottomCardArea(
onExpandDrag = { delta -> viewModel.onExpandDrag(delta) }, onExpandDrag = { delta -> viewModel.onExpandDrag(delta) },
onExpandDragEnd = { velocity -> viewModel.onExpandDragEnd(velocity) }, onExpandDragEnd = { velocity -> viewModel.onExpandDragEnd(velocity) },
dragRangePx = dragRangePx, dragRangePx = dragRangePx,
modifier = modifier.graphicsLayer { modifier = modifier
translationY = slideProgress * 200.dp.toPx() .offset(y = with(density) { (slideProgress * 200).dp })
alpha = 1f - slideProgress .alpha(1f - slideProgress)
}
) )
} }
} }

View File

@ -1,10 +1,9 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.animation.animateColor import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@ -69,6 +68,7 @@ fun DayCell(
showLegalHoliday: Boolean, showLegalHoliday: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
lunarCache: LunarCache = LunarCache.default lunarCache: LunarCache = LunarCache.default
) { ) {
val lunarData by produceState( val lunarData by produceState(
@ -89,83 +89,74 @@ fun DayCell(
else -> DayCellState.NORMAL else -> DayCellState.NORMAL
} }
val transition = updateTransition(targetState = currentState, label = "dayCell") val revealProgress by animateFloatAsState(
targetValue = when (currentState) {
val revealProgress by transition.animateFloat(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
label = "revealProgress"
) { state ->
when (state) {
DayCellState.SELECTED, DayCellState.SELECTED_TODAY -> 1f DayCellState.SELECTED, DayCellState.SELECTED_TODAY -> 1f
else -> 0f else -> 0f
} },
} animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "revealProgress"
)
val contentColor by transition.animateColor( val contentColor by animateColorAsState(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, targetValue = when (currentState) {
label = "contentColor"
) { state ->
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer
DayCellState.SELECTED -> MaterialTheme.colorScheme.primary DayCellState.SELECTED -> MaterialTheme.colorScheme.primary
DayCellState.TODAY -> MaterialTheme.colorScheme.primary DayCellState.TODAY -> MaterialTheme.colorScheme.primary
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface
} },
} animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "contentColor"
)
// 选中今天:实心填充 primaryContainer;其他状态不填充。 // 选中今天:实心填充 primaryContainer;其他状态不填充。
val selectedFillColor by transition.animateColor( val selectedFillColor by animateColorAsState(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, targetValue = when (currentState) {
label = "selectedFillColor"
) { state ->
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.primaryContainer DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.primaryContainer
else -> Color.Transparent else -> Color.Transparent
} },
} animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "selectedFillColor"
)
// 选中非今天:绘制描边圆,避免遮挡右上角角标。 // 选中非今天:绘制描边圆,避免遮挡右上角角标。
val selectedOutlineAlpha by transition.animateFloat( val selectedOutlineAlpha by animateFloatAsState(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, targetValue = when (currentState) {
label = "selectedOutlineAlpha"
) { state ->
when (state) {
DayCellState.SELECTED -> 1f DayCellState.SELECTED -> 1f
else -> 0f else -> 0f
} },
} animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "selectedOutlineAlpha"
)
val selectedOutlineColor = MaterialTheme.colorScheme.primary val selectedOutlineColor = MaterialTheme.colorScheme.primary
val lunarColor by transition.animateColor( val lunarColor by animateColorAsState(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, targetValue = if (isAnnotationHighlight) {
label = "lunarColor" when (currentState) {
) { state ->
if (isAnnotationHighlight) {
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy( DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(
alpha = 0.85f alpha = 0.85f
) )
DayCellState.SELECTED -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) DayCellState.SELECTED -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f)
DayCellState.TODAY -> MaterialTheme.colorScheme.primary DayCellState.TODAY -> MaterialTheme.colorScheme.primary
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.error.copy(alpha = 0.35f) DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.error.copy(alpha = 0.35f)
DayCellState.NORMAL -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) DayCellState.NORMAL -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f)
} }
} else { } else {
when (state) { when (currentState) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy( DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(
alpha = 0.7f alpha = 0.7f
) )
DayCellState.SELECTED -> MaterialTheme.colorScheme.primary.copy(alpha = 0.7f) DayCellState.SELECTED -> MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)
DayCellState.TODAY -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) DayCellState.TODAY -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f) DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f)
DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
} }
} },
} animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "lunarColor"
)
val holidayBadgeColor = when (holidayBadge) { val holidayBadgeColor = when (holidayBadge) {
"" -> MaterialTheme.colorScheme.error "" -> MaterialTheme.colorScheme.error
@ -206,7 +197,7 @@ fun DayCell(
} }
} }
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = interactionSource,
indication = null, indication = null,
onClick = onClick onClick = onClick
), ),

View File

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerDefaults import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -45,6 +46,7 @@ fun WeekPager(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val initialWeekMonday = remember { selectedDate.toWeekMonday() } val initialWeekMonday = remember { selectedDate.toWeekMonday() }
val interactionSource = remember { MutableInteractionSource() }
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = START_PAGE, initialPage = START_PAGE,
pageCount = { Int.MAX_VALUE } pageCount = { Int.MAX_VALUE }
@ -97,7 +99,8 @@ fun WeekPager(
shiftKind = shiftKindAt(date), shiftKind = shiftKindAt(date),
showLegalHoliday = showLegalHoliday, showLegalHoliday = showLegalHoliday,
onClick = { onDateClick(date) }, onClick = { onDateClick(date) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
interactionSource = interactionSource
) )
} }
} }

242
scripts/profile.sh Executable file
View File

@ -0,0 +1,242 @@
#!/bin/bash
#
# YaYa 性能追踪脚本
# 使用 Perfetto 抓取应用 trace保存到 logs/ 目录
#
# 用法:
# ./scripts/profile.sh # 默认抓取 8 秒
# ./scripts/profile.sh 15 # 抓取 15 秒
# ./scripts/profile.sh --no-launch # 不自动启动应用(应用已在运行)
#
set -euo pipefail
PACKAGE="plus.rua.project"
ACTIVITY="plus.rua.project.MainActivity"
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LOGS_DIR="${PROJECT_ROOT}/logs"
# 解析参数
DURATION_SEC=8
NO_LAUNCH=false
for arg in "$@"; do
case "$arg" in
--no-launch)
NO_LAUNCH=true
;;
--help|-h)
echo "用法: $0 [秒数] [--no-launch]"
echo ""
echo "选项:"
echo " 秒数 抓取时长(默认 8 秒)"
echo " --no-launch 不自动启动应用"
echo " --help 显示此帮助"
exit 0
;;
[0-9]*)
DURATION_SEC="$arg"
;;
esac
done
DURATION_MS=$((DURATION_SEC * 1000))
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
echo "========================================"
echo " YaYa 性能追踪"
echo " 包名: ${PACKAGE}"
echo " 时长: ${DURATION_SEC}s"
echo " 输出: ${LOGS_DIR}/"
echo "========================================"
# 创建 logs 目录
mkdir -p "${LOGS_DIR}"
# 检查 adb
if ! command -v adb &>/dev/null; then
echo "错误: adb 未找到。请确保 Android SDK 的 platform-tools 在 PATH 中。"
exit 1
fi
# 检查设备连接
DEVICE_COUNT=$(adb devices | grep -c "device$" || true)
if [ "$DEVICE_COUNT" -eq 0 ]; then
echo "错误: 没有已连接的 Android 设备。"
exit 1
fi
if [ "$DEVICE_COUNT" -gt 1 ]; then
echo "警告: 检测到多个设备,将使用默认设备。"
fi
# 检查应用是否已安装
if ! adb shell pm list packages | grep -q "${PACKAGE}"; then
echo "错误: 应用 ${PACKAGE} 未安装。请先运行 ./gradlew :app:installDebug"
exit 1
fi
# 启动应用(如果未禁用)
if [ "$NO_LAUNCH" = false ]; then
echo ""
echo "[1/5] 启动应用..."
adb shell am start -n "${PACKAGE}/${ACTIVITY}" >/dev/null 2>&1 || true
sleep 2
else
echo ""
echo "[1/5] 跳过启动 (--no-launch)"
fi
# 抓取 Perfetto trace
echo ""
echo "[2/5] 抓取 Perfetto trace (${DURATION_SEC}s)..."
echo " 请在设备上操作应用trace 正在记录..."
TRACE_FILE="/data/misc/perfetto-traces/yaya_${TIMESTAMP}.perfetto-trace"
LOCAL_TRACE="${LOGS_DIR}/trace_${TIMESTAMP}.perfetto-trace"
LOCAL_CONFIG="${LOGS_DIR}/.perfetto_config_${TIMESTAMP}.txt"
DEVICE_CONFIG="/data/misc/perfetto-configs/yaya_config_${TIMESTAMP}.txt"
# 生成本地配置文件,然后 push 到设备
cat > "${LOCAL_CONFIG}" <<EOF
buffers {
size_kb: 131072
fill_policy: RING_BUFFER
}
buffers {
size_kb: 4096
fill_policy: RING_BUFFER
}
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "power/cpu_frequency"
ftrace_events: "power/cpu_idle"
atrace_categories: "gfx"
atrace_categories: "view"
atrace_categories: "wm"
atrace_categories: "am"
atrace_categories: "input"
atrace_categories: "sched"
atrace_categories: "freq"
atrace_categories: "idle"
atrace_apps: "${PACKAGE}"
}
}
}
data_sources {
config {
name: "linux.process_stats"
target_buffer: 1
process_stats_config {
scan_all_processes_on_start: true
}
}
}
duration_ms: ${DURATION_MS}
EOF
adb push "${LOCAL_CONFIG}" "${DEVICE_CONFIG}" > /dev/null
rm -f "${LOCAL_CONFIG}"
# 运行 perfetto前台阻塞直到 duration_ms 结束)
adb shell "perfetto --txt -c ${DEVICE_CONFIG} -o ${TRACE_FILE}"
# 清理设备上的临时配置文件
adb shell "rm -f ${DEVICE_CONFIG}"
# 拉取 trace 文件
echo " 拉取 trace 文件..."
adb pull "${TRACE_FILE}" "${LOCAL_TRACE}"
adb shell "rm -f ${TRACE_FILE}" || true
# 抓取帧统计
echo ""
echo "[3/5] 抓取帧统计..."
FRAMESTATS_FILE="${LOGS_DIR}/framestats_${TIMESTAMP}.txt"
adb shell dumpsys gfxinfo "${PACKAGE}" framestats > "${FRAMESTATS_FILE}"
# 抓取内存信息
echo ""
echo "[4/5] 抓取内存信息..."
MEMINFO_FILE="${LOGS_DIR}/meminfo_${TIMESTAMP}.txt"
adb shell dumpsys meminfo "${PACKAGE}" > "${MEMINFO_FILE}"
# 生成报告摘要
echo ""
echo "[5/5] 生成摘要..."
REPORT_FILE="${LOGS_DIR}/report_${TIMESTAMP}.md"
# 计算帧率相关数据
FRAME_COUNT=$(grep -c "FrameTimeline" "${FRAMESTATS_FILE}" 2>/dev/null || echo "0")
JANK_COUNT=$(grep -c "jank" "${FRAMESTATS_FILE}" 2>/dev/null || echo "0")
# 获取应用版本
APP_VERSION=$(adb shell dumpsys package "${PACKAGE}" | grep versionName | head -1 | awk '{print $1}' | cut -d= -f2 2>/dev/null || echo "unknown")
# 获取设备信息
DEVICE_MODEL=$(adb shell getprop ro.product.model 2>/dev/null | tr -d '\r')
ANDROID_VERSION=$(adb shell getprop ro.build.version.release 2>/dev/null | tr -d '\r')
cat > "${REPORT_FILE}" <<EOF
# YaYa 性能追踪报告
**时间:** $(date '+%Y-%m-%d %H:%M:%S')
**设备:** ${DEVICE_MODEL} (Android ${ANDROID_VERSION})
**应用版本:** ${APP_VERSION}
**追踪时长:** ${DURATION_SEC}s
## 文件清单
| 文件 | 说明 |
|------|------|
| \`trace_${TIMESTAMP}.perfetto-trace\` | Perfetto trace在 https://ui.perfetto.dev 中打开) |
| \`framestats_${TIMESTAMP}.txt\` | GPU 帧统计 |
| \`meminfo_${TIMESTAMP}.txt\` | 内存快照 |
| \`report_${TIMESTAMP}.md\` | 本报告 |
## 快速分析指南
### 1. 查看 Compose 渲染耗时
打开 [Perfetto UI](https://ui.perfetto.dev),上传 trace 文件,搜索:
- \`MonthView:Compose\` — 月视图重组耗时
- \`YearView:Compose\` — 年视图重组耗时
- \`VM:collapseProgress\` — 折叠动画状态更新
- \`getMonthDays\` — 月份网格计算
### 2. 分析帧率
\`framestats_${TIMESTAMP}.txt\` 中查看:
- \`FrameTimeline\` 行 — 每帧的 CPU/GPU 耗时
- \`jank\` 标记 — 掉帧情况
### 3. 内存分析
\`meminfo_${TIMESTAMP}.txt\` 中关注:
- \`TOTAL\` 行 — 应用总内存占用
- \`Graphics\` 行 — GPU 内存使用
- \`Native Heap\` 行 — 原生堆内存
## 帧统计摘要
- FrameTimeline 条目数: ${FRAME_COUNT}
- Jank 标记数: ${JANK_COUNT}
EOF
echo ""
echo "========================================"
echo " 完成!"
echo "========================================"
echo ""
echo "输出文件:"
echo " trace: ${LOCAL_TRACE}"
echo " framestats: ${FRAMESTATS_FILE}"
echo " meminfo: ${MEMINFO_FILE}"
echo " report: ${REPORT_FILE}"
echo ""
echo "下一步:"
echo " 1. 打开 https://ui.perfetto.dev"
echo " 2. 上传 trace_${TIMESTAMP}.perfetto-trace"
echo " 3. 搜索 'MonthView:Compose' 查看自定义标记"
echo ""