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:
parent
fb7e19ddc9
commit
baedf878b4
@ -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
|
||||
|
||||
```bash
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package plus.rua.project.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -62,32 +65,14 @@ fun CalendarMonthPage(
|
||||
generateMonthDays(year, month)
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val weeks = remember(days) { days.chunked(7) }
|
||||
val anchorIndex = remember(weeks, 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 h = rowHeightPx.toFloat()
|
||||
val totalPx = h * (1 + (effectiveWeeks - 1) * (1f - collapseProgress))
|
||||
with(density) { totalPx.toDp() }
|
||||
} else null
|
||||
@ -99,69 +84,122 @@ fun CalendarMonthPage(
|
||||
)
|
||||
) {
|
||||
weeks.forEachIndexed { weekIndex, week ->
|
||||
val isAnchor = hasAnchor && weekIndex == anchorIndex
|
||||
val isAbove = hasAnchor && weekIndex < anchorIndex
|
||||
val isBelow = hasAnchor && weekIndex > anchorIndex
|
||||
|
||||
val yOffsetPx = if (rowHeightPx > 0) {
|
||||
when {
|
||||
!hasAnchor -> weekIndex * h - collapseProgress * weeks.size * 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
|
||||
key(weekIndex) {
|
||||
WeekRow(
|
||||
weekIndex = weekIndex,
|
||||
week = week,
|
||||
anchorIndex = anchorIndex,
|
||||
weeksSize = weeks.size,
|
||||
collapseProgress = collapseProgress,
|
||||
rowHeightPx = rowHeightPx,
|
||||
selectedDate = selectedDate,
|
||||
today = today,
|
||||
shiftKindAt = shiftKindAt,
|
||||
showLegalHoliday = showLegalHoliday,
|
||||
onDateClick = onDateClick,
|
||||
onRowHeightMeasured = onRowHeightMeasured,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
.graphicsLayer {
|
||||
translationY = yOffsetPx
|
||||
alpha = rowAlpha
|
||||
@Composable
|
||||
private fun WeekRow(
|
||||
weekIndex: Int,
|
||||
week: List<DayData>,
|
||||
anchorIndex: Int,
|
||||
weeksSize: Int,
|
||||
collapseProgress: Float,
|
||||
rowHeightPx: Int,
|
||||
selectedDate: LocalDate,
|
||||
today: LocalDate,
|
||||
shiftKindAt: (LocalDate) -> ShiftKind?,
|
||||
showLegalHoliday: Boolean,
|
||||
onDateClick: (LocalDate) -> Unit,
|
||||
onRowHeightMeasured: ((Int) -> Unit)?,
|
||||
interactionSource: MutableInteractionSource,
|
||||
) {
|
||||
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(
|
||||
if (weekIndex == 0 && rowHeightPx == 0) {
|
||||
Modifier.onSizeChanged { size ->
|
||||
if (size.height > 0) {
|
||||
onRowHeightMeasured?.invoke(size.height)
|
||||
}
|
||||
}
|
||||
} else Modifier
|
||||
)
|
||||
.padding(vertical = ROW_PADDING_DP.dp)
|
||||
) {
|
||||
week.forEach { dayData ->
|
||||
DayCell(
|
||||
date = dayData.date,
|
||||
isCurrentMonth = dayData.isCurrentMonth,
|
||||
isSelected = dayData.date == selectedDate,
|
||||
isToday = dayData.date == today,
|
||||
shiftKind = shiftKindAt(dayData.date),
|
||||
showLegalHoliday = showLegalHoliday,
|
||||
onClick = { onDateClick(dayData.date) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else Modifier
|
||||
)
|
||||
.padding(vertical = ROW_PADDING_DP.dp)
|
||||
) {
|
||||
week.forEach { dayData ->
|
||||
DayCell(
|
||||
date = dayData.date,
|
||||
isCurrentMonth = dayData.isCurrentMonth,
|
||||
isSelected = dayData.date == selectedDate,
|
||||
isToday = dayData.date == today,
|
||||
shiftKind = shiftKindAt(dayData.date),
|
||||
showLegalHoliday = showLegalHoliday,
|
||||
onClick = { onDateClick(dayData.date) },
|
||||
modifier = Modifier.weight(1f),
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,6 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@ -558,10 +557,9 @@ private fun BottomCardArea(
|
||||
onExpandDrag = { delta -> viewModel.onExpandDrag(delta) },
|
||||
onExpandDragEnd = { velocity -> viewModel.onExpandDragEnd(velocity) },
|
||||
dragRangePx = dragRangePx,
|
||||
modifier = modifier.graphicsLayer {
|
||||
translationY = slideProgress * 200.dp.toPx()
|
||||
alpha = 1f - slideProgress
|
||||
}
|
||||
modifier = modifier
|
||||
.offset(y = with(density) { (slideProgress * 200).dp })
|
||||
.alpha(1f - slideProgress)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
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.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@ -69,6 +68,7 @@ fun DayCell(
|
||||
showLegalHoliday: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
lunarCache: LunarCache = LunarCache.default
|
||||
) {
|
||||
val lunarData by produceState(
|
||||
@ -89,83 +89,74 @@ fun DayCell(
|
||||
else -> DayCellState.NORMAL
|
||||
}
|
||||
|
||||
val transition = updateTransition(targetState = currentState, label = "dayCell")
|
||||
|
||||
val revealProgress by transition.animateFloat(
|
||||
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
|
||||
label = "revealProgress"
|
||||
) { state ->
|
||||
when (state) {
|
||||
val revealProgress by animateFloatAsState(
|
||||
targetValue = when (currentState) {
|
||||
DayCellState.SELECTED, DayCellState.SELECTED_TODAY -> 1f
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
},
|
||||
animationSpec = tween(150, easing = FastOutSlowInEasing),
|
||||
label = "revealProgress"
|
||||
)
|
||||
|
||||
val contentColor by transition.animateColor(
|
||||
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
|
||||
label = "contentColor"
|
||||
) { state ->
|
||||
when (state) {
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = when (currentState) {
|
||||
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||
DayCellState.SELECTED -> MaterialTheme.colorScheme.primary
|
||||
DayCellState.TODAY -> MaterialTheme.colorScheme.primary
|
||||
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
}
|
||||
},
|
||||
animationSpec = tween(150, easing = FastOutSlowInEasing),
|
||||
label = "contentColor"
|
||||
)
|
||||
|
||||
// 选中今天:实心填充 primaryContainer;其他状态不填充。
|
||||
val selectedFillColor by transition.animateColor(
|
||||
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
|
||||
label = "selectedFillColor"
|
||||
) { state ->
|
||||
when (state) {
|
||||
val selectedFillColor by animateColorAsState(
|
||||
targetValue = when (currentState) {
|
||||
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> Color.Transparent
|
||||
}
|
||||
}
|
||||
},
|
||||
animationSpec = tween(150, easing = FastOutSlowInEasing),
|
||||
label = "selectedFillColor"
|
||||
)
|
||||
|
||||
// 选中非今天:绘制描边圆,避免遮挡右上角角标。
|
||||
val selectedOutlineAlpha by transition.animateFloat(
|
||||
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
|
||||
label = "selectedOutlineAlpha"
|
||||
) { state ->
|
||||
when (state) {
|
||||
val selectedOutlineAlpha by animateFloatAsState(
|
||||
targetValue = when (currentState) {
|
||||
DayCellState.SELECTED -> 1f
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
},
|
||||
animationSpec = tween(150, easing = FastOutSlowInEasing),
|
||||
label = "selectedOutlineAlpha"
|
||||
)
|
||||
|
||||
val selectedOutlineColor = MaterialTheme.colorScheme.primary
|
||||
|
||||
val lunarColor by transition.animateColor(
|
||||
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
|
||||
label = "lunarColor"
|
||||
) { state ->
|
||||
if (isAnnotationHighlight) {
|
||||
when (state) {
|
||||
val lunarColor by animateColorAsState(
|
||||
targetValue = if (isAnnotationHighlight) {
|
||||
when (currentState) {
|
||||
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(
|
||||
alpha = 0.85f
|
||||
)
|
||||
|
||||
DayCellState.SELECTED -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f)
|
||||
DayCellState.TODAY -> MaterialTheme.colorScheme.primary
|
||||
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.error.copy(alpha = 0.35f)
|
||||
DayCellState.NORMAL -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f)
|
||||
}
|
||||
} else {
|
||||
when (state) {
|
||||
when (currentState) {
|
||||
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(
|
||||
alpha = 0.7f
|
||||
)
|
||||
|
||||
DayCellState.SELECTED -> MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)
|
||||
DayCellState.TODAY -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f)
|
||||
DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
animationSpec = tween(150, easing = FastOutSlowInEasing),
|
||||
label = "lunarColor"
|
||||
)
|
||||
|
||||
val holidayBadgeColor = when (holidayBadge) {
|
||||
"休" -> MaterialTheme.colorScheme.error
|
||||
@ -206,7 +197,7 @@ fun DayCell(
|
||||
}
|
||||
}
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
onClick = onClick
|
||||
),
|
||||
|
||||
@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerDefaults
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -45,6 +46,7 @@ fun WeekPager(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val initialWeekMonday = remember { selectedDate.toWeekMonday() }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = START_PAGE,
|
||||
pageCount = { Int.MAX_VALUE }
|
||||
@ -97,7 +99,8 @@ fun WeekPager(
|
||||
shiftKind = shiftKindAt(date),
|
||||
showLegalHoliday = showLegalHoliday,
|
||||
onClick = { onDateClick(date) },
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.weight(1f),
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
242
scripts/profile.sh
Executable file
242
scripts/profile.sh
Executable 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 ""
|
||||
Loading…
x
Reference in New Issue
Block a user