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
```bash

View File

@ -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
)
}
}
}

View File

@ -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)
)
}
}

View File

@ -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
),

View File

@ -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
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 ""