diff --git a/scripts/profile.ps1 b/scripts/profile.ps1 new file mode 100644 index 0000000..c8ccade --- /dev/null +++ b/scripts/profile.ps1 @@ -0,0 +1,332 @@ +#requires -Version 5.1 + +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [int]$Duration = 8, + + [switch]$NoLaunch, + [switch]$Trace, + [switch]$Help +) + +if ($Help) { + $helpLines = @( + "用法: .\scripts\profile.ps1 [秒数] [-NoLaunch] [-Trace]" + "" + "选项:" + " 秒数 抓取时长(默认 8 秒)" + " -NoLaunch 不自动启动应用" + " -Trace 使用 trace 构建(release 优化 + 保留 trace 标记)" + " -Help 显示此帮助" + ) + $helpLines | ForEach-Object { Write-Host $_ } + exit 0 +} + +$PACKAGE = "plus.rua.project" +$ACTIVITY = "plus.rua.project.MainActivity" +$PROJECT_ROOT = Split-Path -Parent $PSScriptRoot +$LOGS_DIR = Join-Path $PROJECT_ROOT "logs" + +$DURATION_MS = $Duration * 1000 +$TIMESTAMP = Get-Date -Format "yyyyMMdd_HHmmss" + +Write-Host "========================================" +Write-Host " YaYa 性能追踪" +Write-Host " 包名: $PACKAGE" +Write-Host " 时长: ${Duration}s" +if ($Trace) { + Write-Host " 构建: trace (release + trace)" +} else { + Write-Host " 构建: debug" +} +Write-Host " 输出: $LOGS_DIR/" +Write-Host "========================================" + +New-Item -ItemType Directory -Force -Path $LOGS_DIR | Out-Null + +$adb = Get-Command adb -ErrorAction SilentlyContinue +if (-not $adb) { + Write-Error "adb 未找到。请确保 Android SDK 的 platform-tools 在 PATH 中。" + exit 1 +} + +$deviceLines = & adb devices | Select-String "device$" +$deviceCount = ($deviceLines | Measure-Object).Count +if ($deviceCount -eq 0) { + Write-Error "没有已连接的 Android 设备。" + exit 1 +} +if ($deviceCount -gt 1) { + Write-Warning "检测到多个设备,将使用默认设备。" +} + +$installed = & adb shell pm list packages | Select-String $PACKAGE +if (-not $installed) { + Write-Error "应用 $PACKAGE 未安装。请先运行 .\gradlew :app:installDebug" + if ($Trace) { + Write-Host " 或使用 .\gradlew :app:installTrace 安装 trace 构建" + } + exit 1 +} + +if (-not $NoLaunch) { + Write-Host "" + Write-Host "[1/5] 启动应用..." + & adb shell am start -n "$PACKAGE/$ACTIVITY" 2>$null + Start-Sleep -Seconds 2 +} else { + Write-Host "" + Write-Host "[1/5] 跳过启动 (-NoLaunch)" +} + +Write-Host "" +Write-Host "[2/5] 抓取 Perfetto trace (${Duration}s)..." +Write-Host " 请在设备上操作应用,trace 正在记录..." + +$TRACE_FILE = "/data/misc/perfetto-traces/yaya_${TIMESTAMP}.perfetto-trace" +$LOCAL_TRACE = Join-Path $LOGS_DIR "trace_${TIMESTAMP}.perfetto-trace" +$LOCAL_CONFIG = Join-Path $LOGS_DIR ".perfetto_config_${TIMESTAMP}.txt" +$DEVICE_CONFIG = "/data/misc/perfetto-configs/yaya_config_${TIMESTAMP}.txt" + +$perfettoLines = @( + "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: `"android.gpu.memory`"" + " }" + "}" + "data_sources {" + " config {" + " name: `"android.surfaceflinger.frametimeline`"" + " }" + "}" + "data_sources {" + " config {" + " name: `"linux.process_stats`"" + " target_buffer: 1" + " process_stats_config {" + " scan_all_processes_on_start: true" + " }" + " }" + "}" + "duration_ms: $DURATION_MS" +) +$perfettoConfig = $perfettoLines -join "`r`n" + +$perfettoConfig | Set-Content -Encoding UTF8 -Path $LOCAL_CONFIG + +& adb push "$LOCAL_CONFIG" "$DEVICE_CONFIG" | Out-Null +Remove-Item -Force -Path $LOCAL_CONFIG -ErrorAction SilentlyContinue + +& adb shell "perfetto --txt -c $DEVICE_CONFIG -o $TRACE_FILE" +& adb shell "rm -f $DEVICE_CONFIG" + +Write-Host " 拉取 trace 文件..." +& adb pull "$TRACE_FILE" "$LOCAL_TRACE" +& adb shell "rm -f $TRACE_FILE" 2>$null + +Write-Host "" +Write-Host "[3/5] 抓取帧统计..." +$FRAMESTATS_FILE = Join-Path $LOGS_DIR "framestats_${TIMESTAMP}.txt" +& adb shell dumpsys gfxinfo "$PACKAGE" framestats | Set-Content -Encoding UTF8 -Path $FRAMESTATS_FILE + +Write-Host "" +Write-Host "[4/5] 抓取内存信息..." +$MEMINFO_FILE = Join-Path $LOGS_DIR "meminfo_${TIMESTAMP}.txt" +& adb shell dumpsys meminfo "$PACKAGE" | Set-Content -Encoding UTF8 -Path $MEMINFO_FILE + +Write-Host "" +Write-Host "[5/5] 生成摘要..." +$REPORT_FILE = Join-Path $LOGS_DIR "report_${TIMESTAMP}.md" + +$frameStatsContent = Get-Content -Path $FRAMESTATS_FILE -Raw +$FRAME_COUNT = ([regex]::Matches($frameStatsContent, "FrameTimeline")).Count + +function Get-FirstMatchValue { + param([string]$Pattern, [string]$Content, [int]$CaptureGroup = 1) + $m = [regex]::Match($Content, $Pattern) + if ($m.Success) { $m.Groups[$CaptureGroup].Value.Trim() } else { "" } +} + +$TOTAL_FRAMES = Get-FirstMatchValue "Total frames rendered:\s*(\d+)" $frameStatsContent +$JANKY_FRAMES = Get-FirstMatchValue "Janky frames:\s*(\d+)" $frameStatsContent +$JANKY_PERCENT = Get-FirstMatchValue "Janky frames:.*?\(([^)]+)\)" $frameStatsContent +$P50 = Get-FirstMatchValue "50th percentile:\s*([\d.]+)" $frameStatsContent +$P90 = Get-FirstMatchValue "90th percentile:\s*([\d.]+)" $frameStatsContent +$P99 = Get-FirstMatchValue "99th percentile:\s*([\d.]+)" $frameStatsContent +$SLOW_UI = Get-FirstMatchValue "Number Slow UI thread:\s*(\d+)" $frameStatsContent +$SLOW_DRAW = Get-FirstMatchValue "Number Slow issue draw commands:\s*(\d+)" $frameStatsContent +$HIGH_INPUT = Get-FirstMatchValue "Number High input latency:\s*(\d+)" $frameStatsContent + +$APP_VERSION = (& adb shell dumpsys package "$PACKAGE" | Select-String "versionName" | Select-Object -First 1) -replace ".*versionName=", "" -replace "\s.*", "" -replace "`r", "" +if (-not $APP_VERSION) { $APP_VERSION = "unknown" } + +$DEVICE_MODEL = (& adb shell getprop ro.product.model 2>$null).Trim() +$ANDROID_VERSION = (& adb shell getprop ro.build.version.release 2>$null).Trim() + +$memInfoContent = Get-Content -Path $MEMINFO_FILE -Raw +$TOTAL_PSS = Get-FirstMatchValue "TOTAL PSS:\s*(\d+)" $memInfoContent +$JAVA_HEAP = Get-FirstMatchValue "^\s*Java Heap:\s*(\d+)" $memInfoContent +$NATIVE_HEAP = Get-FirstMatchValue "^\s*Native Heap:\s*(\d+)" $memInfoContent +$GRAPHICS = Get-FirstMatchValue "^\s*Graphics:\s*(\d+)" $memInfoContent + +$buildType = if ($Trace) { "trace" } else { "debug" } +$now = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + +$reportLines = @( + "# YaYa 性能追踪报告" + "" + "**时间:** $now" + "**设备:** $DEVICE_MODEL (Android $ANDROID_VERSION)" + "**应用版本:** $APP_VERSION" + "**构建类型:** $buildType" + "**追踪时长:** ${Duration}s" + "" + "## 文件清单" + "" + "| 文件 | 说明 |" + "|------|-------------|" + "| ``trace_${TIMESTAMP}.perfetto-trace`` | Perfetto trace(在 https://ui.perfetto.dev 中打开) |" + "| ``framestats_${TIMESTAMP}.txt`` | GPU 帧统计 |" + "| ``meminfo_${TIMESTAMP}.txt`` | 内存快照 |" + "| ``report_${TIMESTAMP}.md`` | 本报告 |" + "" + "## 帧率摘要" + "" + "| 指标 | 数值 |" + "|------|------|" + "| 总渲染帧数 | $TOTAL_FRAMES |" + "| 掉帧数 | $JANKY_FRAMES |" + "| 掉帧比例 | $JANKY_PERCENT |" + "| 50th percentile | $P50 |" + "| 90th percentile | $P90 |" + "| 99th percentile | $P99 |" + "| Slow UI thread | $SLOW_UI |" + "| Slow draw commands | $SLOW_DRAW |" + "| High input latency | $HIGH_INPUT |" + "" + "## 内存摘要" + "" + "| 指标 | 数值 (KB) |" + "|------|----------|" + "| Total PSS | $TOTAL_PSS |" + "| Java Heap | $JAVA_HEAP |" + "| Native Heap | $NATIVE_HEAP |" + "| Graphics | $GRAPHICS |" + "" + "## Perfetto 分析指南" + "" + "打开 [Perfetto UI](https://ui.perfetto.dev),上传 trace 文件:" + "" + "### 1. 查看 Compose 自定义标记" + "搜索以下 trace section:" + "" + "**月视图相关:**" + "- ``MonthView:Compose`` — 月视图整体重组" + "- ``CalendarPagerArea`` — 日历分页器区域重组" + "- ``CalendarPager:Page`` — 月视图单页重组" + "- ``WeekPager:Page`` — 周视图单页重组" + "- ``getMonthDays:*`` — 月份网格计算" + "" + "**年视图相关:**" + "- ``YearView:Compose`` — 年视图整体重组" + "- ``YearGridView:*`` — 年视图网格重组" + "- ``generateMiniMonthDays:*`` — 迷你月日期计算" + "- ``YearView:SelectMonth`` — 年视图选择月份" + "" + "**转场动画:**" + "- ``MonthView->YearView`` — 月->年视图切换" + "- ``YearView->MonthView`` — 年->月视图切换" + "- ``VM:collapseProgress`` — 折叠动画状态更新" + "" + "**单日单元格:**" + "- ``DayCell`` — 单个日期单元格(通过 transition label)" + "" + "### 2. 分析帧率" + "在 ``framestats_${TIMESTAMP}.txt`` 中查看:" + "- ``FrameTimeline`` 行 — 每帧的 CPU/GPU 耗时" + "- ``jank`` 标记 — 掉帧情况" + "" + "### 3. 内存分析" + "在 ``meminfo_${TIMESTAMP}.txt`` 中关注:" + "- ``TOTAL`` 行 — 应用总内存占用" + "- ``Graphics`` 行 — GPU 内存使用" + "- ``Native Heap`` 行 — 原生堆内存" + "" + "### 4. 年月视图切换专项分析" + "" + "在 Perfetto 中按以下步骤分析转场性能:" + "" + "1. 找到 ``MonthView->YearView`` 或 ``YearView->MonthView`` 标记" + "2. 查看标记前后 500ms 的帧数据:" + " - 查找超过 16.67ms 的帧(Choreographer#doFrame)" + " - 检查是否有连续多帧超过预算" + "3. 同时搜索 ``MonthView:Compose`` 和 ``YearView:Compose``,观察重组重叠情况" + "4. 查看 ``YearGridView:*`` 的耗时,年视图 12 个月网格的计算和绘制成本" + "" + "## 基线对比方法" + "" + "要对比优化前后的性能:" + "" + "``````powershell" + "# 1. 记录当前数据作为基线" + ".\scripts\profile.ps1 -Trace 15" + "" + "# 2. 修改代码后重新编译" + ".\gradlew :app:installTrace" + "" + "# 3. 再次记录" + ".\scripts\profile.ps1 -Trace 15" + "" + "# 4. 对比两个 report 中的帧率摘要表格" + "``````" +) +$report = $reportLines -join "`r`n" + +$report | Set-Content -Encoding UTF8 -Path $REPORT_FILE + +Write-Host "" +Write-Host "========================================" +Write-Host " 完成!" +Write-Host "========================================" +Write-Host "" +Write-Host "输出文件:" +Write-Host " trace: $LOCAL_TRACE" +Write-Host " framestats: $FRAMESTATS_FILE" +Write-Host " meminfo: $MEMINFO_FILE" +Write-Host " report: $REPORT_FILE" +Write-Host "" +Write-Host "下一步:" +Write-Host " 1. 打开 https://ui.perfetto.dev" +Write-Host " 2. 上传 trace_${TIMESTAMP}.perfetto-trace" +Write-Host " 3. 搜索 'MonthView->YearView' 查看转场 trace" +Write-Host ""