From e0b77003069dfc505e5fb846fd33bd4848b88c5b Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 19 May 2026 00:44:26 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20Perfetto=20?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E6=8E=92=E6=9F=A5=E6=96=87=E6=A1=A3=EF=BC=9B?= =?UTF-8?q?refactor:=20=E7=BC=93=E5=AD=98=20animatable=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DEVELOPMENT.md 新增 Perfetto / Systrace trace 分析指南 - CalendarViewModel.onDrag/onExpandDrag 中缓存 _collapseAnimatable.value 到局部变量,避免在 coroutineScope.launch 闭包中重复读取 - .gitignore 添加 .claude/ 目录 Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + DEVELOPMENT.md | 139 ++++++++++++++++++ .../plus/rua/project/CalendarViewModel.kt | 20 ++- 3 files changed, 152 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index e947f30..4587307 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ node_modules/ # OMC runtime state .omc/ logs/ +.claude/ \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6786acf..cdf5fa0 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -111,3 +111,142 @@ CalendarMonthView (顶层屏幕) **折叠动画:** `CalendarViewModel.collapseProgress` 控制 0f(月)↔1f(周) 过渡。`BottomCard` 捕获垂直拖拽,释放时超过 50% 则弹簧动画吸附到最近状态。完全折叠后 `WeekPager` 替代 `CalendarPager` 实现高效单周分页。 **分页映射:** 两个 Pager 均使用 `Int.MAX_VALUE` 页数,中心页为 `Int.MAX_VALUE / 2`。页码到日期为算术转换,无索引列表。两者均跳过初始 `snapshotFlow` 发射 (`.drop(1)`) 以保留首次渲染时的"今日"选中。 + +## 性能排查(Perfetto / Systrace) + +项目使用 `composeTraceBeginSection` / `composeTraceEndSection` 在关键代码段插入 trace marker,Android 上会被记录到系统 trace 中。iOS 为空操作。 + +已有的 trace section: +- `MonthView:Compose` / `YearView:Compose` — 顶层重组耗时 +- `YearView→MonthView` / `MonthView→YearView` — 年视图切换动画 +- `YearGridView:$year` / `generateMiniMonthDays:$year-$month` — 年网格渲染 +- `getMonthDays:$year-$month` — 月网格数据生成 + +### 分析折叠器卡顿的方法 + +1. **录制 trace**:Android Studio → Profiler → CPU → 选择 "Trace Java Methods" 或命令行: + ```bash + adb shell perfetto -c - --txt \<= len(pkt): break + tag = pkt[po]; po += 1 + fn = tag >> 3; wt = tag & 0x07 + if wt == 0: + _, po = read_varint(pkt, po) + elif wt == 2: + length, po = read_varint(pkt, po) + chunk = pkt[po:po + length] + if fn == 2: + # 扫描 bundle 内的 FtraceEvent (field 1, 0x0a) + eo = 0 + while eo < len(chunk): + if chunk[eo] != 0x0a: + eo += 1; continue + eo += 1 + try: + el, eno = read_varint(chunk, eo) + if el > 0 and eno + el <= len(chunk): + evt = chunk[eno:eno + el] + # 提取 timestamp (field 1, varint) + if len(evt) > 1 and evt[0] == 0x08: + ts, _ = read_varint(evt, 1) + # 搜索 marker 字符串 + for pat in [b'BC:', b'VM:', b'MonthView:']: + idx = evt.find(pat) + if idx >= 0: + me = idx + while me < len(evt) and 32 <= evt[me] < 127: + me += 1 + name = evt[idx:me].decode() + events.append((ts, name)) + break + eo = eno + el + else: + eo = eno + except: + eo += 1 + po += length + elif wt in (1, 5): + po += 8 if wt == 1 else 4 + else: + break + + events.sort() + return events + + # 使用 + events = parse_trace('cpu-perfetto-xxxx.trace') + for ts, name in events: + print(f"{ts}: {name}") + ``` + +3. **关注点**: + - **触摸事件间隔**:统计相邻 `BC:delta` marker 的时间差。理想间隔 ≤16ms;若出现 >33ms 说明丢帧,>100ms 说明触摸断流。 + - **重组耗时**:`VM:collapseProgress` → `MonthView:Compose` 的间隔,应在亚毫秒级。 + - **ViewModel → Compose 延迟**:从 `snapTo` 调用到下一帧重组完成的间隔。 + +### 已知排查结论(2026-05-19) + +对折叠器 trace 的分析显示: +- **重组本身很快**(VM progress → Compose 约 500μs),不是卡顿来源。 +- **触摸事件采样间隔不均匀**是主要问题。某些拖拽序列中出现 30-50ms 的触摸事件间隔,偶尔有 >100ms 的断流。这属于系统/模拟器层的事件分发问题,而非 Compose 代码问题。 +- 若在真机上复现,建议检查是否有 CPU 抢占或手指短暂离屏。 diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index eeb795a..a108c2e 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -19,6 +19,8 @@ import kotlinx.datetime.minus import kotlinx.datetime.number import kotlinx.datetime.plus import kotlinx.datetime.todayIn +import plus.rua.project.composeTraceBeginSection +import plus.rua.project.composeTraceEndSection import plus.rua.project.ui.COLLAPSE_THRESHOLD import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP import kotlin.time.Clock @@ -186,8 +188,9 @@ class CalendarViewModel( * @param delta 拖拽增量,已归一化到 [0,1] 区间 */ fun onDrag(delta: Float) { + val old = _collapseAnimatable.value coroutineScope.launch { - val new = (_collapseAnimatable.value + delta).coerceIn(0f, 1f) + val new = (old + delta).coerceIn(0f, 1f) _collapseAnimatable.snapTo(new) } } @@ -203,9 +206,9 @@ class CalendarViewModel( coroutineScope.launch { val progress = _collapseAnimatable.value val shouldCollapse = when { - velocityDpPerSec > FLING_VELOCITY_THRESHOLD_DP -> true // 快速上滑→折叠 - velocityDpPerSec < -FLING_VELOCITY_THRESHOLD_DP -> false // 快速下滑→展开 - else -> progress > COLLAPSE_THRESHOLD // 慢速按 progress 判断 + velocityDpPerSec > FLING_VELOCITY_THRESHOLD_DP -> true + velocityDpPerSec < -FLING_VELOCITY_THRESHOLD_DP -> false + else -> progress > COLLAPSE_THRESHOLD } if (shouldCollapse) { _collapseAnimatable.animateTo( @@ -228,8 +231,9 @@ class CalendarViewModel( * @param delta 拖拽增量,已归一化到 [0,1] 区间 */ fun onExpandDrag(delta: Float) { + val old = _collapseAnimatable.value coroutineScope.launch { - val new = (_collapseAnimatable.value + delta).coerceIn(0f, 1f) + val new = (old + delta).coerceIn(0f, 1f) _collapseAnimatable.snapTo(new) } } @@ -245,9 +249,9 @@ class CalendarViewModel( coroutineScope.launch { val progress = _collapseAnimatable.value val shouldExpand = when { - velocityDpPerSec < -FLING_VELOCITY_THRESHOLD_DP -> true // 快速下滑→展开 - velocityDpPerSec > FLING_VELOCITY_THRESHOLD_DP -> false // 快速上滑→保持折叠 - else -> progress < 1f - COLLAPSE_THRESHOLD // 慢速按 progress 判断 + velocityDpPerSec < -FLING_VELOCITY_THRESHOLD_DP -> true + velocityDpPerSec > FLING_VELOCITY_THRESHOLD_DP -> false + else -> progress < 1f - COLLAPSE_THRESHOLD } if (shouldExpand) { _collapseAnimatable.animateTo(