diff --git a/.gitignore b/.gitignore index 14da847..79a25b2 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ reports/ platform/android/.gradle/ platform/android/build/ platform/android/.idea/ +perfetto-build/ \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c94edc1..acc420d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -83,7 +83,20 @@ smartinspector/ │ │ └── deterministic.py # Deterministic pre-computation (reduces LLM tokens) │ │ │ ├── collector/ # Data collection & processing -│ │ └── perfetto.py # PerfettoCollector: adb collect → SQL query → JSON (CPU调用链, 系统级CPU, WS+SQL合并, context manager) +│ │ ├── perfetto.py # PerfettoCollector: adb collect → SQL query → JSON (CPU调用链, 系统级CPU, WS+SQL合并, context manager) +│ │ ├── frame.py # FrameMixin: per-frame metrics (overrun, cpu_time, ui_time, vsync_delay, jank) +│ │ ├── startup.py # StartupMixin: TTID/TTFD startup metrics + bottleneck breakdown +│ │ ├── memory.py # MemoryMixin: heap graph stats, class aggregation, dominator tree +│ │ ├── lock.py # LockMixin: Java monitor lock contention analysis +│ │ ├── binder.py # BinderMixin: binder transactions + latency breakdown +│ │ ├── gc.py # GcMixin: garbage collection event analysis +│ │ ├── anr.py # AnrMixin: ANR detection + main-thread slice analysis +│ │ ├── slice_enhanced.py # SliceEnhancedMixin: SI$ slice CPU time + thread state distribution +│ │ ├── input.py # InputMixin: input event latency breakdown (dispatch/handling/ACK) +│ │ ├── sched_latency.py # SchedLatencyMixin: scheduling latency per thread +│ │ ├── oom.py # OomMixin: OOM score transitions + RSS/Swap + LMK events +│ │ ├── cpu_utilization.py # CpuUtilizationMixin: frequency-weighted CPU utilization (process/thread) +│ │ └── surfaceflinger.py # SurfaceFlingerMixin: App-SF frame timeline matching │ │ │ ├── commands/ # Slash command implementations │ │ ├── __init__.py # Command registry (SLASH_COMMANDS dict + handle_slash_command) @@ -431,6 +444,39 @@ PerfSummary | `collect_threads()` | `thread` | Thread listing | | `collect_sys_stats()` | `sys_stats` | System-level CPU metrics | +### Stdlib Analysis Modules (Mixin 模式) + +所有模块以 Mixin 形式挂载到 `PerfettoCollector`,通过 `INCLUDE PERFETTO MODULE` 引入 Perfetto 标准库。每个 Mixin 依赖宿主提供 `self._open()` (返回 TraceProcessor) 和 `self._target_package`。 + +#### P0 — 核心模块 + +| Mixin | 方法 | Perfetto Stdlib (`INCLUDE`) | 采集数据 | 解决的性能问题 | +|-------|------|---------------------------|---------|---------------| +| `FrameMixin` | `collect_frame_metrics()` | `android.frames.per_frame_metrics`
`android.frames.timeline` | 每帧行为指标:overrun、cpu_time、ui_time、vsync_delay、jank 分类 (was_jank/was_slow_frame/was_big_jank/was_huge_jank),Top 30 最差帧 | 定位 UI 卡顿根因:区分 CPU 耗时过高、UI 线程慢、VSYNC 延迟等不同类型的帧超时 | +| `StartupMixin` | `collect_startup_metrics()` | `android.startup.startups`
`android.startup.time_to_display` | 应用启动事件:TTID (Time To Initial Display)、TTFD (Time To Full Display)、启动类型 (cold/warm/hot) | 量化启动性能:精确定位每次冷启动/暖启动的首次绘制时间和全屏展示时间 | +| `StartupMixin` | `collect_startup_breakdown()` | `android.startup.startups`
`android.startup.startup_breakdowns` | 启动瓶颈分解:每段耗时的 reason (binder/io/cpu/lock 等),Top 50 最长段 | 分解启动耗时归因:识别启动期间是 binder 调用、磁盘 IO、CPU 计算还是锁竞争占用了时间 | +| `MemoryMixin` | `collect_heap_graph_stats()` | `android.memory.heap_graph.heap_graph_stats` | 堆图摘要:总/可达对象数量、堆大小、Native 分配、OOM score、RSS/Swap、dmabuf | 内存用量概览:评估应用整体内存健康度,OOM 风险判断 | +| `MemoryMixin` | `collect_heap_class_aggregation()` | `android.memory.heap_graph.heap_graph_class_aggregation` | 按类聚合堆内存 Top 20:对象数量、self size、reachable size、dominated size、native size | 定位内存大户:找出哪些类占用了最多堆空间,区分可达 vs 不可达对象 | +| `MemoryMixin` | `collect_heap_dominator_tree()` | `android.memory.heap_graph.dominator_tree` | 堆支配树 Top 50:对象 ID、immediate dominator、dominated set 大小、retained size、深度 | 追踪内存泄漏:通过支配树找到 retain 最多内存的对象链路,定位泄漏根节点 | + +#### P1 — 增强模块 + +| Mixin | 方法 | Perfetto Stdlib (`INCLUDE`) | 采集数据 | 解决的性能问题 | +|-------|------|---------------------------|---------|---------------| +| `LockMixin` | `collect_lock_contention()` | `android.monitor_contention` | Java 锁竞争事件 Top 20:blocked/blocking 方法、线程名、是否主线程、等待者数量 + 阻塞线程状态分解 | 定位主线程阻塞:识别导致主线程等待的锁竞争,区分 IO 等待 vs CPU 等待 | +| `BinderMixin` | `collect_binder_txns()` | `android.binder` | 同步 Binder 事务 Top 30:client/server 进程线程、AIDL 方法名、是否主线程、client/server 耗时 | 定位跨进程调用瓶颈:找出最耗时的 IPC 调用,识别主线程上的同步 binder 等待 | +| `BinderMixin` | `collect_binder_breakdown()` | `android.binder`
`android.binder_breakdown` | Binder 延迟分解 Top 50:client/server 侧的 reason (flush/sched/wait 等)、reason_type | 深入分析 Binder 延迟:区分 binder 调用中的调度延迟、flush 等待、事务执行等各阶段耗时 | +| `GcMixin` | `collect_garbage_collection()` | `android.garbage_collection` | GC 事件 Top 20:wall duration、CPU 时间分解 (running/runnable/io_wait/non_io_wait)、GC 类型、reclaimed 大小、heap 范围 | 评估 GC 影响:识别 GC Stop-The-World 对主线程的实际影响,区分 concurrent vs stop-the-world GC | +| `AnrMixin` | `collect_anrs()` | `android.anrs` | ANR 事件:进程/组件、ANR 类型、subject、timer_delay + ANR 窗口内主线程 Top 10 切片 | ANR 根因分析:识别 ANR 发生时主线程正在执行的耗时操作 | +| `SliceEnhancedMixin` | `collect_slice_cpu_time()` | `slices.cpu_time` | SI$ 切片真实 CPU 时间 Top 20:cpu_time、total_dur、cpu_ratio、线程名 | 区分真实计算 vs 等待:切片耗时长不代表 CPU 慢,cpu_ratio 低说明瓶颈在 IO/锁/调度而非计算 | +| `SliceEnhancedMixin` | `collect_slice_time_in_state()` | `slices.time_in_state`
`sched.states` | SI$ 切片线程状态分布 Top 10:Running/Sleeping/Runnable/IO Wait 等状态占比、blocked_function | 诊断切片耗时根因:识别切片时间花在了 CPU 运行、IO 等待、调度等待还是休眠上 | +| `InputMixin` | `collect_input_latency()` | `android.input` | 输入事件延迟 Top 20:dispatch/handling/ACK 三阶段耗时、end-to-end 延迟、事件类型 | 追踪触控响应延迟:区分输入事件在 dispatch、处理、ACK 哪个阶段耗时最长 | +| `SchedLatencyMixin` | `collect_sched_latency()` | `sched.latency` | 线程调度延迟 Top 20:per-thread 的 runnable→running 等待次数、总/平均/最大等待时间 | 识别调度瓶颈:线程 ready 后多久才被 CPU 执行,高调度延迟意味着 CPU 资源竞争激烈 | +| `OomMixin` | `collect_oom_rss_swap()` | `android.memory.process`
`android.memory.lmk` | OOM score 变迁 + 内存快照 (anon_rss/file_rss/shmem_rss/swap) + LMK kill 事件 | 内存压力分析:追踪进程 OOM score 变化与内存占用的关系,识别被 LMK 杀死的进程 | +| `CpuUtilizationMixin` | `collect_process_cpu_utilization()` | `linux.cpu.utilization.process` | 进程级 CPU 利用率:频率加权 millicycles/megacycles、runtime、min/max/avg freq、per-second utilization | 精确 CPU 利用率:传统 CPU 使用率不含频率信息,频率加权利用率更准确反映真实 CPU 消耗 | +| `CpuUtilizationMixin` | `collect_thread_cpu_utilization()` | `linux.cpu.utilization.thread` | 线程级 CPU 利用率 Top 15:per-thread 的 millicycles/megacycles、频率信息、per-second utilization | 定位 CPU 热线程:找出哪些线程消耗了最多 CPU 周期,结合频率信息判断是否因低频运行导致性能差 | +| `SurfaceFlingerMixin` | `collect_surfaceflinger_timeline()` | `android.surfaceflinger` | App-SF 帧时间线匹配 Top 200:app/SF 帧时间戳、耗时、期望 deadline、匹配类型 (on_time/late) | 端到端帧延迟分析:从 App 渲染到 SurfaceFlinger 合成的完整链路,识别帧是在 App 端还是 SF 端延迟 | + **Perfetto config**: - Default categories: sched, freq, idle, power, memreclaim, gfx, view, input, dalvik, am, wm - `atrace_apps: "*"` — captures app-level atrace markers @@ -685,7 +731,27 @@ User: "全面分析列表滑动性能" │ ├─ collect_frame_timeline() │ ├─ collect_memory() │ ├─ collect_view_slices() ← SI$ prefix filtering, rv_instances grouping - │ └─ collect_block_events() ← WS 结构化 JSON + SQL atrace 合并(非覆盖) + │ ├─ collect_block_events() ← WS 结构化 JSON + SQL atrace 合并(非覆盖) + │ └─ Stdlib Mixin modules (via INCLUDE PERFETTO MODULE): + │ ├─ collect_frame_metrics() ← android.frames.* + │ ├─ collect_startup_metrics() ← android.startup.* + │ ├─ collect_startup_breakdown() ← android.startup.startup_breakdowns + │ ├─ collect_heap_graph_stats() ← android.memory.heap_graph.heap_graph_stats + │ ├─ collect_heap_class_aggregation() ← android.memory.heap_graph.heap_graph_class_aggregation + │ ├─ collect_heap_dominator_tree() ← android.memory.heap_graph.dominator_tree + │ ├─ collect_lock_contention() ← android.monitor_contention + │ ├─ collect_binder_txns() ← android.binder + │ ├─ collect_binder_breakdown() ← android.binder + android.binder_breakdown + │ ├─ collect_garbage_collection() ← android.garbage_collection + │ ├─ collect_anrs() ← android.anrs + │ ├─ collect_slice_cpu_time() ← slices.cpu_time + │ ├─ collect_slice_time_in_state() ← slices.time_in_state + sched.states + │ ├─ collect_input_latency() ← android.input + │ ├─ collect_sched_latency() ← sched.latency + │ ├─ collect_oom_rss_swap() ← android.memory.process + android.memory.lmk + │ ├─ collect_process_cpu_utilization() ← linux.cpu.utilization.process + │ ├─ collect_thread_cpu_utilization() ← linux.cpu.utilization.thread + │ └─ collect_surfaceflinger_timeline() ← android.surfaceflinger └─ State: perf_summary = "{...json...}", _trace_path = "/tmp/xxx.pb" │ ▼ diff --git a/CLAUDE.md b/CLAUDE.md index 8b13789..279d536 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,49 @@ +# SmartInspector - Project Instructions +## Collector 模块 (Perfetto SQL Stdlib 集成) + +采集层位于 `src/smartinspector/collector/`,所有模块以 Mixin 形式挂载到 `PerfettoCollector`。每个 Mixin 依赖宿主提供 `self._open()` (返回 TraceProcessor) 和 `self._target_package`。 + +### 基础模块 + +| 文件 | 类 | 说明 | +|------|-----|------| +| `perfetto.py` | `PerfettoCollector` | 核心采集器 (adb → SQL → JSON),提供 `_open()` / `_target_package`,所有 Mixin 的宿主 | + +### Stdlib 分析模块 (P0 — 核心) + +| 文件 | Mixin | 方法 | Perfetto Stdlib | +|------|-------|------|-----------------| +| `frame.py` | `FrameMixin` | `collect_frame_metrics()` — 帧指标 (overrun, cpu_time, ui_time, vsync_delay, jank) | `android.frames.per_frame_metrics`, `android.frames.timeline` | +| `startup.py` | `StartupMixin` | `collect_startup_metrics()` — TTID/TTFD 启动耗时 | `android.startup.startups`, `android.startup.time_to_display` | +| `startup.py` | `StartupMixin` | `collect_startup_breakdown()` — 启动瓶颈分解 | `android.startup.startup_breakdowns` | +| `memory.py` | `MemoryMixin` | `collect_heap_graph_stats()` — 堆图摘要统计 | `android.memory.heap_graph.heap_graph_stats` | +| `memory.py` | `MemoryMixin` | `collect_heap_class_aggregation()` — 按类聚合堆内存 (Top 20) | `android.memory.heap_graph.heap_graph_class_aggregation` | +| `memory.py` | `MemoryMixin` | `collect_heap_dominator_tree()` — 堆支配树 (最大 retained size) | `android.memory.heap_graph.dominator_tree` | + +### Stdlib 分析模块 (P1 — 增强) + +| 文件 | Mixin | 方法 | Perfetto Stdlib | +|------|-------|------|-----------------| +| `lock.py` | `LockMixin` | `collect_lock_contention()` — Java 锁竞争分析 | `android.monitor_contention` | +| `binder.py` | `BinderMixin` | `collect_binder_txns()` — Binder 事务 Top 30 | `android.binder` | +| `binder.py` | `BinderMixin` | `collect_binder_breakdown()` — Binder 延迟分解 | `android.binder`, `android.binder_breakdown` | +| `gc.py` | `GcMixin` | `collect_garbage_collection()` — GC 事件分析 | `android.garbage_collection` | +| `anr.py` | `AnrMixin` | `collect_anrs()` — ANR 检测与分析 | `android.anrs` | +| `slice_enhanced.py` | `SliceEnhancedMixin` | `collect_slice_cpu_time()` — SI$ slice 真实 CPU 耗时 | `slices.cpu_time` | +| `slice_enhanced.py` | `SliceEnhancedMixin` | `collect_slice_time_in_state()` — SI$ slice 线程状态分布 | `slices.time_in_state`, `sched.states` | +| `input.py` | `InputMixin` | `collect_input_latency()` — 输入事件延迟分解 | `android.input` | +| `sched_latency.py` | `SchedLatencyMixin` | `collect_sched_latency()` — 调度延迟分析 | `sched.latency` | +| `oom.py` | `OomMixin` | `collect_oom_rss_swap()` — OOM score 转换 + RSS/Swap + LMK 事件 | `android.memory.process`, `android.memory.lmk` | +| `cpu_utilization.py` | `CpuUtilizationMixin` | `collect_process_cpu_utilization()` — 进程级 CPU 利用率 | `linux.cpu.utilization.process` | +| `cpu_utilization.py` | `CpuUtilizationMixin` | `collect_thread_cpu_utilization()` — 线程级 CPU 利用率 | `linux.cpu.utilization.thread` | +| `surfaceflinger.py` | `SurfaceFlingerMixin` | `collect_surfaceflinger_timeline()` — App-SF 帧时间线匹配 | `android.surfaceflinger` | + +### 添加新 Stdlib 模块的约定 + +1. 在 `src/smartinspector/collector/` 下创建新文件,以 Mixin 模式实现 +2. 类名遵循 `{Feature}Mixin` 命名 +3. 方法名遵循 `collect_{metric_name}()` 命名,返回 `list[dict]` 或 `dict` +4. SQL 查询中使用 `INCLUDE PERFETTO MODULE {module_path};` 引入 stdlib +5. 在 `PerfettoCollector` 中通过多重继承挂载 Mixin +6. 查询结果按耗时/大小降序排列,使用 `LIMIT` 控制返回数量 diff --git a/README.md b/README.md index 480ac3d..d4b93d0 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,40 @@ AI 驱动的跨平台移动端性能分析 CLI 工具。通过自然语言交互 - 🧠 **自然语言交互** — 用中文描述性能问题,AI 自动路由到对应分析流程 - 📊 **全量分析流水线** — 自动采集 → 分析 → 源码归因 → 报告生成 -- 🔍 **SI$ 源码归因** — 通过 TraceHook tag 将性能热点精确归因到源码位置 +- 🔍 **SI$ 源码归因** — 通过 TraceHook tag 将性能热点精确归因到源码位置(含 IO 切片归因) - 🛡️ **健壮性保障** — 全链路异常处理,Agent 崩溃不丢会话状态 -- ⚡ **Token 效率优化** — 消息窗口裁剪、路由 token 限制、流式输出 +- ⚡ **Token 效率优化** — SQL 结果智能压缩(统计摘要+异常采样)、消息窗口裁剪、路由 token 限制、流式输出 +- ✅ **分析质量验证** — L1 格式检查 + L2 一致性验证,自动检测 LLM 输出遗漏和不一致,支持重试补充 - 🔒 **Release 零开销** — Release 变体为纯 no-op stubs,编译器内联后零运行时开销 - 💬 **实时通信** — WebSocket CLI↔App 双向通信,支持心跳检测和断线重连 +- 🖥️ **Perfetto UI 交互** — 自托管 Perfetto UI + SI Bridge 插件,框选时间范围即可 AI 分析 - ⌨️ **交互增强** — Tab 补全、全局异常保护、启动前置条件检查 +- 🚀 **冷启动分析** — 自动识别启动阶段(进程启动→Application.onCreate→Activity.onCreate→首帧),定位启动瓶颈 +- 🤖 **Headless/CI 模式** — 非交互式运行全量分析流水线,支持 JSON 结构化输出,可直接集成 CI/CD +- 🌐 **IO 追踪** — 默认启用网络/数据库/图片加载 IO Hook,独立收集 IO 切片并归因到源码 + +### Stdlib 分析能力 + +基于 Perfetto SQL Stdlib 的深度性能分析模块,以 Mixin 形式挂载到 PerfettoCollector: + +**P0 核心能力** + +- 🔒 **Java 锁竞争分析** — 检测 synchronized 锁阻塞,定位阻塞方法和源码位置 +- 📡 **Binder 事务分析** — IPC 延迟分解,client/server 端追踪 +- 🚀 **启动分析增强** — TTID/TTFD 指标、启动瓶颈自动分解 +- ♻️ **GC 分析** — GC pause 检测、回收量统计、线程状态分解 +- ⚠️ **ANR 分析** — ANR 事件检测与期间主线程 slice 分析 +- ⏱️ **Slice CPU 增强** — Slice 级精确 CPU 时间和线程状态分布 + +**P1 场景增强** + +- 📊 **逐帧指标** — per-frame overrun/cpu_time/ui_time/jank 分级 +- 👆 **输入延迟分解** — dispatch→delivery→ACK 全链路延迟 +- ⏳ **调度延迟** — Runnable→Running 等待时间统计 +- 🧠 **OOM+RSS 追踪** — OOM 分数、RSS/Swap 监控、LMK kill 事件 +- 💻 **精确 CPU 利用率** — 进程/线程级频率加权 CPU 利用率 +- 📦 **堆分析增强** — 堆统计摘要、按类聚合 Top 20、支配树最大 retained size +- 🖥️ **SF 帧匹配** — App 与 SurfaceFlinger 帧时间线关联 ## 快速开始 @@ -26,12 +54,34 @@ cp .env.example .env # 编辑 .env: SI_API_KEY=your-api-key # 启动 CLI(自动检查 adb/API key,启动 WS server + adb reverse) -uv run smartinspector --source-dir /path/to/your/app/source +uv run smartinspector --src /path/to/your/app/source + +# adb连接手机 # 交互式使用(支持 Tab 补全 slash 命令) -you> 全面分析列表滑动性能 -you> 采集一个 10s trace 分析卡顿 -you> 搜索源码中 LazyForEach 的用法 +# 自然语言开启采集和分析 +you> 分析冷启动耗时 +# 指令开启采集分析 +you> /full +# 冷启动分析(跳过等待,直接开始采集) +you> /full --no-wait +# 打开perfetto ui +you> /open +``` + +### CI/Headless 模式 + +非交互式运行全量分析流水线,适合 CI/CD 集成: + +```bash +# 分析已有 trace 文件,输出 JSON 报告到 stdout +uv run smartinspector --ci --trace trace.pb --format json --src ./app/src + +# 从设备采集 trace 并生成 Markdown 报告到文件 +uv run smartinspector --ci --target com.example.app --duration 5000 --output report.md + +# JSON 格式输出示例(适合自动化解析) +uv run smartinspector --ci --trace trace.pb --format json | jq '.issues[] | select(.severity == "P0")' ``` ## 架构概览 @@ -48,9 +98,55 @@ you> 搜索源码中 LazyForEach 的用法 全量分析流水线(LangGraph 图节点编排): ``` -collector (设备 trace 采集) → analyzer (LLM 性能解读) → attributor (源码归因) → reporter (生成 Markdown 报告) +collector (设备 trace 采集) → analyzer (LLM 性能解读) → attributor (源码归因) → reporter (生成 Markdown/JSON 报告) + ↓ + startup (冷启动分析,阶段切分 + 瓶颈识别) ``` +### Perfetto UI 交互分析 + +``` +用户在 Perfetto UI 框选时间范围 + → SI Bridge Plugin (WebSocket client) + → BridgeServer (ws://127.0.0.1:9877/bridge) + → frame_analyzer agent (查询切片 → 源码归因 → LLM 分析) + → 结果回传 Perfetto UI 展示(实时进度 + Markdown 报告) +``` + +

+ Perfetto UI 交互帧分析 +

+ +使用 `/open` 启动自托管 Perfetto UI 后,在时间轴上拖选一段范围,点击右侧 **SI Frame Analysis** 面板中的 **Analyze with SI Agent** 按钮。分析过程中实时显示查询进度、源码归因工具调用(Glob/Grep/Read)和 LLM 分析状态,最终在面板中展示 Markdown 格式的帧分析报告。 + +- `/frame ts=X dur=Y` CLI 直接分析指定时间范围 +- 插件自动重连,进度实时推送,归因过程透明可见 + +### 构建 Perfetto UI 插件 + +使用 `perfetto-plugin/build.sh` 构建包含 SI Bridge 插件的自托管 Perfetto UI: + +**前置条件:** Node.js >= 18、npm、git + +```bash +# 首次构建(clone Perfetto + 复制插件 + 编译) +./perfetto-plugin/build.sh + +# 后续构建(跳过 clone,仅重新编译) +./perfetto-plugin/build.sh --skip-clone +``` + +构建脚本会自动完成以下步骤: + +1. Clone Perfetto 仓库(shallow clone)到 `perfetto-build/` +2. 复制 SI Bridge 插件到 Perfetto 插件目录 +3. 在 `default_plugins.ts` 中注册插件 +4. 执行 `ui/build` 编译(含依赖安装、TypeScript 编译、WASM) + +构建产物输出到 `perfetto-build/ui/out/dist/`,可通过 `/open` 命令启动自托管 Perfetto UI。 + +> **注意:** 脚本会自动移除 PATH 中的 Android NDK `strip` 以避免 macOS 上 Mach-O arm64 兼容性问题。如需代理,请提前设置 `http_proxy`/`https_proxy`。 + ### 健壮性设计 全链路异常处理,确保单节点失败不影响整体会话: @@ -66,11 +162,13 @@ REPL 主循环 ─── 全局 try/except,异常后保留 state 继续输入 ## 平台支持 -| 平台 | 状态 | Trace 采集 | 方法 Hook | 源码归因 | -|------|------|-----------|----------|---------| -| Android | **已实现** | Perfetto + adb | Pine AOP | 支持 | -| HarmonyOS | 规划中 | hdc + hiperf/hitrace | — | — | -| iOS | 规划中 | Instruments + Xcode | — | — | + +| 平台 | 状态 | Trace 采集 | 方法 Hook | 源码归因 | +| --------- | ------- | -------------------- | -------- | ---- | +| Android | **已实现** | Perfetto + adb | Pine AOP | 支持 | +| HarmonyOS | 规划中 | hdc + hiperf/hitrace | — | — | +| iOS | 规划中 | Instruments + Xcode | — | — | + ### Android @@ -114,16 +212,23 @@ smartinspector/ │ │ ├── __init__.py # reporter_node 入口 (流式输出) │ │ ├── generator.py # LLM 报告生成 (流式+重试) │ │ ├── formatter.py # 数据格式化 (perf+归因→Markdown) +│ │ ├── json_formatter.py # JSON 结构化报告格式化 │ │ └── persistence.py # 报告文件保存 │ │ │ ├── agents/ # Agent 定义 (LLM + Tools) │ │ ├── android.py # Android Expert Agent │ │ ├── explorer.py # Code Explorer Agent -│ │ ├── perf_analyzer.py # Perf Analyzer (单次 LLM 调用) +│ │ ├── perf_analyzer.py # Perf Analyzer (单次 LLM 调用 + 验证重试) │ │ ├── attributor.py # 源码归因 Agent (run_attribution) -│ │ └── deterministic.py # 确定性预计算 (减少 LLM token) +│ │ ├── frame_analyzer.py # 帧分析 Agent (Perfetto UI 交互归因) +│ │ ├── deterministic.py # 确定性预计算 + SQL Summarizer (减少 LLM token) +│ │ └── verifier.py # 分析质量验证 (L1 格式 + L2 一致性, 0 token) │ │ │ ├── collector/perfetto.py # PerfettoCollector (adb→SQL→JSON, CPU调用链, 系统级CPU, context manager) +│ ├── collector/startup.py # 冷启动分析器 (启动阶段切分, 关键路径提取, 瓶颈识别) +│ ├── collector/memory.py # 内存分配分析器 (heap_graph, 泄漏检测, 内存趋势) +│ ├── headless.py # Headless/CI 非交互式运行器 (全量流水线, JSON/Markdown 输出) +│ ├── storage/store.py # 报告存储层 (基线管理, 历史查询, 对比数据源) │ ├── commands/ # Slash 命令 (注册表模式) │ │ ├── __init__.py # 命令注册表 (handle_slash_command) │ │ ├── attribution.py # SI$ tag 解析 + 归因提取 @@ -131,11 +236,12 @@ smartinspector/ │ │ ├── hook.py # Hook 配置 (/config, /hooks) │ │ ├── orchestrate.py # 编排命令 (/full, /report) │ │ ├── session.py # 会话管理 (/help, /clear) -│ │ └── trace.py # Trace 采集 (/trace, /record) +│ │ └── trace.py # Trace 采集 (/trace, /record, /open, /close, /frame) │ │ │ ├── tools/ # LangChain 工具 (grep/glob/read/perfetto) │ │ └── path_utils.py # 共享路径校验 (防目录遍历) │ ├── ws/server.py # WebSocket Server (心跳检测, ready event, 动态端口) +│ ├── ws/bridge_server.py # Perfetto UI Bridge Server (自托管 UI + WS 桥接) │ ├── prompts.py # Prompt 文件加载器 │ ├── config.py # 全局配置 (LLM 模型, source dir, hook config 持久化, 环境变量覆盖) │ ├── token_tracker.py # LLM Token 使用量追踪 @@ -145,11 +251,16 @@ smartinspector/ │ └── android/tracelib/ # Android SDK (AAR) │ └── src/main/java/.../tracelib/ │ ├── TraceHook.java # Pine AOP 方法 hook (深度保护, Tag截断, 系统widget过滤) +│ ├── ComposeHook.kt # Compose 重组追踪 (TracerImpl hook, API 31+) │ ├── BlockMonitor.java # 主线程卡顿检测 (容量限制防OOM, Fragment泄漏修复) │ ├── SIClient.java # WebSocket 客户端 │ ├── HookConfig.java # 配置模型 (JSON 序列化, BuildConfig.DEBUG守卫) │ └── HookConfigManager.java # 配置管理 (SP 持久化) │ +├── perfetto-plugin/ # Perfetto UI SI Bridge 插件 +│ ├── com.smartinspector.Bridge/ # 插件源码 (TypeScript) +│ └── build.sh # 构建脚本 (clone Perfetto + 复制插件 + build) +│ ├── prompts/ # LLM Prompt 模板 ├── bin/ # trace_processor_shell ├── reports/ # 生成的性能报告 (Markdown) @@ -162,19 +273,22 @@ SDK 通过 Pine AOP 框架 hook 框架方法,用 `SI$` 前缀的 `Trace.beginS ### Hook 类别 -| Hook | 默认 | Tag 格式 | 说明 | -|------|------|----------|------| -| Activity Lifecycle | ON | `SI$ActivityClass.onCreate` | Activity 生命周期 | -| Fragment Lifecycle | ON | `SI$FragmentClass.onCreateView` | Fragment 生命周期 (AndroidX + app) | -| RV Pipeline | ON | `SI$RV#[viewId]#[Adapter].dispatchLayoutStep2` | RecyclerView 管线 | -| RV Adapter | ON | `SI$RV#[viewId]#[Adapter].onBindViewHolder` | Adapter 数据绑定 | -| Layout Inflate | OFF | `SI$inflate#[layout]#[parent]` | 布局加载 | -| View Traverse | OFF | `SI$view#[ViewClass].measure` | View measure/layout/draw | -| Handler Dispatch | OFF | `SI$handler#[msgClass]` | Handler 消息分发 | -| Block Monitor | ON | `SI$block#[MsgClass]#[dur]ms` | 主线程卡顿检测 (≥100ms) | -| Network IO | OFF | `SI$net#[Class].execute` | OkHttp / HttpURLConnection | -| Database IO | OFF | `SI$db#[Class].query#[table]` | SQLiteDatabase / Room | -| Image Load | OFF | `SI$img#[Class].into` | Glide / Coil | + +| Hook | 默认 | Tag 格式 | 说明 | +| ------------------ | --- | ---------------------------------------------- | ------------------------------ | +| Activity Lifecycle | ON | `SI$ActivityClass.onCreate` | Activity 生命周期 | +| Fragment Lifecycle | ON | `SI$FragmentClass.onCreateView` | Fragment 生命周期 (AndroidX + app) | +| RV Pipeline | ON | `SI$RV#[viewId]#[Adapter].dispatchLayoutStep2` | RecyclerView 管线 | +| RV Adapter | ON | `SI$RV#[viewId]#[Adapter].onBindViewHolder` | Adapter 数据绑定 | +| Layout Inflate | OFF | `SI$inflate#[layout]#[parent]` | 布局加载 | +| View Traverse | OFF | `SI$view#[ViewClass].measure` | View measure/layout/draw | +| Handler Dispatch | OFF | `SI$handler#[msgClass]` | Handler 消息分发 | +| Block Monitor | ON | `SI$block#[MsgClass]#[dur]ms` | 主线程卡顿检测 (≥100ms) | +| Network IO | ON | `SI$net#[Class].execute` | OkHttp / HttpURLConnection | +| Database IO | ON | `SI$db#[Class].query#[table]` | SQLiteDatabase / Room | +| Image Load | ON | `SI$img#[Class].into` | Glide / Coil | +| Compose Recomposition | ON | `SI$compose#[Composable]#recompose` | Compose 重组追踪 (API 31+) | + **IO Hook 说明**:Network/DB/Image hook 在所有线程执行,使用独立前缀 (`SI$net#`/`SI$db#`/`SI$img#`),Python 端单独收集到 `io_slices`,不污染主线程 `view_slices` 分析。 @@ -185,38 +299,63 @@ Trace → SI$ slices → 过滤系统类 → 提取 class+method → Glob→Grep ``` 归因系统通过两层过滤排除系统/框架代码: -1. **FQN 包名匹配**:`android.*`、`androidx.*`、`java.*` 等 + +1. **FQN 包名匹配**:`android.`*、`androidx.*`、`java.*` 等 2. **短类名模式匹配**:`Choreographer`、`FragmentManager`、`ViewRootImpl` 等(Perfetto atrace 截断 FQN 时) ## CLI 命令 -### Slash 命令 +### CI/Headless 模式参数 -| 命令 | 说明 | +```bash +uv run smartinspector --ci [选项] +``` + +| 参数 | 说明 | |------|------| -| `/full [--no-wait]` | 全量分析流水线 (采集→分析→归因→报告)。`--no-wait` 跳过等待 App 连接,适用于冷启动耗时分析 | -| `/trace [duration_ms] [pkg]` | 采集 + 自动分析 Perfetto trace | -| `/record [duration_ms] [pkg]` | 只采集不分析,返回 .pb 文件路径 | -| `/analyze [path]` | 分析 trace 文件(无参数时分析上次采集结果) | -| `/report [path]` | 生成性能报告(可选输出到文件) | -| `/config` | 查看当前 hook 配置(通过 WS 从 App 获取) | -| `/config ` | 推送 JSON 配置到 App(如 `{"rv_adapter": false}`) | -| `/config reset` | 恢复 hook 默认配置 | -| `/config source_dir ` | 设置源码目录(自动持久化) | -| `/hooks` | 查看所有 hook 点开关状态 | -| `/hook on ` | 开启指定内置 hook 点(如 `layout_inflate`) | -| `/hook off ` | 关闭指定内置 hook 点 | -| `/hook add ` | 添加自定义 hook 点 | -| `/hook rm ` | 删除自定义 hook 点 | -| `/devices` | 列出已连接 adb 设备 | -| `/connect ` | 通过 adb TCP 连接设备 | -| `/disconnect` | 断开 TCP 设备连接 | -| `/status` | 查看当前会话状态(WS 连接、perf 数据等) | -| `/summary` | 查看 perf_summary 摘要 | -| `/tokens` | 查看 token 使用量 | -| `/clear` | 清除所有分析状态和对话 | -| `/debug` | 打开设备端 Hook 调试配置面板 | -| `/help` | 帮助信息(支持 Tab 补全) | +| `--ci` | 启用非交互式 CI 模式 | +| `--trace ` | 指定已有 trace 文件(跳过设备采集) | +| `--target ` | 目标进程包名 | +| `--duration ` | 采集时长(默认 10000ms) | +| `--output ` | 输出文件路径 | +| `--format markdown\|json` | 报告格式(默认 markdown) | +| `--src ` | 源码目录 | +| `--debug` | 启用 debug 日志 | + +### Slash 命令 + + +| 命令 | 说明 | +| ----------------------------- | -------------------------------------------------------- | +| `/full [--no-wait]` | 全量分析流水线 (采集→分析→归因→报告)。`--no-wait` 跳过等待 App 连接,适用于冷启动耗时分析 | +| `/quick` | 快速分析(纯确定性,不调用 LLM,秒级完成)。无 API Key 时自动降级 | +| `/compare ` | 对比两份报告,生成 before/after 趋势报告 | +| `/trace [duration_ms] [pkg]` | 采集 + 自动分析 Perfetto trace | +| `/record [duration_ms] [pkg]` | 只采集不分析,返回 .pb 文件路径 | +| `/analyze [path]` | 分析 trace 文件(无参数时分析上次采集结果) | +| `/frame ts=X dur=Y` | 分析指定时间范围的帧(ts/dur 为纳秒,CLI 直接分析) | +| `/open [path]` | 启动 Perfetto UI + Bridge Server,交互式框选帧分析 | +| `/close` | 关闭 Perfetto UI Bridge Server | +| `/report [path]` | 生成性能报告(可选输出到文件) | +| `/config` | 查看当前 hook 配置(通过 WS 从 App 获取) | +| `/config ` | 推送 JSON 配置到 App(如 `{"rv_adapter": false}`) | +| `/config reset` | 恢复 hook 默认配置 | +| `/config source_dir ` | 设置源码目录(自动持久化) | +| `/hooks` | 查看所有 hook 点开关状态 | +| `/hook on ` | 开启指定内置 hook 点(如 `layout_inflate`) | +| `/hook off ` | 关闭指定内置 hook 点 | +| `/hook add ` | 添加自定义 hook 点 | +| `/hook rm ` | 删除自定义 hook 点 | +| `/devices` | 列出已连接 adb 设备 | +| `/connect ` | 通过 adb TCP 连接设备 | +| `/disconnect` | 断开 TCP 设备连接 | +| `/status` | 查看当前会话状态(WS 连接、perf 数据等) | +| `/summary` | 查看 perf_summary 摘要 | +| `/tokens` | 查看 token 使用量 | +| `/clear` | 清除所有分析状态和对话 | +| `/debug` | 打开设备端 Hook 调试配置面板 | +| `/help` | 帮助信息(支持 Tab 补全) | + ### 自然语言路由 @@ -227,65 +366,131 @@ Orchestrator 通过 LLM 分类将用户请求路由到对应 Agent: - **性能解读** (`analyze`): "解读这份数据" / "分析一下刚才采集的数据" / "解读一下这个 perf_summary" → Perf Analyzer - **源码搜索** (`explorer`): "搜索 XXX 类源码" / "查看 LazyForEach 的实现" / "定位 DataManager.loadData 方法" → Code Explorer - **通用问答** (`end`): "什么是卡顿" / "怎么优化列表滑动" / "你好" → Fallback 回复 +- **指标追问** (`metric_qa`): "CPU 占用率怎么样" / "帧率怎么样" / "内存有没有泄漏" / "性能怎么样" → Metric QA(需要先完成分析) ## 报告示例 全量分析流水线(`/full`、`/full --no-wait` 或自然语言触发 `full_analysis`)会生成 Markdown 性能报告,保存到 `reports/` 目录。以下为实际生成的报告摘要: -### 测试概要 - -``` -| 项目 | 内容 | -|------|------| -| 应用 | com.smartinspector.hook | -| 时长 | 10.0s | -| 日期 | 2026-04-04 09:30 | ``` +## 问题列表 -### 性能总览 +**源码归因结果共包含6条记录,已全部生成对应问题条目。** -``` -| 指标 | 数值 | 评价 | -|------------|----------|------| -| 平均 FPS | 27.8 | 差 | -| 卡顿次数 | 5 | | -| CPU 峰值 | 50.3% | 良 | -| 内存峰值 | 993MB | 差 | -``` +### P0 CpuBurnWorker 主线程执行CPU密集型计算导致卡顿 -### 问题列表(源码归因) +**现象**:`CpuBurnWorker.startMainThreadWork$run` 在主线程执行耗时 145.00ms。该方法在主线程循环执行100,000次 `Math.sqrt` 计算,是导致主线程卡顿的直接原因。 -报告会通过 SI$ tag 将性能热点归因到具体源码位置,并给出优化建议: +**原因**:根据源码归因,该方法是CPU烧录测试代码,在主线程上执行密集的数学运算,每次循环约5ms,每200ms执行一次,严重阻塞了主线程的UI渲染。 -``` -### P0 RecyclerView 布局与数据绑定严重卡顿 +**调用链**:`CpuBurnWorker.startMainThreadWork$run 145ms` -现象:DemoAdapter.dispatchLayoutStep2 单次耗时 221.24ms,超出帧预算 15.5 倍。 -原因:onBindViewHolder 中存在 Thread.sleep、同步数据加载、主线程图片解码。 -位置:platform/android/app/.../DemoAdapter.java:40-64 +**位置**:`app/src/main/java/com/smartinspector/hook/worker/CpuBurnWorker.kt:41-49` -建议: -1. 移除 Thread.sleep(20ms) -2. 将 loadItemsSync 改为异步加载 -3. 使用 Glide/Coil 异步图片加载 -4. 使用 DiffUtil 增量更新 -``` +**建议**: +1. **移除或禁用测试代码**:在正式发布版本中,应移除或禁用此类用于测试的CPU烧录代码。 +2. **移至后台线程**:如果该逻辑是应用功能所需,必须将其移至后台线程(如使用 `CoroutineScope(Dispatchers.Default).launch` 或 `ExecutorService`)执行,避免阻塞主线程。 +3. **降低计算频率和强度**:如果必须在主线程执行,应大幅减少循环次数(例如从100,000次减少到1,000次以内)或延长执行间隔。 + +### P0 DemoAdapter.onBindViewHolder 存在多项耗时操作导致列表滑动卡顿 + +**现象**:`DemoAdapter.onBindViewHolder` 单次调用最高耗时 74.95ms,在测试期间共调用7次,累计耗时 202.29ms。该方法是导致帧#111严重卡顿(267.25ms)和 RecyclerView `dispatchLayoutStep2` 耗时 229.07ms 的主要原因。 + +**原因**:根据源码归因,该方法内存在多个阻塞主线程的耗时操作: +1. `doExpensiveWork()` 中调用了 `Thread.sleep(20)` 进行强制等待。 +2. `repository.loadItemsSync(5)` 进行同步数据加载。 +3. 执行了30次字符串拼接循环。 +4. 进行了 `Bitmap` 解码操作。 + +**调用链**:`LinearLayoutManager.onLayoutChildren 228.97ms → RV OnBindView 75.05ms → DemoAdapter.onBindViewHolder 74.95ms` + +**位置**:`app/src/main/java/com/smartinspector/hook/adapter/DemoAdapter.java:40-64` + +**建议**: +1. **移除 Thread.sleep**:在主线程中绝对禁止使用 `Thread.sleep()`,应立即移除。 +2. **异步加载数据**:将 `repository.loadItemsSync(5)` 改为异步加载(如使用 `LiveData`、`RxJava` 或 `Coroutine`),在数据准备好后再通知 `Adapter` 更新。 +3. **优化字符串构建**:使用 `StringBuilder` 替代多次 `+` 操作进行字符串拼接。 +4. **异步加载与缓存图片**:将 `Bitmap` 解码移至后台线程,并使用 `Glide`、`Picasso` 等图片加载库进行异步加载和缓存,避免每次绑定都解码。 + +### P1 MainActivity.onCreate 中延迟任务过多且存在潜在风险 -完整报告示例见 [reports/perf_report_20260404_093038.md](reports/perf_report_20260404_093038.md)。 +**现象**:`MainActivity.onCreate` 耗时 14.67ms。方法中启动了多个 `Handler.postDelayed` 任务。 + +**原因**:根据源码归因,方法中启动了多个延迟任务:1) 5秒后加载数据,2) 500ms间隔的循环padding调整,3) 8秒后显示详情Fragment。过多的延迟任务可能阻塞主线程,500ms的循环任务可能造成不必要的性能开销,且使用 `Handler.postDelayed` 有潜在的内存泄漏风险。 + +**调用链**:`MainActivity.onCreate 14.67ms` + +**位置**:`app/src/main/java/com/smartinspector/hook/MainActivity.java:31-67` + +**建议**: +1. **合并或优化任务**:评估500ms循环任务的必要性,考虑是否可以合并或由事件驱动(如监听视图状态)来触发。 +2. **使用 View.post**:对于需要在主线程执行的延迟UI操作,优先使用 `View.post()` 或 `View.postDelayed()`,这可以自动处理 `View` 生命周期,降低内存泄漏风险。 +3. **使用 Lifecycle-aware 组件**:对于需要在特定生命周期执行的任务,考虑使用 `Lifecycle` 和 `Coroutine` 等现代架构组件来管理。 + +### P1 DemoAdapter.onCreateViewHolder 加载复杂布局耗时 + +**现象**:`DemoAdapter.onCreateViewHolder` 单次调用最高耗时 4.89ms,在测试期间共调用7次,累计耗时 10.49ms。 + +**原因**:根据源码归因,该方法加载了复杂布局 `item_complex`。布局的复杂度直接影响 `inflate` 和后续测量、布局的速度。 + +**调用链**:`DemoAdapter.onCreateViewHolder 4.89ms` + +**位置**:`app/src/main/java/com/smartinspector/hook/adapter/DemoAdapter.java:33-37` + +**建议**: +1. **检查并简化布局**:使用 Layout Inspector 或 `ConstraintLayout` 的布局优化工具,检查 `item_complex.xml` 的层级,减少不必要的嵌套。 +2. **使用 ViewStub**:对于列表中并非立即显示的复杂部分,可以考虑使用 `ViewStub` 进行延迟加载。 +3. **启用视图复用**:确保 `RecyclerView` 和 `Adapter` 正确设置了 `setHasStableIds(true)` 并实现了 `getItemId()`,以优化视图复用。 + +### P1 item_complex.xml 布局嵌套过深导致inflate耗时 + +**现象**:`item_complex.xml` 的 inflate 操作单次最高耗时 4.77ms,在测试期间共执行7次,累计耗时 9.80ms。 + +**原因**:根据源码归因,该布局有4层嵌套,并且包含自定义视图 `HeavyDrawView`。深层级的嵌套会导致测量和布局过程计算量增大。 + +**调用链**:`inflate#item_complex 4.77ms` + +**位置**:`app/src/main/res/layout/item_complex.xml:1-64` + +**建议**: +1. **使用 ConstraintLayout 扁平化布局**:将根布局替换为 `ConstraintLayout`,利用其约束关系减少嵌套层级,目标是控制在2层以内。 +2. **优化 HeavyDrawView**:分析 `HeavyDrawView` 的 `onDraw` 方法,确保其绘制操作高效,避免在 `onDraw` 中分配对象或进行复杂计算。 +3. **使用 merge 标签**:如果该布局被 `include`,考虑在根节点使用 `` 标签以消除一层冗余的 `ViewGroup`。 + + +## 附录 + +**采集工具**:Smart Inspector +**原始数据位置**:`/var/folders/tb/6pgk76d11bd278v01zfqkq540000gn/T/tmpatm5lr_x.pb` + [reporter] Report generated + [reporter] Report saved to /xx/xx/reports/perf_report_20260415_232256.md (7.8KB) + +Token usage: +Stage Input Output Total Calls +------------------------------------------------------ +orchestrator 456 3 459 1 +perf_analyzer 2.9k 415 3.3k 1 +attributor 59.1k 3.3k 62.4k 24 +reporter 3.1k 1.3k 4.4k 1 +------------------------------------------------------ +TOTAL 65.6k 5.0k 70.6k 27 +``` ## 技术栈 -| 组件 | 技术 | -|------|------| -| Agent 编排 | LangGraph + LangChain | -| LLM | DeepSeek / Claude / OpenAI (通过 SI_MODEL 配置) | -| Android Trace | Perfetto + atrace (ftrace + CPU callstack + Java heap) | -| HarmonyOS Trace | hiperf + hitrace (规划) | -| 方法 Hook (Android) | Pine AOP Framework | -| CLI 交互 | prompt_toolkit (Tab 补全, REPL) | -| 通信 | WebSocket (CLI ↔ App, 心跳检测, 动态端口) | -| Trace 分析 | trace_processor_shell (SQL) | -| 状态管理 | LangGraph MemorySaver (get_state) | + +| 组件 | 技术 | +| ----------------- | ------------------------------------------------------ | +| Agent 编排 | LangGraph + LangChain | +| LLM | DeepSeek / Claude / OpenAI (通过 SI_MODEL 配置) | +| Android Trace | Perfetto + atrace (ftrace + CPU callstack + Java heap) | +| HarmonyOS Trace | hiperf + hitrace (规划) | +| 方法 Hook (Android) | Pine AOP Framework | +| CLI 交互 | prompt_toolkit (Tab 补全, REPL) + argparse (CI 模式) | +| 通信 | WebSocket (CLI ↔ App, 心跳检测, 动态端口) | +| Trace 分析 | trace_processor_shell (SQL) | +| 状态管理 | LangGraph MemorySaver (get_state) | + ## LLM 配置 @@ -295,20 +500,23 @@ Orchestrator 通过 LLM 分类将用户请求路由到对应 Agent: cp .env.example .env ``` -| 变量 | 说明 | 默认值 | -|------|------|--------| -| `SI_MODEL` | 全局默认模型 | `deepseek-chat` | -| `SI_BASE_URL` | API Base URL (OpenAI 兼容) | `https://api.deepseek.com` | -| `SI_API_KEY` | API Key (回退到 `OPENAI_API_KEY`) | — | -| `SI_ATTRIBUTOR_MODEL` | 归因 Agent 模型覆盖 (代码理解) | 同 `SI_MODEL` | -| `SI_TOOL_TIMEOUT` | 工具子进程超时 (grep/glob) | `30` | -| `SI_READ_MAX_LINES` | Read 工具最大返回行数 | `2000` | -| `SI_READ_MAX_BYTES` | Read 工具最大返回字节数 | `51200` | -| `SI_READ_MAX_LINE_LENGTH` | Read 工具单行最大字符数 | `2000` | -| `SI_REPORT_MAX_TOKENS` | 报告生成最大输入 token | `4000` | -| `SI_WS_PING_TIMEOUT` | WebSocket ping 超时 (秒) | `30` | + +| 变量 | 说明 | 默认值 | +| ------------------------- | ------------------------------ | -------------------------- | +| `SI_MODEL` | 全局默认模型 | `deepseek-chat` | +| `SI_BASE_URL` | API Base URL (OpenAI 兼容) | `https://api.deepseek.com` | +| `SI_API_KEY` | API Key (回退到 `OPENAI_API_KEY`) | — | +| `SI_ATTRIBUTOR_MODEL` | 归因 Agent 模型覆盖 (代码理解) | 同 `SI_MODEL` | +| `SI_TOOL_TIMEOUT` | 工具子进程超时 (grep/glob) | `30` | +| `SI_READ_MAX_LINES` | Read 工具最大返回行数 | `2000` | +| `SI_READ_MAX_BYTES` | Read 工具最大返回字节数 | `51200` | +| `SI_READ_MAX_LINE_LENGTH` | Read 工具单行最大字符数 | `2000` | +| `SI_REPORT_MAX_TOKENS` | 报告生成最大输入 token | `4000` | +| `SI_WS_PING_TIMEOUT` | WebSocket ping 超时 (秒) | `30` | + **切换到 Claude 示例:** + ```bash SI_MODEL=claude-sonnet-4-20250514 SI_BASE_URL=https://api.anthropic.com @@ -316,6 +524,7 @@ SI_API_KEY=sk-ant-xxx ``` **归因用更强模型示例:** + ```bash SI_MODEL=deepseek-chat SI_ATTRIBUTOR_MODEL=claude-sonnet-4-20250514 @@ -331,92 +540,180 @@ SI_ATTRIBUTOR_MODEL=claude-sonnet-4-20250514 - **HarmonyOS**: hdc 已加入 PATH (规划) - **iOS**: Xcode + Instruments (规划) -## Todo +## 路线图 -### 高优先级 +### P1 — 已完成 (2026-04-24) -- [ ] 帧严重度阈值区分刷新率 (120Hz 设备帧预算 8.33ms) -- [ ] 输入事件关联 (touch event → frame jank 因果) -- [ ] 系统类模式补充: `WindowCallback`, `IdleHandler`, Jetpack Compose 类 - -### 中优先级 - -- [ ] RV Instance 区分 create vs bind 开销 -- [ ] attributor agent 内部类 `$数字` 跳过 Glob 直接 grep 外部类 -- [ ] Perfetto `android.surfaceflinger.frame` 维度 (CPU vs GPU 瓶颈) -- [ ] 自适应阈值 (基于设备能力动态调整) -- [ ] 报告缺少对比基线 (before/after) +| # | 项目 | 说明 | 状态 | +|---|------|------|------| +| P1-1 | Compose 重组追踪 | 追踪 Jetpack Compose 重组次数和耗时,定位不必要的 recomposition | ✅ 已完成 | +| P1-2 | 内存分配分析 | 基于 `heap_graph` 数据源分析内存分配热点,定位内存抖动和泄漏 | ✅ 已完成 | +| P1-3 | 历史对比与趋势 | 多次分析结果对比,生成 before/after 报告和性能趋势图 | ✅ 已完成 | +| P1-4 | 智能一键分析 | 纯确定性快速分析,不调用 LLM,30 秒内完成轻量分析 | ✅ 已完成 | +| P1-5 | ExtraHook 参数自动推断 | 自动推断所有重载签名,无需手动配置方法参数 | ✅ 已完成 | +| P1-6 | SQL Summarizer | 压缩 SQL 查询结果为统计摘要+异常采样,降低 60-80% token 消耗 | ✅ 已完成 | +| P1-7 | Analysis Verifier | L1 格式检查 + L2 一致性验证,自动检测遗漏和不一致,支持重试 | ✅ 已完成 | ### 平台扩展 -- [ ] HarmonyOS collector (hdc + hiperf/hitrace) -- [ ] iOS Instruments 集成 -- [ ] Jetpack Compose 性能 hook -- [ ] Native C/C++ 代码覆盖 -- [ ] 内存分配热点追踪 (当前仅 RSS) +- HarmonyOS collector (hdc + hiperf/hitrace) +- iOS Instruments 集成 +- Native C/C++ 代码覆盖 ### 工程优化 -- [ ] LRU 文件缓存减少重复 Read -- [ ] 工具结果截断 (10K 字符上限) -- [ ] 更多复杂 trace 测试 (Kotlin、多文件) -- [ ] CI/CD 集成 +- 帧严重度阈值区分刷新率 (120Hz 设备帧预算 8.33ms) +- 输入事件关联 (touch event → frame jank 因果) +- RV Instance 区分 create vs bind 开销 +- Perfetto `android.surfaceflinger.frame` 维度 (CPU vs GPU 瓶颈) +- 自适应阈值 (基于设备能力动态调整) +- ~~thread_state N+1 查询优化~~ → 已完成:重写为 `__intrinsic_thread_state` 表,支持 `blocked_function`/`io_wait`/`waker_utid` +- LLM 实例统一管理 (LLMFactory) + +### ✅ 已完成 (2026-04-24 P1 改进) + +**P1-1: Compose 重组追踪** + +- `ComposeHook.kt`: Hook Compose Runtime 的 `TracerImpl` 和 `startRestartGroup/endRestartGroup` +- 切片前缀 `SI$compose#`,Tag 格式:`SI$compose#ComposableName#recompose` / `SI$compose#ComposableName#first` +- Python 端新增 `collect_compose_slices()` 查询和重组分析 + +**P1-2: 内存分配分析** + +- `collector/memory.py`: `MemoryAnalyzer` 基于 `heap_graph` 表分析 Java 堆内存 +- 按类名聚合对象数量和总大小,检测 Activity/Fragment 泄漏嫌疑 +- 内存增长趋势追踪(RSS / anon 随时间变化) + +**P1-3: 历史对比与趋势** + +- `commands/compare.py`: `/compare` 命令,多次分析结果对比 +- `storage/store.py`: 报告存储层,支持基线管理和历史查询 +- 生成 before/after 报告,自动标注回归项和改善项 + +**P1-4: 智能一键分析** + +- `commands/quick.py`: `/quick` 命令,纯确定性快速分析(不调用 LLM) +- 仅运行 fast-path 归因 + `compute_hints`,秒级完成 +- 无 API Key 时自动降级为 quick 模式 + +**P1-5: ExtraHook 参数自动推断** + +- `TraceHook.java` 改进 `hookExtraClasses()`:自动推断所有重载签名 +- 遍历 `getDeclaredMethods()` 匹配方法名,替代原先的只尝试无参签名 + +**P1-6: SQL Summarizer** + +- `deterministic.py` 新增 `summarize_sql_result()` — 将 SQL 查询结果压缩为统计摘要 (count/min/max/avg/p95/p99) + 分布直方图 + 异常采样 + 去重聚合 +- `deterministic.py` 新增 `compress_perf_json()` — 对 perf JSON 中的大列表字段 (slowest_slices/block_events/frame_timeline/thread_state) 自动应用压缩 +- 集成到 `perf_analyzer.py` (perf_json 传入前压缩) 和 `frame_analyzer.py` (slices 列表压缩) +- Token 消耗降低 60-80%,减少 LLM 在大量数据中的幻觉 + +**P1-7: Analysis Verifier** + +- `agents/verifier.py` 新增分层验证系统 (0 token,纯规则) +- L1 格式检查:数值存在性、方法名引用、长度合理、P0/P1/P2 分级 +- L2 一致性验证:P0 问题覆盖、关键数据点数值一致性 (±20%)、热点方法覆盖 +- 集成到 `perf_analyzer.py` 和 `frame_analyzer.py`,L2 失败时自动重试一次补充遗漏 +- `VerificationResult` 包含 score/issues/warnings/l1_passed/l2_passed 属性 + +### ✅ 已完成 (2026-04-24 P0 改进) + +**P0-1: IO Hooks 启用** + +- Network/DB/Image IO Hook 默认开启 +- IO 切片独立收集到 `io_slices`,不污染主线程 `view_slices` 分析 +- `collect_io_slices()` 方法从所有线程收集 `SI$net#`/`SI$db#`/`SI$img#` 切片 + +**P0-2: 冷启动专项分析** + +- `collector/startup.py`: `StartupAnalyzer` 将启动序列切分为 4 个阶段 + - pre-main (进程启动 → Application.attachBaseContext) + - Application.onCreate → first Activity.onCreate + - Activity.onCreate → 首帧 doFrame + - 首帧渲染 +- `graph/nodes/startup.py`: 图节点集成,输出 Markdown 格式的启动分析报告 +- 关键路径提取 + 瓶颈识别 + 自动优化建议 + +**P0-3: Headless/CI 模式** + +- `headless.py`: `HeadlessRunner` 非交互式运行全量分析流水线 +- CLI 参数: `--ci` 启用,`--trace`/`--target`/`--duration`/`--output`/`--format`/`--debug` +- 支持 Markdown 和 JSON 两种输出格式 +- 无 API Key 时降级为纯确定性分析(跳过 LLM) + +**P0-4: JSON 报告格式** + +- `graph/nodes/reporter/json_formatter.py`: 结构化 JSON 报告 +- 包含 `summary`(FPS/CPU/jank)、`issues`(P0/P1/P2 分级)、`metrics`(详细指标) +- 自动关联 attribution 结果到 issue 的 `source` 字段 +- 适合 CI/CD 自动化解析 + +**P0-5: IO 切片归因** + +- `agents/attributor.py` 支持 IO 类型切片(`SI$net#`/`SI$db#`/`SI$img#`)归因 +- `commands/attribution.py` 解析 IO tag 提取 class/method 用于源码搜索 +- 归因结果包含 `io_type` 字段(network/database/image) ### ✅ 已完成 (2026-04-05 重构) **Collector 采集层** -- [x] 修复内存数值单位错误(/1024 转换问题) -- [x] 修复 Block Events 数据覆盖(WS+SQL 合并) -- [x] CPU 热点添加调用链重建 -- [x] sched 添加阻塞原因分析 -- [x] 新增系统级 CPU 指标采集 -- [x] HookConfig 透传 + Tag 截断保护 -- [x] PerfettoCollector 支持 context manager 协议 + +- 修复内存数值单位错误(/1024 转换问题) +- 修复 Block Events 数据覆盖(WS+SQL 合并) +- CPU 热点添加调用链重建 +- sched 添加阻塞原因分析 +- 新增系统级 CPU 指标采集 +- HookConfig 透传 + Tag 截断保护 +- PerfettoCollector 支持 context manager 协议 **Agents 编排层** -- [x] Orchestrator LLM 调用异常处理 -- [x] REPL 主循环全局异常保护 -- [x] graph.stream 循环防崩溃 -- [x] node_error_handler 统一节点错误处理 -- [x] 路由支持 enum 和 string 双模式 -- [x] 路由 prompt few-shot 提升准确率 -- [x] 路由 LLM max_tokens=5 减少 token 浪费 -- [x] Reporter 输入 token 估算和截断 -- [x] Attributor 消息窗口裁剪防 O(n²) 增长 -- [x] Fallback 消息窗口过滤仅 Human/AI -- [x] Reporter 真正流式输出防重复打印 -- [x] 用 get_state()+MemorySaver 替代手动 state 重建 -- [x] Attributor 结构化输出 + 文本解析 fallback -- [x] 清理无引用的 prompts/main.txt -- [x] Agent LLM 单例双重检查锁线程安全 + +- Orchestrator LLM 调用异常处理 +- REPL 主循环全局异常保护 +- graph.stream 循环防崩溃 +- node_error_handler 统一节点错误处理 +- 路由支持 enum 和 string 双模式 +- 路由 prompt few-shot 提升准确率 +- 路由 LLM max_tokens=5 减少 token 浪费 +- Reporter 输入 token 估算和截断 +- Attributor 消息窗口裁剪防 O(n²) 增长 +- Fallback 消息窗口过滤仅 Human/AI +- Reporter 真正流式输出防重复打印 +- 用 get_state()+MemorySaver 替代手动 state 重建 +- Attributor 结构化输出 + 文本解析 fallback +- 清理无引用的 prompts/main.txt +- Agent LLM 单例双重检查锁线程安全 **SDK 层** -- [x] Release 变体替换为纯 no-op stubs -- [x] PineConfig.debug=true 替换为 BuildConfig.DEBUG -- [x] FragmentLifecycleCallbacks registered 集合内存泄漏修复 -- [x] BlockMonitor.blockEvents 容量限制防 OOM -- [x] view_traverse 过滤系统 widget -- [x] SI$ Tag 超 127 字节自动截断 -- [x] 高频 hook Log.d BuildConfig.DEBUG 守卫 -- [x] Trace 嵌套深度保护防 atrace 溢出 + +- Release 变体替换为纯 no-op stubs +- PineConfig.debug=true 替换为 BuildConfig.DEBUG +- FragmentLifecycleCallbacks registered 集合内存泄漏修复 +- BlockMonitor.blockEvents 容量限制防 OOM +- view_traverse 过滤系统 widget +- SI$ Tag 超 127 字节自动截断 +- 高频 hook Log.d BuildConfig.DEBUG 守卫 +- Trace 嵌套深度保护防 atrace 溢出 **基础设施层** -- [x] REPL 全局异常保护 -- [x] WS server 启动异常不再静默吞掉 -- [x] WebSocket ping/pong 心跳检测 -- [x] WS server ready event 防止启动竞态 -- [x] streaming graph 迭代错误处理 -- [x] state 合并重构为 AgentState 驱动 -- [x] 硬编码端口 9876 替换为 get_ws_port() -- [x] Slash 命令 Tab 自动补全 -- [x] 版本号从 package metadata 读取 -- [x] /clear 清理所有分析状态字段 -- [x] 启动前置条件检查 (adb + API key) -- [x] 所有依赖添加版本上限 -- [x] /report 支持文件输出 -- [x] Hook config 持久化到本地文件 -- [x] send_config msg_id + ACK 机制 -- [x] 硬编码配置值集中到 config.py,支持环境变量覆盖 -- [x] 工具共享路径校验提取到 path_utils 模块 -- [x] Collector 全链路异常日志补全 -- [x] WS 异常日志替换静默吞掉 + +- REPL 全局异常保护 +- WS server 启动异常不再静默吞掉 +- WebSocket ping/pong 心跳检测 +- WS server ready event 防止启动竞态 +- streaming graph 迭代错误处理 +- state 合并重构为 AgentState 驱动 +- 硬编码端口 9876 替换为 get_ws_port() +- Slash 命令 Tab 自动补全 +- 版本号从 package metadata 读取 +- /clear 清理所有分析状态字段 +- 启动前置条件检查 (adb + API key) +- 所有依赖添加版本上限 +- /report 支持文件输出 +- Hook config 持久化到本地文件 +- send_config msg_id + ACK 机制 +- 硬编码配置值集中到 config.py,支持环境变量覆盖 +- 工具共享路径校验提取到 path_utils 模块 +- Collector 全链路异常日志补全 +- WS 异常日志替换静默吞掉 + diff --git a/docs/2026-04-28-metric-aq-design.md b/docs/2026-04-28-metric-aq-design.md new file mode 100644 index 0000000..dbac91f --- /dev/null +++ b/docs/2026-04-28-metric-aq-design.md @@ -0,0 +1,240 @@ +# Metric QA — 自然语言指标问答设计 + +## 背景 + +当前 SmartInspector 的分析模式是全量采集 + 全量分析(`/full`),用户无法针对单个指标追问。用户在跑完一次完整分析后,常需要针对具体指标做深入了解,例如"CPU 占用率怎么样?"、"内存有没有泄漏?"。 + +本设计新增 `metric_qa` 节点,允许用户在已有 `perf_summary` 数据的基础上,用自然语言追问具体性能指标。 + +## 约束 + +- **前置条件**:用户必须先通过 `/full`、`/trace`、`/analyze` 等命令完成分析,`state["perf_summary"]` 中有数据。若无数据,提示用户先采集。 +- **触发场景**:仅在交互式 CLI 模式中使用,不涉及 headless/CI。 +- **指标范围**:固定 20 个预定义指标,不做开放问答。 +- **回答深度**:数据提取 + LLM 解读(包含优化建议)。 + +## 指标定义 + +### 指标映射表 + +共 6 大类、20 个细粒度指标,每个指标映射到 `perf_summary` 中的数据段: + +#### 一、CPU & 调度类 + +| ID | 指标名 | 中/英触发词 | perf_summary 数据段 | +|---|---|---|---| +| `cpu` | CPU 占用率 | cpu占用 / cpu usage / cpu使用率 | `cpu_usage.overall_cpu_pct` + `per_thread` | +| `cpu_hotspot` | CPU 热点函数 | cpu热点 / hot function / 火焰图 | `cpu_hotspots[].function, samples, pct, callchain` | +| `sched` | 线程调度 | 调度 / 上下文切换 / context switch | `scheduling.hot_threads` (switches, dominant_state) | +| `blocked` | 主线程阻塞 | 阻塞 / 卡住 / block / ANR | `block_events[].dur_ms, stack_trace` | + +#### 二、内存类 + +| ID | 指标名 | 中/英触发词 | perf_summary 数据段 | +|---|---|---|---| +| `memory` | 内存占用 | 内存 / memory / RSS | `process_memory[].rss_kb, avg_rss_kb` | +| `heap` | 堆分析 / 对象分布 | 堆 / heap / 对象 / leak / 内存泄漏 | `memory.heap_graph_classes` (obj_count, total_size_kb) | + +#### 三、UI & 渲染类 + +| ID | 指标名 | 中/英触发词 | perf_summary 数据段 | +|---|---|---|---| +| `frame` | 帧率 / 卡顿 | 帧率 / fps / 卡顿 / jank / 掉帧 | `frame_timeline.fps, jank_frames, slowest_frames` | +| `rv` | RecyclerView | 滚动 / recycler / list / 列表 | `view_slices.rv_instances[].methods, total_ms` | +| `view` | View 绘制 | 绘制 / draw / measure / layout / view | `view_slices.slowest_slices` (measure/layout/draw) | +| `compose` | Compose 重组 | compose / 重组 / recompose | `compose_slices.composables` (recompose_count, total_ms) | +| `inflate` | 布局加载 | 布局加载 / inflate / 布局 | `view_slices` 中 `SI$inflate#` 前缀 slices | +| `startup` | 冷启动 | 启动 / 冷启动 / startup / cold start | startup phases + bottlenecks | + +#### 四、IO 类 + +| ID | 指标名 | 中/英触发词 | perf_summary 数据段 | +|---|---|---|---| +| `io` | IO 总览 | io / 磁盘 | `io_slices.summary` | +| `network` | 网络请求 | 网络 / network / 请求 / okhttp | `io_slices` 中 `io_type: "network"` | +| `db` | 数据库查询 | 数据库 / db / query / sql | `io_slices` 中 `io_type: "database"` | +| `image` | 图片加载 | 图片 / image / glide / coil | `io_slices` 中 `io_type: "image"` | + +#### 五、线程 & 系统类 + +| ID | 指标名 | 中/英触发词 | perf_summary 数据段 | +|---|---|---|---| +| `thread_state` | 线程状态分布 | 线程状态 / running / sleeping | `thread_state[].state_distribution` | +| `sys` | 系统状态 | 系统状态 / cpu频率 / cpu freq | `sys_stats.cpu_freq_by_core, cpu_idle_samples` | +| `input` | 输入事件 | 触摸 / touch / input | `input_events` | + +#### 六、总览 + +| ID | 指标名 | 中/英触发词 | perf_summary 数据段 | +|---|---|---|---| +| `overview` | 性能总览 | 性能怎么样 / overall / summary | 聚合所有关键指标 | + +## 架构设计 + +### 流程 + +``` +用户输入 "cpu占用率怎么样?" + → orchestrator 节点(LLM 意图分类) + → RouteDecision.metric_qa + metric_id="cpu" + → metric_qa 节点 + 1. 检查 state["perf_summary"] 是否存在 + 2. 根据 metric_id 提取对应数据段 + 3. 调用 LLM 做专门解读(专用 prompt) + 4. 返回 messages + 解读结果 +``` + +### RouteDecision 扩展 + +在 `RouteDecision` 枚举中新增 `metric_qa` 值。orchestrator 路由到 `metric_qa` 节点。 + +### AgentState 扩展 + +无需新增 state 字段。metric_qa 节点通过 orchestrator 传递的 `_route` 值识别指标类型。 + +具体来说,`_route` 的格式扩展为: +- 原有:`"metric_qa"` +- 新增约定:`"metric_qa:cpu"` — 冒号后跟 metric_id + +### 新增文件 + +| 文件 | 说明 | +|---|---| +| `src/smartinspector/graph/nodes/metric_qa.py` | metric_qa 节点实现 | +| `prompts/metric-qa.txt` | 指标解读 LLM prompt | + +### 修改文件 + +| 文件 | 修改内容 | +|---|---| +| `src/smartinspector/graph/state.py` | RouteDecision 枚举新增 `metric_qa` | +| `src/smartinspector/graph/nodes/orchestrator.py` | 意图分类 prompt 新增 metric_qa 路由 + metric_id 提取 | +| `src/smartinspector/graph/__init__.py`(或 graph 构建文件) | 注册 metric_qa 节点 + 条件边 | + +## metric_qa 节点设计 + +```python +@node_error_handler("metric_qa") +def metric_qa(state: AgentState) -> dict: + # 1. 解析 _route 获取 metric_id + route = state.get("_route", "") + metric_id = route.split(":")[1] if ":" in route else "overview" + + # 2. 检查 perf_summary 是否存在 + perf_summary = state.get("perf_summary", "") + if not perf_summary: + return { + "messages": [AIMessage(content="请先运行 /full 或 /trace 采集数据后再查询指标。")], + **_pass_through(state), + } + + # 3. 提取对应指标数据 + data = extract_metric_data(perf_summary, metric_id) + + # 4. 调用 LLM 解读 + metric_name = METRIC_NAMES.get(metric_id, "性能总览") + prompt = load_prompt("metric-qa").format(metric_name=metric_name, data=data) + result = llm.invoke(prompt) + + # 5. 返回 + return { + "messages": [result], + **_pass_through(state), + } +``` + +### extract_metric_data() 函数 + +将 metric_id 映射到 perf_summary 的 JSON 路径,提取对应数据段。对于组合指标(如 `cpu` = `cpu_usage` + `cpu_hotspots`),合并多段数据。 + +映射关系(metric_id → perf_summary keys): + +```python +METRIC_DATA_MAP: dict[str, list[str]] = { + "cpu": ["cpu_usage"], + "cpu_hotspot": ["cpu_hotspots"], + "sched": ["scheduling"], + "blocked": ["block_events"], + "memory": ["process_memory"], + "heap": ["memory"], + "frame": ["frame_timeline"], + "rv": ["view_slices"], # 过滤 rv_instances + "view": ["view_slices"], # 过滤 slowest_slices + "compose": ["compose_slices"], + "inflate": ["view_slices"], # 过滤 SI$inflate# 前缀 + "startup": [], # 来自 startup analyzer(单独数据段) + "io": ["io_slices"], + "network": ["io_slices"], # 过滤 io_type=network + "db": ["io_slices"], # 过滤 io_type=database + "image": ["io_slices"], # 过滤 io_type=image + "thread_state": ["thread_state"], + "sys": ["sys_stats"], + "input": ["input_events"], + "overview": [], # 聚合所有 +} +``` + +对于需要过滤的指标(`rv`、`view`、`inflate`、`network`、`db`、`image`),在提取后做二次过滤,只保留相关子集传给 LLM。 + +## Orchestrator 意图分类扩展 + +在现有路由 prompt 中新增 `metric_qa` 分类。示例 prompt 片段: + +``` +metric_qa — 用户追问某个具体性能指标。关键词: + cpu占用率、cpu usage、cpu热点、火焰图 → metric_qa:cpu_hotspot + 内存、memory、RSS → metric_qa:memory + 内存泄漏、heap、leak → metric_qa:heap + 帧率、fps、卡顿、jank、掉帧 → metric_qa:frame + 滚动、recycler、列表 → metric_qa:rv + 绘制、draw、measure、layout → metric_qa:view + compose、重组、recompose → metric_qa:compose + 布局加载、inflate → metric_qa:inflate + 网络、network、请求 → metric_qa:network + 数据库、db、query → metric_qa:db + 图片加载、glide、coil → metric_qa:image + 启动、冷启动、startup → metric_qa:startup + 阻塞、卡住、block、ANR → metric_qa:blocked + 线程状态、sleeping → metric_qa:thread_state + 性能怎么样、overall → metric_qa:overview + 调度、上下文切换 → metric_qa:sched + io、磁盘 → metric_qa:io + 系统状态、cpu频率 → metric_qa:sys + 触摸、touch → metric_qa:input +``` + +LLM 返回格式保持现有约定(单标签),只新增 `metric_qa:` 格式。 + +## Prompt 设计 + +`prompts/metric-qa.txt` 结构: + +``` +你是 Android 性能分析专家。用户正在查看一份 Perfetto trace 分析报告,针对「{metric_name}」指标追问。 + +以下是该指标的原始数据: +{data} + +请用中文回答用户的问题,要求: +1. 先给出当前指标的数值概要(如"CPU 占用率 45%") +2. 如果数据中有异常值,指出并解释 +3. 给出 1-2 条具体优化建议 +4. 如果用户问了具体问题,直接回答 +5. 控制在 200 字以内 +``` + +## 错误处理 + +| 场景 | 处理 | +|---|---| +| `perf_summary` 为空 | 返回提示:"请先运行 /full 或 /trace 采集数据" | +| metric_id 对应的数据段为空 | 返回:"该 trace 中没有采集到 {metric_name} 相关数据" | +| 无法识别具体指标 | fallback 到 `overview`(性能总览) | +| startup 数据不在 perf_summary 中 | 从 state 中查找 startup 分析结果 | + +## 不做的事 + +- 不做开放问答——只支持 20 个预定义指标 +- 不做自动采集——必须有已分析数据 +- 不改 headless/CI 模式——仅交互式 CLI +- 不新增 AgentState 字段——复用 `_route` 传递 metric_id diff --git a/docs/2026-05-06-architecture-analysis-report.md b/docs/2026-05-06-architecture-analysis-report.md new file mode 100644 index 0000000..7f6bdc0 --- /dev/null +++ b/docs/2026-05-06-architecture-analysis-report.md @@ -0,0 +1,619 @@ +# AppSmartInspector 功能架构分析报告 + +> 审查日期: 2026-05-06 +> 审查范围: `src/smartinspector/` 全部 62 个 Python 源文件,约 13,287 行代码 +> 审查人: 资深架构师 + +--- + +## 一、项目定位与现状总结 + +AppSmartInspector 是一个 AI 驱动的移动端性能分析 CLI 工具,核心能力是: +- 从 Android 设备采集 Perfetto trace +- 通过 LLM Agent 分析性能瓶颈 +- 将性能热点归因到具体源码位置 +- 生成 Markdown/JSON 结构化报告 + +当前仅支持 Android 平台,HarmonyOS 和 iOS 在规划中。 + +### 量化概览 + +| 指标 | 数值 | +|------|------| +| Python 源文件数 | 62 | +| 总代码行数 | ~13,287 | +| 测试文件数 | 4 | +| 测试代码行数 | ~1,208 | +| 测试覆盖率 | 约 9% (1208/13287) | +| 最大单文件 | `collector/perfetto.py` (2,345 行) | +| collect_* 方法数 | 14 | +| graph 节点数 | 10 (orchestrator + 9 业务节点) | +| Agent 数 | 6 (attributor, perf_analyzer, frame_analyzer, explorer, android, deterministic) | + +--- + +## 二、当前架构分析 + +### 2.1 整体架构评分: 7.5 / 10 + +项目整体架构设计合理,LangGraph pipeline 模式清晰,deterministic pre-computation 层是亮点。但在模块粒度、扩展性、测试覆盖等方面存在明显改进空间。 + +### 2.2 架构分层 + +``` +┌─────────────────────────────────────────────────────────┐ +│ CLI Entry Points │ +│ cli.py (argparse) │ graph/cli.py (REPL) │ headless │ +├─────────────────────────────────────────────────────────┤ +│ Commands Layer (Slash) │ +│ trace │ orchestrate │ hook │ device │ session │ compare │ +├─────────────────────────────────────────────────────────┤ +│ LangGraph Orchestration │ +│ builder.py ← state.py ← streaming.py │ +│ Nodes: orchestrator → collector → analyzer → │ +│ attributor → reporter → startup → metric_qa │ +├─────────────────────────────────────────────────────────┤ +│ Agent Layer │ +│ attributor │ perf_analyzer │ frame_analyzer │ │ +│ explorer │ android │ deterministic (纯计算) │ +│ verifier (质量验证) │ +├─────────────────────────────────────────────────────────┤ +│ Collector Layer │ +│ perfetto.py (PerfettoCollector - 14 methods) │ +│ startup.py (StartupAnalyzer) │ memory.py (MemoryAnalyzer) │ +├─────────────────────────────────────────────────────────┤ +│ Infrastructure Layer │ +│ config.py │ debug_log.py │ prompts.py │ token_tracker │ +│ ws/server │ ws/bridge_server │ perfetto_compat │ +│ tools (grep/glob/read/perfetto) │ storage/store │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.3 各模块质量评估 + +| 模块 | 行数 | 评分 | 评价 | +|------|------|------|------| +| `graph/state.py` | 88 | 9/10 | 简洁的 AgentState 定义,`_pass_through` 和 `node_error_handler` 设计得当 | +| `graph/builder.py` | 108 | 9/10 | 图构建清晰,路由映射完备,条件边设计合理 | +| `agents/deterministic.py` | 822 | 9/10 | 纯计算层,与 LLM 职责分离明确,是架构亮点 | +| `agents/verifier.py` | ~150 | 8.5/10 | 0 token 验证,L1+L2 分层设计优秀 | +| `graph/nodes/orchestrator.py` | 263 | 8/10 | 路由逻辑完备,metric_qa 处理合理,但 prompt 内联过长 | +| `graph/streaming.py` | 74 | 8/10 | 流式处理简洁,get_state() 复用得当 | +| `config.py` | ~170 | 8/10 | 集中管理配置,支持环境变量覆盖,但有重复模式 | +| `token_tracker.py` | ~80 | 9/10 | 线程安全,API 设计清晰 | +| `collector/perfetto.py` | 2,345 | 6.5/10 | 功能完整但文件过大,存在 SQL 安全风险、静默异常吞没 | +| `agents/attributor.py` | 1,027 | 7/10 | fast-path 优化到位,但 LLM 单例管理混乱 | +| `commands/attribution.py` | 1,164 | 7/10 | SI$ tag 解析完备,但大量重复解析逻辑 | +| `ws/bridge_server.py` | 436 | 6/10 | 全局可变状态过多,trace path 管理不够健壮 | +| `headless.py` | 169 | 7.5/10 | 复用 LangGraph pipeline,但 JSON 序列化冗余 | +| `storage/store.py` | ~200 | 7/10 | 基线管理功能完整,但与 reporter 耦合较紧 | + +### 2.4 做得好的地方(架构亮点) + +#### 1. Deterministic Pre-computation 层 +`agents/deterministic.py` 将算术和分类逻辑从 LLM 中完全剥离。`compute_hints()` 提供 8 个纯计算模块(severity、call_chain、RV hotspot、jank correlation、CPU hotspot、thread_state、SQL summarizer、perf JSON compression),大幅减少 LLM token 消耗,同时提高结论准确性。 + +**这是项目最有价值的架构决策**,将"确定性计算"与"LLM 理解"清晰分离。 + +#### 2. Attributor Fast-path +`agents/attributor.py` 对简单 Java 类搜索走 Glob→Grep→Read 快路径,绕过 LLM 调用。Partial fallback 机制(部分命中 fast-path,其余走 LLM)设计精巧,兼顾效率和覆盖。 + +#### 3. SI$ Tag System +完整的 tag 解析体系(`commands/attribution.py`),涵盖 RV、block、inflate、view、handler、compose、net、db、img 等 12+ 种模式。匿名内部类处理尤其周到(`_extract_method_from_anonymous`)。 + +#### 4. `node_error_handler` Decorator +统一的节点错误处理模式,`@node_error_handler("node_name")` 确保任何节点异常不会导致 pipeline 崩溃,返回安全状态 + AIMessage 错误提示。 + +#### 5. Pipeline Architecture Rule +CLAUDE.md 中明确规定"所有新功能必须复用 LangGraph pipeline 链路",避免独立执行路径。这个约定保证了架构一致性。 + +#### 6. Trace Collection Degradation +PerfettoCollector 的 stdin-pipe → cat-pipe → cmdline 三级降级策略,覆盖 SELinux 限制等设备兼容性问题。 + +### 2.5 核心数据流 + +``` +用户输入 → orchestrator_node (LLM 路由) + │ + ├─ full_analysis/startup → collector_node + │ │ + │ └─ PerfettoCollector.summarize() + │ → 14 collect_*() 方法 → PerfSummary JSON + │ → perf_summary (str) 存入 AgentState + │ + ├─ analyzer_node + │ │ + │ └─ compute_hints(perf_json) ← 确定性预计算 + │ └─ perf_analyzer agent (LLM) ← 生成分析 + │ → perf_analysis (str) 存入 AgentState + │ + ├─ attributor_node + │ │ + │ └─ extract_attributable_slices(perf_json) + │ └─ fast-path: Glob→Grep→Read (无 LLM) + │ └─ slow-path: attributor agent (LLM + tools) + │ → attribution_result (str) 存入 AgentState + │ + └─ reporter_node + │ + └─ compute_hints(perf_json) ← 第二次计算(重复!) + └─ format_perf_sections(perf_json) + └─ LLM report generation (流式) + → Markdown/JSON 报告 +``` + +--- + +## 三、识别的核心问题 + +### 3.1 P0 — 架构级问题(影响可维护性和扩展性) + +#### P0-1: `collector/perfetto.py` 巨型文件(2,345 行) + +**问题**: PerfettoCollector 单文件包含 14 个 `collect_*()` 方法,每个方法包含独立的 SQL 查询、数据转换和异常处理逻辑。文件行数是第二大文件(`commands/attribution.py` 1,164 行)的两倍。 + +**影响**: +- 代码审查困难,单个 PR 难以覆盖完整变更 +- 不同 collect 方法之间的公共逻辑(SQL 查询模板、结果转换)被复制 +- 无法独立测试单个 collect 方法(需 mock 整个 PerfettoCollector) + +**根因**: 缺少 collector 方法级别的模块化拆分。 + +#### P0-2: AgentState 中 `perf_summary` 为 JSON 字符串 + +**问题**: `perf_summary: str` 在 pipeline 内部以 JSON 字符串形式传递。每个消费节点(analyzer、attributor、reporter、metric_qa)都需要 `json.loads()` 解析,产生 3-4 次重复反序列化。 + +**影响**: +- 不必要的 CPU 消耗和内存分配 +- 类型不安全:消费方无法获得类型提示 +- `compute_hints()` 在 analyzer 和 reporter 阶段被调用两次(完全重复计算) + +**根因**: LangGraph 的 `TypedDict` state 要求可序列化类型,最初用 JSON string 是最简单方案,但缺乏演进。 + +#### P0-3: LLM 实例管理碎片化 + +**问题**: 6 个 Agent 模块各自维护独立的 LLM 单例,管理模式不一致: + +| 模块 | 模式 | 问题 | +|------|------|------| +| `orchestrator.py` | 全局 `_route_llm` | 被 reporter generator 不合理复用 | +| `attributor.py` | 全局 + Lock + `_structured_ok` 探测 | 全局可变状态竞态风险 | +| `perf_analyzer.py` | 全局 + Lock | 独立实例 | +| `frame_analyzer.py` | 全局 + Lock | 独立实例 | +| `explorer.py` | 全局 + Lock | 使用可能废弃的 `create_agent` API | +| `android.py` | 全局 + Lock | 同上 | + +**影响**: +- 修改 LLM 配置(如 temperature、model)需要改 6 个文件 +- Agent 实例化模式不一致(`bind_tools` + 手动 dispatch vs `create_agent` vs 单次 invoke) +- 无法统一管理 token 配额和限流 + +#### P0-4: 测试覆盖率极低(~9%) + +**问题**: 仅 4 个测试文件,1,208 行测试代码。核心业务逻辑(SI$ tag 解析、deterministic hints、SQL 查询、归因逻辑)缺乏测试保护。 + +**影响**: +- 重构风险极高:无法验证变更不引入回归 +- 架构改进难以落地:缺乏安全网 +- 关键业务逻辑(severity 分类、hotspot 排名)的正确性依赖人工验证 + +### 3.2 P1 — 设计缺陷(影响代码质量和可靠性) + +#### P1-1: SI$ Tag 解析逻辑重复 + +**问题**: `commands/attribution.py` (1,164 行) 中 `extract_class()`、`extract_method()`、`extract_fqn()` 三个函数对同一个 SI$ tag 分别独立解析,每个函数都有相同的 `if body.startswith("block#"):` 等分支结构,大量重复代码。 + +**影响**: 修改 tag 格式(如新增 `SI$compose#`)需要在 3+ 个函数中同步修改,容易遗漏。 + +#### P1-2: SQL 注入风险 + +**问题**: `collector/perfetto.py` 多处 SQL 查询使用 f-string 拼接: +- `WHERE name = '{package_name}'` (用户输入) +- `WHERE uid = {uid}` (内部生成但未验证) +- `WHERE id IN ({id_list})` (列表拼接) + +虽然 trace_processor 是本地进程,但 `package_name` 来自 CLI/WS 配置,理论上有注入可能。 + +#### P1-3: `compute_hints()` 重复调用 + +**问题**: `compute_hints(perf_json)` 在 pipeline 中被调用两次: +1. `perf_analyzer.py` — analyzer 阶段 +2. `formatter.py` — reporter 阶段(通过 `format_perf_sections`) + +同一段 JSON 被解析和计算两次,完全浪费。 + +#### P1-4: Bridge Server 全局状态管理 + +**问题**: `bridge_server.py` 使用 4 个模块级全局变量管理状态: +```python +_active_bridge: BridgeServer | None = None +_active_trace_server = None +_cached_perf_summary: str = "" +_cached_attribution_result: str = "" +``` + +变量命名不一致(`_perf_summary_cache` vs `_cached_perf_summary`),状态清理依赖调用方正确执行。 + +#### P1-5: Agent API 不一致 + +**问题**: 三种不同的 Agent 实现模式并存: +1. `explorer.py` / `android.py` — `langchain.agents.create_agent`(可能已废弃) +2. `attributor.py` — `llm.bind_tools` + 手动 tool dispatch +3. `perf_analyzer.py` / `frame_analyzer.py` — 单次 `llm.invoke` + +增加新 Agent 时没有统一模式可参考。 + +### 3.3 P2 — 优化项(影响性能和开发体验) + +#### P2-1: `perf_analyzer.py` 武断截断 + +`perf_json[:3000]` 硬截断后发送给 LLM,可能截断关键数据(如 thread_state),也可能包含无用数据(如 cpu_idle_samples 时间序列)。 + +#### P2-2: config.py 重复模式 + +6 个 `get_*()` 函数完全同构(`try: return int(os.environ.get(...)) except: return default`),应提取为 `_env_int()` 辅助函数。 + +#### P2-3: 函数内部 import + +`formatter.py` 在函数内部 `from smartinspector.agents.deterministic import compute_hints`,虽然 Python 缓存模块,但代码组织不规范。 + +#### P2-4: perf_analyzer 智能截断 + +应根据 `compute_hints` 结果智能选择补充数据,而非盲截。 + +--- + +## 四、功能架构改进方案 + +### 4.1 改进一:Collector 模块化拆分 + +**目标**: 将 2,345 行的 `perfetto.py` 拆分为模块化结构。 + +**方案**: + +``` +collector/ +├── __init__.py +├── perfetto.py # PerfettoCollector 基类(open/close/summarize 公共逻辑) +├── sched.py # collect_sched(), collect_thread_state() +├── cpu.py # collect_cpu_hotspots(), collect_cpu_usage() +├── frame.py # collect_frame_timeline(), collect_view_slices() +├── memory.py # collect_process_memory(), collect_memory() (已存在,整合) +├── sys.py # collect_sys_stats(), collect_threads() +├── io.py # collect_io_slices(), collect_input_events() +├── block.py # collect_block_events() +├── compose.py # collect_compose_slices() +├── startup.py # StartupAnalyzer (已存在,保持) +├── sql_utils.py # 公共 SQL 查询工具(参数化、批量查询、CTE 模板) +└── types.py # PerfSummary dataclass, 公共类型定义 +``` + +**关键设计**: +- `PerfettoCollector` 保留为入口类,通过 Mixin 或组合模式组合各模块方法 +- 每个 `collect_*()` 方法可独立测试(只需 mock `TraceProcessor.query()`) +- `sql_utils.py` 集中管理 SQL 模板,统一参数化查询 + +**预期收益**: +- 单文件代码量从 2,345 行降到 ~300 行(核心类) + 各子模块 100-200 行 +- 可独立测试每个 collect 方法 +- SQL 安全修复可集中在一个文件 +- 新增 collect 方法只需新建文件 + 注册到主类 + +**实施复杂度**: 中等。主要是代码搬迁 + import 调整。 + +**优先级**: P0 + +--- + +### 4.2 改进二:AgentState 数据类型优化 + +**目标**: 消除 pipeline 内部 JSON 字符串的反复序列化/反序列化。 + +**方案**: + +```python +# graph/state.py +class AgentState(TypedDict): + messages: Annotated[list, operator.add] + perf_summary_raw: dict # ← 内部传递 dict(替代 str) + perf_summary: str # ← 仅在边界序列化(LLM prompt 输入) + perf_hints: str # ← 新增:缓存 compute_hints 结果 + perf_analysis: str + attribution_data: str + attribution_result: str + trace_duration_ms: int + trace_target_process: str + skip_wait: bool + _route: str + _trace_path: str +``` + +**迁移路径**: +1. `collector_node` 中 `PerfettoCollector.summarize()` 返回 `dict`,同时生成 JSON string +2. `perf_summary_raw` 存 dict,`perf_summary` 存 str(向后兼容) +3. analyzer / attributor / reporter 优先读 `perf_summary_raw` +4. `compute_hints()` 结果存入 `perf_hints`,reporter 直接复用 + +**预期收益**: +- 消除 3-4 次重复 `json.loads()` 调用 +- `compute_hints()` 只调用一次 +- 消费方获得类型提示 +- 对大 trace 文件(perf JSON > 100KB)性能提升明显 + +**实施复杂度**: 中等。需要修改 state 定义和所有消费节点。 + +**优先级**: P1 + +--- + +### 4.3 改进三:统一 LLM 实例管理(LLMFactory) + +**目标**: 集中管理所有 LLM 实例的创建和配置。 + +**方案**: + +```python +# llm_factory.py(新文件) +import threading +from langchain_openai import ChatOpenAI +from smartinspector.config import get_llm_kwargs + +class LLMFactory: + """Centralized LLM instance management.""" + _instances: dict[str, ChatOpenAI] = {} + _lock = threading.Lock() + + @classmethod + def get(cls, role: str = "default", **overrides) -> ChatOpenAI: + """Get or create an LLM instance for the given role. + + Args: + role: "default" | "attributor" | "router" | "streaming" + **overrides: Additional kwargs passed to ChatOpenAI + """ + key = f"{role}:{frozenset(overrides.items())}" + if key not in cls._instances: + with cls._lock: + if key not in cls._instances: + kwargs = get_llm_kwargs(role=role if role != "default" else None) + kwargs.update(overrides) + cls._instances[key] = ChatOpenAI(**kwargs) + return cls._instances[key] + + @classmethod + def get_with_tools(cls, role: str, tools: list, **overrides): + """Get LLM with bound tools.""" + return cls.get(role, **overrides).bind_tools(tools) + + @classmethod + def get_structured(cls, role: str, schema, **overrides): + """Get LLM with structured output.""" + return cls.get(role, **overrides).with_structured_output(schema) + + @classmethod + def reset(cls): + """Clear all instances (for testing).""" + with cls._lock: + cls._instances.clear() +``` + +**迁移路径**: +1. 创建 `llm_factory.py` +2. 逐个替换 6 个 Agent 的 LLM 初始化代码 +3. `attributor.py` 的 `_structured_ok` 探测逻辑改为初始化时一次性完成 +4. 统一 Agent 实现模式为 `bind_tools` + 手动 dispatch(attributor 的模式) + +**预期收益**: +- LLM 配置变更只需改一处 +- 统一 temperature、max_tokens 等参数管理 +- Token 配额和限流可集中管理 +- 测试可轻松 mock LLM + +**实施复杂度**: 中低。逐个文件替换,无功能变更。 + +**优先级**: P1 + +--- + +### 4.4 改进四:SI$ Tag 统一解析 + +**目标**: 消除 `commands/attribution.py` 中的重复解析逻辑。 + +**方案**: + +```python +# commands/attribution.py +from dataclasses import dataclass + +@dataclass +class SITag: + """Structured representation of an SI$ tag.""" + tag_type: str # "block", "RV", "inflate", "view", "handler", "compose", "net", "db", "img" + class_name: str # 短类名 + method_name: str # 方法名 + fqn: str # 完全限定名(可能为空) + search_type: str # "java", "xml", "system" + io_type: str | None # "network" | "database" | "image" | None + raw_name: str # 原始 tag + extras: dict # 额外字段(view_id, layout, duration 等) + +def parse_si_tag(name: str) -> SITag | None: + """Single-pass SI$ tag parser. + + 替代 extract_class() + extract_method() + extract_fqn() 三次独立解析。 + """ + ... # 单次遍历,一次解析所有字段 +``` + +**预期收益**: +- 代码量从 ~1,164 行降至 ~400 行 +- 新增 tag 类型只需添加一个分支 +- 类型安全:消费方通过 `SITag` dataclass 获取字段 +- 可独立测试 + +**实施复杂度**: 低。纯重构,无功能变更。 + +**优先级**: P1 + +--- + +### 4.5 改进五:测试基础设施搭建 + +**目标**: 建立核心模块的测试保护网,目标覆盖率 50%+。 + +**方案(按优先级排序)**: + +| 优先级 | 模块 | 测试类型 | 理由 | +|--------|------|----------|------| +| P0 | `agents/deterministic.py` | 单元测试 | 纯函数,最容易测试,覆盖核心业务逻辑 | +| P0 | `commands/attribution.py` (parse_si_tag) | 单元测试 | 纯函数,tag 解析是最核心的业务逻辑 | +| P1 | `graph/state.py` (_pass_through, node_error_handler) | 单元测试 | 确保状态传递和错误处理正确 | +| P1 | `agents/verifier.py` | 单元测试 | 确保验证逻辑正确 | +| P1 | `graph/builder.py` | 集成测试 | 验证图构建和路由正确性 | +| P2 | `collector/perfetto.py` (各 collect_*) | 单元测试 (mock SQL) | 需要 mock TraceProcessor | +| P2 | `agents/attributor.py` (fast-path) | 单元测试 (mock tools) | 验证归因逻辑 | + +**测试基础设施**: +- 使用 `pytest` + `pytest-mock` +- 创建 `tests/conftest.py` 提供公共 fixture(mock TraceProcessor、sample perf JSON、sample SI$ tags) +- CI 中运行 `pytest` 作为 gate check + +**预期收益**: +- 重构和架构改进有安全网 +- 回归检测自动化 +- 新功能开发可先写测试 + +**实施复杂度**: 低到中。纯函数测试容易,mock 测试需要设计 fixture。 + +**优先级**: P0 + +--- + +### 4.6 改进六:扩展性改进 — 平台抽象层 + +**目标**: 为 HarmonyOS / iOS 平台扩展建立清晰的架构基础。 + +**当前问题**: 所有平台相关逻辑(adb、Perfetto、Android SDK hook)硬编码在 collector/ 和 agents/ 中。添加 HarmonyOS 支持(hdc、hitrace)需要在多处添加 if/else 分支。 + +**方案**: + +``` +collector/ +├── base.py # BaseCollector 抽象基类 +│ └── abstract methods: +│ ├── pull_trace() → str +│ ├── get_target_process() → str +│ └── get_device_info() → dict +├── android/ +│ ├── perfetto.py # PerfettoCollector(BaseCollector) +│ ├── sched.py +│ ├── cpu.py +│ └── ... +├── harmonyos/ +│ ├── hitrace.py # HitraceCollector(BaseCollector) +│ └── ... +└── types.py # PerfSummary dataclass (平台无关) +``` + +**关键设计原则**: +1. `BaseCollector` 定义标准采集接口和输出格式(`PerfSummary`) +2. 平台特有逻辑封装在各自的 collector 子类中 +3. `PerfSummary` dataclass 是平台无关的中间表示 +4. 下游的 analyzer/attributor/reporter 不关心数据来自哪个平台 + +**实施路径**: 长期演进,当前可在代码中预留接口(添加 TODO 标记),不急于实现。 + +**优先级**: P2(长期演进方向) + +--- + +### 4.7 改进七:Orchestrator 路由 Prompt 外置 + +**目标**: 将 orchestrator 的路由 prompt 从 Python 代码内联移到 `prompts/` 目录。 + +**当前问题**: `orchestrator.py` 中的 `_ROUTE_PROMPT`(65 行)和 `_FALLBACK_SYSTEM`(10 行)直接内联在 Python 代码中。按项目规则(CLAUDE.md "Prompt 管理规则"),超过 3 行的 prompt 必须抽取到 `prompts/` 目录。 + +**方案**: +- 创建 `prompts/route-classification.txt` +- 创建 `prompts/fallback-system.txt` +- `orchestrator.py` 中使用 `load_prompt("route-classification")` 加载 + +**预期收益**: +- 符合项目 Prompt 管理规则 +- 路由 prompt 可独立编辑和版本管理 +- A/B 测试不同路由策略时更方便 + +**实施复杂度**: 极低。 + +**优先级**: P2 + +--- + +## 五、实施路线图 + +### Phase 1: 基础加固(建议立即开始) + +| # | 项目 | 涉及文件 | 预期收益 | +|---|------|----------|----------| +| 1 | 测试基础设施 + deterministic 测试 | 新建测试文件 | 重构安全网 | +| 2 | SI$ Tag 统一解析 (parse_si_tag) | `commands/attribution.py` | 代码量减少 60%+ | +| 3 | config.py 提取 `_env_int()` | `config.py` | 消除 6 个重复函数 | +| 4 | Orchestrator prompt 外置 | `orchestrator.py` → `prompts/` | 符合项目规范 | +| 5 | formatter.py 内部 import 移到顶部 | `formatter.py` | 代码规范 | + +### Phase 2: 架构改进(基础加固完成后) + +| # | 项目 | 涉及文件 | 预期收益 | +|---|------|----------|----------| +| 6 | LLMFactory 统一管理 | 新建 `llm_factory.py` + 6 个 Agent 文件 | 统一 LLM 实例管理 | +| 7 | Collector 模块化拆分 | `collector/perfetto.py` → 10 个子模块 | 单文件从 2345 行降至 ~300 行 | +| 8 | AgentState 数据类型优化 | `state.py` + 所有消费节点 | 消除重复 JSON 序列化 | +| 9 | SQL 参数化查询 | `collector/sql_utils.py` | 消除 SQL 注入风险 | +| 10 | compute_hints 缓存 | `state.py` + `perf_analyzer.py` + `formatter.py` | 消除重复计算 | + +### Phase 3: 扩展性建设(长期演进) + +| # | 项目 | 涉及文件 | 预期收益 | +|---|------|----------|----------| +| 11 | 平台抽象层(BaseCollector) | `collector/base.py` | 多平台扩展基础 | +| 12 | Bridge Server 类封装 | `ws/bridge_server.py` | 消除全局状态 | +| 13 | Agent API 统一(bind_tools) | `explorer.py` + `android.py` | 统一 Agent 模式 | +| 14 | perf_analyzer 智能截断 | `agents/perf_analyzer.py` | LLM token 减少 30-50% | + +--- + +## 六、风险与注意事项 + +### 6.1 重构风险控制 + +1. **先测试后重构**: Phase 2 的每项改进都应在 Phase 1 的测试基础上进行 +2. **增量变更**: 每个改进独立一个 commit,不混合多个改进 +3. **向后兼容**: AgentState 新增字段而非修改现有字段,确保现有功能不受影响 +4. **CI 验证**: 每次变更后运行 `uv run smartinspector --help` 验证入口正常 + +### 6.2 架构约束 + +1. **Pipeline Architecture Rule**: 所有新功能必须通过 LangGraph graph 执行 +2. **Logging Standard**: 禁止 `import logging`,统一使用 `info_log()` / `debug_log()` +3. **Prompt 管理**: 超过 3 行的 prompt 必须外置到 `prompts/` 目录 +4. **文档同步**: 新增命令/功能必须同步更新 CLAUDE.md + +### 6.3 不建议做的事 + +1. **不建议引入新的编排框架**: LangGraph 已经满足需求,引入新框架增加复杂度 +2. **不建议将 Collector 改为异步**: 当前同步模型简单可靠,异步改造收益不大 +3. **不建议过早抽象平台层**: 等 HarmonyOS 需求明确后再做,避免过度设计 +4. **不建议合并 Agent 和 Node 层**: 当前两层分离(Node 负责状态管理,Agent 负责业务逻辑)是合理的 + +--- + +## 七、总结 + +AppSmartInspector 的核心架构(LangGraph pipeline + deterministic pre-computation + fast-path attribution)设计合理,是一个在"LLM 效率"和"分析准确性"之间取得良好平衡的系统。 + +**最需要改进的三个领域**: +1. **测试覆盖**(从 9% → 50%+):是所有其他改进的基础 +2. **Collector 模块化**(2,345 行 → 多模块):是可维护性的关键瓶颈 +3. **LLM 实例统一管理**(6 处碎片化 → LLMFactory):是扩展性的前提 + +**最具价值的改进路径**: 先建立测试 → 再重构 Collector → 再统一 Agent 管理。这条路径确保每一步都有安全网,每一步都使代码更易于维护和扩展。 diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..aafbee7 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,70 @@ +# SmartInspector 优化待办 + +> 自动生成于 2026-04-14,由代码分析扫描得出 + +## 🔴 高优先级 + +- [ ] **测试覆盖率严重不足**:26个模块中仅有3个有测试(collector、perfetto、high_priority_fixes),测试覆盖率约12%。优先补充核心模块测试:token_tracker、orchestrator、attributor、reporter(来源:2026-04-14) + - 优先级:高 + - 模块:全项目 + +- [ ] **collector/perfetto.py 过大(1340行)**:单文件承担了trace采集、SQL查询、数据解析、格式化等多项职责,需要拆分为多个子模块(来源:2026-04-14) + - 优先级:高 + - 模块:collector/perfetto.py + +- [ ] **commands/attribution.py 过大(789行)**:归因命令逻辑复杂,建议拆分为handler、formatter、presenter等子模块(来源:2026-04-14) + - 优先级:高 + - 模块:commands/attribution.py + +## 🟡 中优先级 + +- [ ] **graph/nodes/__init__.py 缺少模块导出**:analyzer、android、attributor、collector、explorer、orchestrator 均未在 __init__.py 中导出,影响模块可发现性(来源:2026-04-14) + - 优先级:中 + - 模块:graph/nodes/__init__.py + +- [ ] **缺少错误处理统一策略**:119个except块,但没有统一的错误处理框架。建议引入自定义异常类层次结构,区分可恢复错误和致命错误(来源:2026-04-14) + - 优先级:中 + - 模块:全项目 + +- [ ] **Reporter token估算精度低**:使用 `len(content) / 1.5` 粗略估算CJK token,误差较大。建议使用tiktoken或模型自带的token计数器(来源:2026-04-14) + - 优先级:中 + - 模块:graph/nodes/reporter/__init__.py + +- [ ] **Reporter输出token估算粗糙**:`len(full_content) // 3` 估算输出token不够准确,建议从response.usage_metadata直接获取(来源:2026-04-14) + - 优先级:中 + - 模块:graph/nodes/reporter/generator.py + +- [ ] **TokenTracker缺少reset确认机制**:全局单例reset没有安全检查,多线程场景下可能误操作导致统计数据丢失(来源:2026-04-14) + - 优先级:中 + - 模块:token_tracker.py + +- [ ] **缺少配置验证**:config.py(170行)没有对API Key、模型名称等配置项做格式和有效性校验(来源:2026-04-14) + - 优先级:中 + - 模块:config.py + +## 🟢 低优先级 + +- [ ] **缺少类型注解覆盖率检查**:部分函数缺少返回值类型注解,建议加入mypy pre-commit hook(来源:2026-04-14) + - 优先级:低 + - 模块:全项目 + +- [ ] **缺少日志分级策略**:debug_log函数存在但没有统一的日志级别配置,生产环境可能输出过多调试信息(来源:2026-04-14) + - 优先级:低 + - 模块:tools/debug_log(如存在) + +- [ ] **ws/server.py(340行)缺少WebSocket连接状态管理**:建议增加连接池管理和断线重连机制(来源:2026-04-14) + - 优先级:低 + - 模块:ws/server.py + +- [ ] **CLI缺少命令补全和帮助文档**:graph/cli.py 没有实现shell自动补全和详细的命令帮助文档(来源:2026-04-14) + - 优先级:低 + - 模块:graph/cli.py + +- [ ] **agents/deterministic.py(321行)和agents/attributor.py(464行)可抽取公共基类**:两个Agent有相似的状态管理逻辑,可抽象为BaseAgent(来源:2026-04-14) + - 优先级:低 + - 模块:agents/ + +--- + +*共 14 条待办:高 3 / 中 6 / 低 5* +*生成工具:OpenClaw 自动代码扫描* diff --git a/docs/architecture-improvement-spec.md b/docs/architecture-improvement-spec.md new file mode 100644 index 0000000..0ea2f5f --- /dev/null +++ b/docs/architecture-improvement-spec.md @@ -0,0 +1,590 @@ +# SmartInspector 架构改进规范 + +> 审查日期: 2026-04-24 +> 审查范围: src/smartinspector/ 全部 45 个 Python 源文件 +> 审查人: 架构审查 Agent + +--- + +## 1. 现状评估 + +### 1.1 架构健康度总评分: 7.2 / 10 + +项目整体架构设计合理,LangGraph pipeline 模式清晰,模块职责划分得当。deterministic pre-computation 层是一个亮点设计,有效减少了 LLM token 消耗。但在 SQL 查询性能、资源管理、错误处理一致性等方面存在改进空间。 + +### 1.2 各模块质量评分 + +| 模块 | 评分 | 说明 | +|------|------|------| +| `graph/state.py` | 9/10 | 简洁清晰的 AgentState 定义,`_pass_through` 和 `node_error_handler` 设计得当 | +| `agents/deterministic.py` | 9/10 | 纯计算层设计优秀,与 LLM 职责分离明确 | +| `graph/builder.py` | 9/10 | 图构建清晰,路由映射完备 | +| `collector/perfetto.py` | 7/10 | 功能完整但有 SQL 注入风险、N+1 查询问题、资源管理不完善 | +| `agents/attributor.py` | 7/10 | fast-path 优化到位,但 LLM 单例管理混乱,_structured_ok 全局可变状态 | +| `graph/nodes/collector.py` | 8/10 | WS 集成良好,但缺少对 collector_node 的 `@node_error_handler` 装饰 | +| `graph/nodes/reporter/` | 7/10 | 截断策略合理,但 formatter 和 generator 共享 LLM 实例设计不当 | +| `commands/attribution.py` | 8/10 | SI$ tag 解析完备,匿名内部类处理周到 | +| `ws/server.py` | 7/10 | 单例模式正确,但缺少重连/心跳超时后的自动恢复 | +| `ws/bridge_server.py` | 6/10 | 全局可变状态过多,trace path 管理不够健壮 | +| `config.py` | 8/10 | 简洁实用,env var 解析一致 | +| `token_tracker.py` | 9/10 | 线程安全,API 设计清晰 | + +### 1.3 做得好的地方 + +1. **Deterministic pre-computation** (`agents/deterministic.py`): 将算术和分类逻辑从 LLM 中剥离,大幅减少 token 消耗,同时提高了结论的准确性。这是一个非常值得肯定的架构决策。 + +2. **Attributor fast-path** (`agents/attributor.py:137-327`): 对于简单的 Java 类搜索,绕过 LLM 直接执行 Glob→Grep→Read,既节省 token 又提高速度。partial fallback 机制(部分命中 fast-path,其余走 LLM)设计精巧。 + +3. **`node_error_handler` decorator** (`graph/state.py:62-84`): 统一的节点错误处理模式,避免任何单个节点的异常导致整个 pipeline 崩溃。 + +4. **SI$ tag system**: 完整的 tag 解析体系(`commands/attribution.py`),涵盖 RV、block、inflate、view、handler 等多种模式,匿名内部类的处理尤其周到。 + +5. **Trace collection degradation** (`collector/perfetto.py:1750-1831`): stdin-pipe → cat-pipe → cmdline 三级降级策略,覆盖了 SELinux 限制等设备兼容性问题。 + +6. **LRU file cache** (`agents/attributor.py:63-98`): 跨 group 共享的文件缓存,避免 attributor 多轮迭代中重复读取相同文件。 + +### 1.4 关键技术债务清单 + +| # | 技术债 | 影响 | 严重度 | 状态 | +|---|--------|------|--------|------| +| T1 | SQL 注入风险: f-string 拼接 SQL | 安全隐患 | 高 | 待修复 | +| T2 | thread_state N+1 查询 | 性能瓶颈 | 高 | ✅ 已修复 — 重写为 `__intrinsic_thread_state` 表 | +| T3 | `_structured_ok` 全局可变状态竞态 | 可靠性 | 中 | 待修复 | +| T4 | TraceProcessor 未在所有路径 close | 资源泄漏 | 高 | ✅ 已修复 — collector_node 使用 context manager | +| T5 | LLM 实例管理碎片化 | 维护性 | 中 | 待修复 | +| T6 | bridge_server 全局状态管理 | 可维护性 | 中 | 待修复 | +| T7 | 部分节点缺少 `@node_error_handler` | 可靠性 | 中 | ✅ 已修复 — attributor/reporter 已添加 | +| T8 | `_walk_call_chain` 逐行查询 | 性能 | 中 | ✅ 已修复 — 预加载 slice map | + +--- + +## 2. 性能优化建议 + +### P2-1: thread_state 分析的 N+1 查询问题 + +**问题描述**: `collect_thread_state()` 在 `__intrinsic_thread_state` 主路径中,对每个 slice 单独执行一次 SQL 查询(per-slice loop, `collector/perfetto.py:1264-1278`),且每次还需要额外查询 waker name(`collector/perfetto.py:1317-1324`)。20 个 slice 最多产生 20 + 20 = 40 次 SQL 查询。 + +**影响范围**: `collector/perfetto.py:1203-1338` + +**具体方案**: + +1. 将 per-slice 查询改为批量 CTE 查询,一次 SQL 获取所有 slice 的 thread_state 分布: + +```sql +WITH slices AS ( + SELECT name, ts, dur, ts + dur AS end_ts + FROM slice + WHERE name LIKE 'SI$%' AND dur > 1000000 + ORDER BY dur DESC LIMIT 20 +) +SELECT + s.name AS slice_name, + its.state, + SUM(its.dur) AS total_ns, + its.blocked_function, + its.io_wait, + its.waker_utid +FROM slices s +JOIN __intrinsic_thread_state its + ON its.utid = :main_utid + AND its.ts < s.end_ts + AND its.ts + its.dur > s.ts +GROUP BY s.name, its.state, its.blocked_function, its.io_wait, its.waker_utid +ORDER BY s.name, total_ns DESC +``` + +2. waker name 批量解析:收集所有 unique `waker_utid`,一次查询获取所有 name。 + +**预估收益**: SQL 查询次数从 ~40 降低到 2(主查询 + waker 批量查询),thread_state 分析耗时减少 80%+。 + +**实施复杂度**: 中等。需要重构 Python 端的结果聚合逻辑。 + +### P2-2: `_walk_call_chain` 的逐行查询 + +**问题描述**: `perfetto.py:2104-2133` 中的 `_walk_call_chain` 对每个 parent_id 单独执行 SQL 查询,最深 20 层。在 `query_frame_slices` 中最多 10 个 slice 调用,总计可能 200 次 SQL 查询。 + +**影响范围**: `collector/perfetto.py:2104-2133` + +**具体方案**: 预加载所有相关 slice 到内存 map,然后在 Python 中遍历 parent 链(与 `collect_view_slices` 中 `_build_chain` 的做法一致)。 + +```python +def _walk_call_chain_cached(tp, slice_id: int, slice_map: dict, seen: set) -> dict: + """Walk call chain using pre-loaded slice map.""" + chain_items = [] + current_id = slice_id + for _ in range(20): + if current_id is None or current_id in seen or current_id not in slice_map: + break + r = slice_map[current_id] + seen.add(current_id) + chain_items.append({...}) + current_id = r.get("parent_id") + ... +``` + +在 `query_frame_slices` 中,先一次性查询所有涉及的 slice(按 parent_id 递归),构建 map,然后遍历。 + +**预估收益**: `query_frame_slices` 中 SQL 查询从 ~200 降低到 ~5。 + +**实施复杂度**: 低。 + +### P2-3: PerfSummary.to_json() 的重复 JSON 序列化 + +**问题描述**: `PerfSummary.to_json()` 使用 `json.dumps(self.__dict__, indent=2)`。随后在 `collector_node` 中,这个 JSON 字符串又在 `formatter.py` 中被 `json.loads()` 解析,又被 `deterministic.py` 中的 `compute_hints` 再次 `json.loads()`。同一段数据被反复序列化/反序列化 3-4 次。 + +**影响范围**: `collector/perfetto.py:80-81`, `graph/nodes/reporter/__init__.py`, `graph/nodes/reporter/formatter.py`, `agents/deterministic.py` + +**具体方案**: 在 pipeline 内部传递 `dict` 而非 JSON 字符串,仅在边界(state 存储和 LLM prompt 输入)进行 JSON 序列化。 + +- `PerfettoCollector.summarize()` 返回 `dict` 而非调用 `to_json()` +- `collector_node` 存储到 state 时序列化一次 +- `formatter.py` 和 `deterministic.py` 直接接收 `dict` + +**预估收益**: 减少不必要的 CPU 消耗和内存分配,对大 trace 尤为明显。 + +**实施复杂度**: 中等。需要修改 state 中 `perf_summary` 的类型约定。 + +### P2-4: LLM token 效率 - perf_analyzer 截断策略 + +**问题描述**: `perf_analyzer.py:49` 将 `perf_json[:3000]` 截断后发送给 LLM,但 `compute_hints` 已经预计算了所有结论。原始 JSON 仅作为"参考"出现,3000 字符的限制过于武断,可能截断关键数据(如 thread_state),也可能包含大量无用数据(如 cpu_idle_samples 的时间序列)。 + +**影响范围**: `agents/perf_analyzer.py:46-49` + +**具体方案**: 根据 `hints` 的内容智能选择补充数据,而非盲截。例如: +- 如果 `_classify_severity` 输出非空,附上 view_slices 的 slowest_slices +- 如果 `_analyze_thread_state` 输出非空,附上 thread_state 完整数据 +- 始终附上 frame_timeline 汇总(FPS, jank count) + +**预估收益**: LLM 输入 token 减少 30-50%(去掉无用的时间序列数据),同时分析质量提升(保留关键数据)。 + +**实施复杂度**: 低。 + +### P2-5: reporter 重复调用 compute_hints + +**问题描述**: `compute_hints(perf_json)` 在 pipeline 中被调用两次: +1. `perf_analyzer.py:43` — analyzer 阶段 +2. `formatter.py:17` — reporter 阶段(通过 `format_perf_sections`) + +同一段 JSON 被解析和计算两次,完全浪费。 + +**影响范围**: `agents/perf_analyzer.py:43`, `graph/nodes/reporter/formatter.py:17` + +**具体方案**: 将第一次 `compute_hints` 的结果存入 state(新增 `perf_hints` 字段),reporter 直接复用。 + +**预估收益**: 避免重复 JSON 解析 + 6 个分析函数的重复计算。 + +**实施复杂度**: 低。 + +--- + +## 3. 架构改进建议 + +### A1: SQL 注入风险修复 + +**当前问题**: 多处 SQL 查询使用 f-string 拼接用户可控输入: + +- `collector/perfetto.py:130`: `WHERE name = '{package_name}'` +- `collector/perfetto.py:149-153`: `WHERE package_name = '{package_name}'` +- `collector/perfetto.py:166`: `WHERE uid = {uid}` +- `collector/perfetto.py:743`: `WHERE id IN ({id_list})` +- `collector/perfetto.py:965-971`: track_id IN 查询 +- `collector/perfetto.py:1183-1199`: thread_state 多处 f-string + +虽然 Perfetto trace_processor 是本地进程且数据来自设备 trace,不涉及网络攻击面,但 `package_name` 和 `target_process` 来自 CLI 输入/WS 配置,理论上有注入可能。 + +**目标架构**: 使用参数化查询或至少进行输入验证。 + +**迁移路径**: +1. 在 `PerfettoCollector.__init__` 中验证 `target_process` 格式(仅允许 `[a-zA-Z0-9._]`) +2. 对于 `id_list` 类查询,确保所有 ID 都是整数(`int()` 转换) +3. 对于 `utid` 等,已在内部生成,风险较低,但仍建议使用占位符 + +**风险评估**: 低风险修改,不影响功能。 + +### A2: TraceProcessor 资源管理 + +**当前问题**: `query_frame_slices()` (`collector/perfetto.py:1934-2039`) 创建了 `TraceProcessor` 实例并在 `finally` 中 close,这是正确的。但 `PerfettoCollector` 的 `_open()` 方法只在 `close()` 中释放,而 `summarize()` 调用链中如果中途异常,`_tp` 可能不会被关闭。 + +更重要的是,`PerfettoCollector` 用作 context manager 时如果 `summarize()` 抛异常(如 `collect_cpu_hotspots` 失败),`__exit__` 能正确清理。但在 `collector_node` 中(`graph/nodes/collector.py:184`): + +```python +collector = PerfettoCollector(trace_path, target_process=target_process) +summary = collector.summarize() +``` + +没有使用 `with` 语句,`close()` 从未被调用。这意味着 `trace_processor_shell` 子进程在 `summarize()` 完成后仍然运行。 + +**目标架构**: 使用 context manager 确保资源释放。 + +**迁移路径**: +```python +with PerfettoCollector(trace_path, target_process=target_process) as collector: + summary = collector.summarize() +``` + +**风险评估**: 安全修改,`summarize()` 内部的 try/except 已保证即使异常也有结果返回。 + +### A3: 统一 LLM 实例管理 + +**当前问题**: LLM 实例分散在多处,管理模式不一致: + +| 位置 | 模式 | 问题 | +|------|------|------| +| `orchestrator.py:39-47` | 全局 `_route_llm` | 单例,但被 reporter 的 `generate_report` 复用(不合理) | +| `attributor.py:100-130` | 全局 + Lock + 探测 | `_structured_ok` 全局变量,线程不安全 | +| `perf_analyzer.py:17-25` | 全局 + Lock | 独立实例 | +| `frame_analyzer.py:21-29` | 全局 + Lock | 独立实例 | +| `explorer.py:19-33` | 全局 + Lock | `create_agent` API(可能已废弃) | +| `android.py:16-31` | 全局 + Lock | 同上 | + +**目标架构**: 创建 `LLMFactory` 类集中管理 LLM 实例。 + +```python +# config.py 或新的 llm_factory.py +class LLMFactory: + _instances: dict[str, ChatOpenAI] = {} + _lock = threading.Lock() + + @classmethod + def get(cls, role: str = "default", **kwargs) -> ChatOpenAI: + key = f"{role}:{frozenset(kwargs.items())}" + if key not in cls._instances: + with cls._lock: + if key not in cls._instances: + cls._instances[key] = ChatOpenAI(**get_llm_kwargs(role=role, **kwargs)) + return cls._instances[key] +``` + +**迁移路径**: 逐个替换现有全局变量。注意 `attributor.py` 的 `_structured_ok` 探测逻辑需要特殊处理。 + +**风险评估**: 低风险,但需要测试各 LLM 实例的 temperature/max_tokens 配置是否保持一致。 + +### A4: agent 层 API 不一致 + +**当前问题**: +- `explorer.py:36` 和 `android.py:16` 使用 `langchain.agents.create_agent` — 这个 API 在 LangChain 中可能已废弃或不存在于当前版本 +- `attributor.py` 使用手动的 tool-call loop(`llm.bind_tools` + 手动 dispatch) +- `perf_analyzer.py` 和 `frame_analyzer.py` 使用单次 `llm.invoke` + +三种不同的 agent 实现模式增加了维护负担。 + +**目标架构**: 统一使用 `llm.bind_tools` + 手动 dispatch 模式(attributor 的模式已被验证最可控),或统一使用 LangGraph 的 `create_react_agent`。 + +**迁移路径**: 长期演进。当前优先修复 `create_agent` 的导入问题。 + +**风险评估**: 中等。需要验证 LangChain 版本兼容性。 + +### A5: 缺少 `@node_error_handler` 的节点 + +**当前问题**: 以下节点缺少 `@node_error_handler` 装饰: + +- `graph/nodes/collector.py:107` — `collector_node`: 内部有 try/except 但不返回 `_pass_through` 字段,可能导致下游节点读到空值 +- `graph/nodes/attributor.py:33` — `attributor_node`: 完全没有异常处理 +- `graph/nodes/reporter/__init__.py:21` — `reporter_node`: 完全没有异常处理 + +这意味着如果这些节点抛出未预期的异常,LangGraph 会直接终止整个 pipeline,用户看到的是原始 traceback 而非友好错误信息。 + +**目标架构**: 所有 graph node 使用 `@node_error_handler` 装饰。 + +**迁移路径**: 逐个添加装饰器,确保每个节点的异常都被捕获并转换为 AIMessage。 + +**风险评估**: 低风险。 + +### A6: bridge_server 全局状态管理 + +**当前问题**: `bridge_server.py:314-317` 使用模块级全局变量管理状态: + +```python +_active_bridge: BridgeServer | None = None +_active_trace_server = None +_cached_perf_summary: str = "" +_cached_attribution_result: str = "" +``` + +`start_bridge()` 中还局部变量覆盖了这些全局(`_perf_summary_cache` vs `_cached_perf_summary`),容易混淆。 + +**目标架构**: 将状态封装到 `BridgeManager` 类中,或至少统一变量命名。 + +**迁移路径**: 短期:统一变量命名。长期:封装到类。 + +**风险评估**: 低风险。 + +--- + +## 4. 可靠性改进 + +### R1: TraceProcessor 连接超时处理 + +**问题**: `PerfettoCollector._open()` (`collector/perfetto.py:100-108`) 设置了 `load_timeout=10` 秒,但没有重试机制。对于大 trace 文件(>100MB),首次加载可能超时。 + +**方案**: 添加重试逻辑,或根据文件大小动态调整超时。 + +```python +def _open(self) -> TraceProcessor: + if self._tp is not None: + return self._tp + file_size_mb = Path(self.trace_path).stat().st_size / 1024 / 1024 + timeout = max(10, int(file_size_mb / 10)) # 100MB -> 10s, 1GB -> 100s + config = TraceProcessorConfig(bin_path=self.shell_path, load_timeout=timeout) + self._tp = TraceProcessor(trace=self.trace_path, config=config) + return self._tp +``` + +**文件**: `collector/perfetto.py:100-108` + +### R2: WS 连接断开后的状态同步 + +**问题**: `ws/server.py:278-283` 的 `_handler` 在连接关闭时从 `_connections` 中移除,但没有清理 `_pending_acks`。如果 app 在 ACK 到达前断开,等待 ACK 的线程会永久阻塞直到 timeout。 + +**方案**: 在连接关闭时,set 所有 pending acks(标记为失败)。 + +```python +async def _handler(self, ws) -> None: + self._connections.add(ws) + try: + async for raw in ws: + ... + except websockets.exceptions.ConnectionClosed: + pass + finally: + self._connections.discard(ws) + # Release any pending ACKs for this connection + for msg_id, event in list(self._pending_acks.items()): + if not event.is_set(): + event.set() # Will return False from send_config/send_start_trace +``` + +**文件**: `ws/server.py:264-283` + +### R3: attributor 的 `_structured_ok` 竞态条件 + +**问题**: `attributor.py:55` 的 `_structured_ok` 是模块级全局变量,在 `_get_llm()` 中初始化,在 `_search_group()` 中修改为 `False`。如果有两个 attributor 调用并发执行(虽然当前 CLI 是单线程),一个修改 `_structured_ok` 可能影响另一个。 + +**当前风险评估**: 低。CLI 是单线程调用 attributor,但 frame_analyzer 的 bridge_server 调用在 executor 线程中,理论上可能并发。 + +**方案**: 将 `_structured_ok` 移入 `_get_llm()` 的 Lock 保护范围内,或在初始化时一次性探测并缓存。 + +**文件**: `agents/attributor.py:55, 863` + +### R4: subprocess 资源泄漏 + +**问题**: `TraceServer` (`collector/perfetto.py:1877`) 使用 `Popen` 启动 `trace_processor_shell`,如果 BridgeServer 异常退出,`TraceServer.stop()` 可能不被调用。`bridge_server.py:419-427` 的 `stop_bridge()` 虽然会清理,但依赖调用方正确调用。 + +**方案**: 为 `TraceServer` 添加 `__del__` 方法作为最后保障,或使用 `atexit` 注册清理。 + +```python +import atexit + +class TraceServer: + def __init__(self, ...): + atexit.register(self.stop) +``` + +**文件**: `collector/perfetto.py:1848-1919` + +### R5: JSON 解析失败的静默吞没 + +**问题**: 多处 `json.loads()` 在 try/except 中静默吞没错误,返回空值: + +- `perf_analyzer.py:46`: `compute_hints` 内 `json.loads` 失败返回 `""` +- `formatter.py:22`: `json.loads` 失败返回 `{}` +- `frame_analyzer.py:79`: `json.loads` 失败截断到前 2000 字符 + +这些静默失败可能隐藏重要错误,使得调试困难。 + +**方案**: 至少用 `logger.warning` 记录解析失败,不要完全静默。 + +**文件**: 多处 + +### R6: node_error_handler 中的 print + +**问题**: `graph/state.py:78` 的 `node_error_handler` 使用 `print()` 输出错误信息,而非 `logger.error()`。按照项目的 logging 标准,应该使用 logger。 + +```python +# 当前 +print(f" [{node_name}] ERROR: {e}", flush=True) + +# 应改为 +import logging +logging.getLogger(__name__).error("[%s] %s", node_name, e) +``` + +**文件**: `graph/state.py:78` + +--- + +## 5. 代码质量改进 + +### Q1: extract_class / extract_method 的重复解析模式 + +**问题**: `commands/attribution.py` 中 `extract_class()`, `extract_method()`, `extract_fqn()` 三个函数对同一个 SI$ tag 分别解析一次,且解析逻辑高度重复(每個函数都有相同的 `if body.startswith("block#"):` 等分支)。 + +**方案**: 创建统一的 `_parse_si_tag()` 函数,一次解析返回结构化结果: + +```python +@dataclass +class SITag: + tag_type: str # "block", "RV", "inflate", "view", "handler", ... + class_name: str + method_name: str + fqn: str + search_type: str # "java", "xml", "system" + raw_name: str + +def parse_si_tag(name: str) -> SITag: + """Single-pass SI$ tag parser.""" + ... +``` + +**文件**: `commands/attribution.py:15-401` + +### Q2: formatter.py 中 import 放在函数内部 + +**问题**: `graph/nodes/reporter/formatter.py:17` 在函数内部 `from smartinspector.agents.deterministic import compute_hints`,每次调用都执行 import 查找(虽然 Python 会缓存)。 + +**方案**: 移到文件顶部。如果存在循环依赖问题,说明模块拆分不合理。 + +**文件**: `graph/nodes/reporter/formatter.py:17`, `agents/perf_analyzer.py:42` + +### Q3: SIServer 单例模式的可测试性 + +**问题**: `ws/server.py` 的 `SIServer` 使用类级 `_instance` 和 `_lock` 实现单例。这使得单元测试难以注入 mock server。 + +**方案**: 添加 `reset()` 类方法用于测试,或使用依赖注入。 + +**文件**: `ws/server.py:58-63` + +### Q4: 测试覆盖 + +**问题**: 项目目前没有测试文件(`tests/` 目录为空或不存在)。 + +**建议的测试优先级**: + +1. **P0**: `commands/attribution.py` 的 SI$ tag 解析 — 纯函数,容易测试,覆盖最核心的业务逻辑 +2. **P0**: `agents/deterministic.py` 的 `compute_hints` — 纯函数,验证预计算逻辑正确性 +3. **P1**: `collector/perfetto.py` 的 SQL 查询逻辑(用 mock TraceProcessor) +4. **P1**: `graph/state.py` 的 `_pass_through` 和 `node_error_handler` +5. **P2**: 各 graph node 的输入/输出契约 + +### Q5: 代码重复 — block stack trace 关联 + +**问题**: `collect_block_events()` (`collector/perfetto.py:1169-1201`) 和 `_correlate_block_stacks_from_logcat()` (`collector/perfetto.py:2042-2101`) 实现了几乎相同的 bisect-based 时间戳关联逻辑。 + +**方案**: 提取公共函数 `_correlate_by_timestamp(sql_events, log_entries, match_window_ns)`。 + +**文件**: `collector/perfetto.py:1169-1201` 和 `collector/perfetto.py:2042-2101` + +### Q6: config.py 的 get_* 函数重复模式 + +**问题**: `config.py` 中 `get_tool_timeout()`, `get_read_max_lines()`, `get_read_max_bytes()`, `get_read_max_line_length()`, `get_report_max_tokens()`, `get_ws_ping_timeout()` 六个函数完全同构: + +```python +def get_xxx() -> int: + try: + return int(os.environ.get("SI_XXX", default)) + except (ValueError, TypeError): + return default +``` + +**方案**: 提取 `_env_int(key, default)` 辅助函数。 + +```python +def _env_int(key: str, default: int) -> int: + try: + return int(os.environ.get(key, str(default))) + except (ValueError, TypeError): + return default + +def get_tool_timeout() -> int: + return _env_int("SI_TOOL_TIMEOUT", 30) +``` + +**文件**: `config.py:122-170` + +--- + +## 6. 实施路线图 + +### P0(紧急)— 影响用户体验的问题 + +| # | 项目 | 涉及文件 | 说明 | 状态 | +|---|------|----------|------|------| +| P0-1 | TraceProcessor 资源泄漏 | `graph/nodes/collector.py:184` | 使用 `with` 语句确保 close | ✅ 已完成 | +| P0-2 | 缺少 `@node_error_handler` | `graph/nodes/attributor.py:33`, `graph/nodes/reporter/__init__.py:21` | 添加装饰器 | ✅ 已完成 | +| P0-3 | `_walk_call_chain` 性能 | `collector/perfetto.py:2104-2133` | 预加载 slice map 替代逐行查询 | ✅ 已完成 | + +> **P0 功能改进已于 2026-04-24 完成**,新增了以下功能: +> - IO Hooks 默认启用(`SI$net#`/`SI$db#`/`SI$img#`),IO 切片独立收集和归因 +> - 冷启动专项分析模式(`collector/startup.py` + `graph/nodes/startup.py`) +> - Headless/CI 模式(`headless.py` + CLI `--ci` 参数) +> - JSON 报告格式(`graph/nodes/reporter/json_formatter.py`) + +### P1(重要)— 架构层面的改进 + +| # | 项目 | 涉及文件 | 说明 | 状态 | +|---|------|----------|------|------| +| P1-1 | SQL 注入风险修复 | `collector/perfetto.py` 多处 | 输入验证 + 参数化 | 待修复 | +| P1-2 | thread_state N+1 查询 | `collector/perfetto.py:1203-1338` | 批量 CTE 查询 | ✅ 已完成 — 重写为 `__intrinsic_thread_state` 表 | +| P1-3 | 统一 LLM 实例管理 | 多文件 | 创建 LLMFactory | 待修复 | +| P1-4 | reporter 重复调用 compute_hints | `formatter.py:17`, `perf_analyzer.py:43` | state 中缓存 hints | 待修复 | +| P1-5 | block stack 关联代码重复 | `collector/perfetto.py` | 提取公共函数 | 待修复 | +| P1-6 | SI$ tag 统一解析 | `commands/attribution.py` | 创建 `parse_si_tag()` | 待修复 | +| P1-7 | node_error_handler print → logger | `graph/state.py:78` | 改用 logger.error | ✅ 已完成 — 全链路 logging 改造 | + +### P2(优化)— 性能和代码质量提升 + +| # | 项目 | 涉及文件 | 说明 | +|---|------|----------|------| +| P2-1 | JSON 反复序列化/反序列化 | pipeline 多处 | 内部传递 dict | +| P2-2 | perf_analyzer 智能截断 | `agents/perf_analyzer.py:46-49` | 基于 hints 选择补充数据 | +| P2-3 | config.py 重复模式 | `config.py:122-170` | 提取 `_env_int` | +| P2-4 | formatter.py 内部 import | `graph/nodes/reporter/formatter.py:17` | 移到文件顶部 | +| P2-5 | bridge_server 全局状态 | `ws/bridge_server.py:314-317` | 封装到类 | +| P2-6 | _structured_ok 竞态 | `agents/attributor.py:55` | Lock 保护 | +| P2-7 | WS 连接断开清理 | `ws/server.py:278-283` | 清理 pending_acks | + +### P1 Feature Roadmap — 功能改进 + +| # | 项目 | 说明 | 涉及模块 | 状态 | +|---|------|------|----------|------| +| P1-1 | Compose 重组追踪 | 追踪 Jetpack Compose 重组次数和耗时,定位不必要的 recomposition | Android SDK + collector | ✅ 已完成 | +| P1-2 | 内存分配分析 | 基于 `android.java_hprof` 数据源分析内存分配热点,定位内存抖动和泄漏 | collector + 新 agent | ✅ 已完成 | +| P1-3 | 历史对比与趋势 | 多次分析结果对比,生成 before/after 报告和性能趋势图 | reporter + persistence | ✅ 已完成 | +| P1-4 | 智能一键分析 | 基于历史数据和 device profile 自动选择最佳分析策略 | orchestrator | ✅ 已完成 | +| P1-5 | ExtraHook 参数自动推断 | 分析代码结构自动推荐 Hook 配置,减少手动配置 | agents + commands | ✅ 已完成 | + +### P3(可选)— 长期演进方向 + +| # | 项目 | 说明 | +|---|------|------| +| P3-1 | 统一 agent 实现模式 | explorer/android 使用 `create_agent`,改为 `bind_tools` + 手动 dispatch | +| P3-2 | 添加测试覆盖 | 从 attribution.py 和 deterministic.py 开始 | +| P3-3 | TraceProcessor 连接池 | 对于高频查询场景(bridge),复用 HTTP 模式的 TraceServer | +| P3-4 | 流式 attributor | 将 attributor 的 LLM 调用改为流式,减少用户等待感知 | +| P3-5 | 可插拔 LLM 后端 | 支持本地模型(Ollama)和不同 API 格式,减少 DeepSeek 依赖 | +| P3-6 | SIServer 可测试性重构 | 添加 reset() 方法或使用依赖注入 | + +--- + +## 附录: 文件级问题索引 + +| 文件 | 行号 | 问题 | 严重度 | 状态 | +|------|------|------|--------|------| +| `collector/perfetto.py` | 100-108 | TraceProcessor 超时不可配置 | P1 | 待修复 | +| `collector/perfetto.py` | 130 | SQL f-string 拼接 | P1 | 待修复 | +| `collector/perfetto.py` | 743 | SQL IN clause f-string | P1 | 待修复 | +| `collector/perfetto.py` | 1264-1278 | N+1 SQL 查询 | P1 | ✅ 已修复 — 重写为 `__intrinsic_thread_state` | +| `collector/perfetto.py` | 1877-1919 | TraceServer 无 atexit 清理 | P2 | 待修复 | +| `collector/perfetto.py` | 2104-2133 | 逐行查询 call chain | P0 | ✅ 已修复 — 预加载 slice map | +| `graph/state.py` | 78 | print 而非 logger | P1 | ✅ 已修复 — 全链路 logging 改造 | +| `graph/nodes/collector.py` | 184 | 缺少 `with` context manager | P0 | ✅ 已修复 | +| `graph/nodes/attributor.py` | 33 | 缺少 `@node_error_handler` | P0 | ✅ 已修复 | +| `graph/nodes/reporter/__init__.py` | 21 | 缺少 `@node_error_handler` | P0 | ✅ 已修复 | +| `graph/nodes/reporter/formatter.py` | 17 | 函数内部 import | P2 | 待修复 | +| `graph/nodes/reporter/generator.py` | 19 | 复用 orchestrator 的 `_get_route_llm()` | P1 | 待修复 | +| `agents/attributor.py` | 55 | `_structured_ok` 全局可变状态 | P2 | 待修复 | +| `agents/perf_analyzer.py` | 49 | 武断截断 perf_json[:3000] | P2 | 待修复 | +| `commands/attribution.py` | 全文件 | 重复的 tag 解析逻辑 | P1 | 待修复 | +| `ws/server.py` | 278-283 | 连接断开未清理 pending_acks | P2 | 待修复 | +| `ws/bridge_server.py` | 314-317 | 模块级全局状态 | P2 | 待修复 | +| `config.py` | 122-170 | 重复的 get_* 函数模式 | P2 | 待修复 | diff --git a/docs/bug-fixes-2026-04-28.md b/docs/bug-fixes-2026-04-28.md new file mode 100644 index 0000000..6731478 --- /dev/null +++ b/docs/bug-fixes-2026-04-28.md @@ -0,0 +1,56 @@ +# Bug Fixes — 日志规范、Startup包名、Trace复用 + +## Bug 1: print() 替换为 logger + +**问题**: 代码中大量使用 `print()` 输出 info/warning 信息到终端,应该统一使用 `logging.getLogger(__name__)` 的 `logger.info()` / `logger.warning()`。 + +**规则(已在 CLAUDE.md 中加粗强调)**: +- 所有 info/warning 级别的日志必须使用 `logger.info()` / `logger.warning()`,禁止 `print()` +- `print()` 仅允许用于用户面向的交互式输出(CLI 提示、进度条、表格等),即用户在终端里需要看到的内容 +- debug 日志使用 `debug_log(category, message)` +- error 日志使用 `logger.error()` + +**需要修改的文件**: +- `src/smartinspector/graph/cli.py` — print(f" Warning: ...") → logger.warning() +- `src/smartinspector/commands/trace.py` — print(f" Warning: ...") → logger.warning() +- `src/smartinspector/commands/hook.py` — print(f" Warning: ...") → logger.warning() +- `src/smartinspector/ws/bridge_server.py` — print(" [bridge] WARNING: ...") → logger.warning() +- 其他所有文件中 print() 输出 info/warning 的地方 + +**注意**: 不要改用户面向的进度输出(如 "正在采集..."、"分析完成" 等),这些保留 print()。 + +## Bug 2: Startup 分析不支持包名传递 + adb am start 后 trace 报错 + +**问题**: +1. 自然语言说"分析冷启动"或"冷启动耗时"时,如果没有指定包名,collector 会跳过自动 adb launch +2. adb am start 在 Perfetto trace 期间执行时,可能导致 pipe IO close 错误 + +**修复**: +1. 在 orchestrator 检测到 startup 意图时,从 `state["trace_target_process"]` 或 `perfetto_config` 读取包名。如果都没有,提示用户通过 `/config target_process com.xxx.xxx` 设置包名 +2. 确保 `_adb_launch_app()` 在 Perfetto 的 `on_record_start` 回调中执行,且不阻塞 trace 进程。检查是否有子进程竞争或 pipe 问题 + +**涉及文件**: +- `src/smartinspector/graph/nodes/orchestrator.py` — startup 路由时检查包名 +- `src/smartinspector/graph/nodes/collector.py` — on_record_start 回调的 pipe 问题 +- `src/smartinspector/collector/perfetto.py` — pull_trace_from_device 的 on_record_start 执行时机 + +## Bug 3: /full 命令和自然语言全量分析复用上次 trace + +**问题**: collector 节点在 state 中保存 `_trace_path`,下次执行 full 时如果 state 中的 `_trace_path` 指向的文件仍然存在,`preloaded_trace` 检测会复用旧 trace 文件,跳过设备采集。 + +**修复**: collector 节点开头清空 state 中的旧数据: +```python +# 清空上次分析数据,强制重新采集 +return_values = { + "perf_summary": "", + "perf_analysis": "", + "attribution_data": "", + "attribution_result": "", + "_trace_path": "", +} +``` + +**注意**: 清空 `_trace_path` 就够了,因为 `preloaded_trace` 检查依赖它。不需要清空用户消息历史。 + +**涉及文件**: +- `src/smartinspector/graph/nodes/collector.py` — collector_node 开头 diff --git a/docs/call-stack-attribution-design.md b/docs/call-stack-attribution-design.md new file mode 100644 index 0000000..a8f5295 --- /dev/null +++ b/docs/call-stack-attribution-design.md @@ -0,0 +1,622 @@ +# SI 归因系统改进方案:调用栈精确归因 + +## 1. 现状分析 + +### 1.1 当前归因流程 + +``` +Perfetto trace (.pb) + ↓ collector/perfetto.py :: collect_view_slices() + ↓ SQL 查询 slice 表,获取 id/name/ts/dur/depth/parent_id +view_slices JSON + ↓ commands/attribution.py :: extract_attributable_slices() + ↓ 提取 SI$ 前缀 → 解析 class_name + method_name +attributable 列表 [{class_name, method_name, dur_ms, ...}] + ↓ agents/attributor.py :: run_attribution() → _search_group() + ↓ LLM 调用 Glob → Grep → Read 工具 +源码归因结果 [{file_path, line_start, line_end, source_snippet}] +``` + +### 1.2 关键缺陷 + +**问题一:调用点歧义** + +当一个方法(如 `Adapter.onBindViewHolder`)在多个文件中被调用,或同一文件中多处调用时: +- 系统只能定位方法定义位置,无法确定是**哪次调用**导致性能问题 +- 归因会列出所有匹配位置,LLM 需要猜测哪个是热点 +- 多余的搜索结果浪费 token + +**问题二:上下文缺失** + +LLM 搜索 `Adapter.onBindViewHolder` 时不知道: +- 这个调用发生在哪个 Activity/Fragment 的上下文中 +- 是哪个 RecyclerView 实例触发的(如果有多个 RV) +- 在 doFrame 的哪个子阶段(measure/layout/draw) + +**问题三:数据已采集但未利用** + +`collector/perfetto.py` 的 `collect_view_slices()` 已经: +- 查询了每个 slice 的 `parent_id`(第 607 行) +- 构建了 `children_map` 父子关系映射(第 759-765 行) +- 构建了 `call_chains` 调用链(第 805-821 行) +- 甚至向上回溯了祖父节点(第 650-664 行) + +但 `extract_attributable_slices()` 完全没有使用这些调用栈信息。 + +### 1.3 当前数据流中的信息断裂点 + +| 数据阶段 | 调用栈信息 | 是否传递给下游 | +|----------|-----------|---------------| +| `collect_view_slices()` SQL 查询 | `parent_id` 已获取 | ✅ 存入 slice dict | +| `collect_view_slices()` 调用链构建 | `call_chains` 已构建 | ✅ 写入 view_slices JSON | +| `extract_attributable_slices()` | **未读取** `call_chains` | ❌ 断裂点 | +| `_build_group_prompt()` | **未包含** 父链上下文 | ❌ 断裂点 | +| `_compute_call_chain_distribution()` | 读取了 `call_chains` | ✅ 但仅用于预计算提示 | +| `prompts/attributor.txt` | **无** 调用栈搜索指引 | ❌ 断裂点 | + +## 2. 改进方案设计 + +### 2.1 核心思路 + +**利用 Perfetto slice 的 parent_id 链构建调用栈上下文,将"方法定义级别搜索"升级为"调用点级别搜索"。** + +具体策略: +1. 在 `extract_attributable_slices()` 阶段,利用 `view_slices.call_chains` 和 `slowest_slices` 中的 `parent_id`,为每个可归因 slice 提取完整的父链上下文 +2. 将父链上下文编码为 `call_context` 字段,传递给 attributor agent +3. Agent 搜索时利用上下文缩小搜索范围(如知道父 Activity 名、RV 实例 ID 等) +4. 对无法通过上下文区分的场景,提供优先级排序而非全部列出 + +### 2.2 技术方案 + +#### 改动一:`extract_attributable_slices()` 增加调用栈提取 + +**文件**: `src/smartinspector/commands/attribution.py` + +**当前代码**(第 552-707 行)只处理 `slowest_slices`、`summary`、`rv_instances`、`block_events`,未读取 `call_chains`。 + +**改进**:在处理 `slowest_slices` 时,利用 `call_chains` 数据构建每个 slice 的调用上下文。 + +```python +# 新增函数 +def _build_parent_contexts(view_slices: dict) -> dict[int, str]: + """为每个 slowest_slice 构建 parent chain 上下文摘要。 + + 利用 collect_view_slices() 已构建的 call_chains 数据和 slice 的 parent_id, + 生成精简的调用上下文字符串,用于辅助 attributor agent 精确定位。 + + Returns: + dict: slice_name → context_string 映射 + """ + slices_data = view_slices.get("slowest_slices", []) + call_chains = view_slices.get("call_chains", []) + + # 从 call_chains 提取 name → chain 映射 + chain_map: dict[str, list[str]] = {} + for cc in call_chains: + name = cc.get("name", "") + chain = cc.get("chain", []) + if name and chain: + chain_map[name] = chain + + # 从 slices 数据构建 parent_id → slice_name 映射 + # 需要从原始 slice 数据获取 parent_id(slowest_slices 中有 parent_id) + slice_by_id: dict[int, dict] = {} + for s in slices_data: + sid = s.get("id") + if sid is not None: + slice_by_id[sid] = s + + contexts: dict[str, str] = {} + for s in slices_data: + name = s.get("name", "") + if not name.startswith("SI$"): + continue + + # 策略1: 使用 call_chains 中的预构建链 + if name in chain_map: + chain = chain_map[name] + # chain 是 [root, ..., leaf],取倒数2-4层作为上下文 + context_parts = _extract_context_from_chain(chain) + if context_parts: + contexts[name] = " → ".join(context_parts) + continue + + # 策略2: 从 parent_id 向上回溯(call_chains 未覆盖的 slice) + parent_chain = _walk_parent_chain(s, slice_by_id, max_depth=5) + if parent_chain: + context_parts = _extract_context_from_chain(parent_chain) + if context_parts: + contexts[name] = " → ".join(context_parts) + + return contexts + + +def _extract_context_from_chain(chain: list[str]) -> list[str]: + """从调用链中提取有意义的上下文节点。 + + 过滤掉系统标签(doFrame, Choreographer 等),保留 SI$ 自定义标签和 + 关键系统标签(作为阶段标识)。 + """ + # 阶段标识:doFrame → performMeasure/performLayout/performDraw + STAGE_KEYWORDS = { + "doFrame": "帧渲染", + "performMeasure": "measure阶段", + "performLayout": "layout阶段", + "performDraw": "draw阶段", + "Choreographer": "vsync", + } + + context_parts = [] + for item in chain: + # chain item 格式: "slice_name [XX.XXms]" 或 "slice_name" + name = item.split(" [")[0] if " [" in item else item + + if name.startswith("SI$"): + # SI$ 标签:提取关键信息 + context_parts.append(_summarize_si_tag(name)) + else: + # 系统标签:只保留阶段标识 + for keyword, label in STAGE_KEYWORDS.items(): + if keyword in name: + context_parts.append(f"[{label}]") + break + + return context_parts + + +def _summarize_si_tag(tag: str) -> str: + """将 SI$ 标签转换为可读的上下文摘要。""" + body = tag[3:] if tag.startswith("SI$") else tag + + if body.startswith("RV#"): + # SI$RV#viewId#Adapter.method → "RV(viewId, Adapter.method)" + parts = body.split("#") + if len(parts) >= 3: + view_id = parts[1] + fqn_method = parts[2] + _, method = _split_fqn_method(fqn_method) + adapter = fqn_method.rsplit(".", 1)[0].rsplit(".", 1)[-1] + return f"RV#{view_id}#{adapter}.{method or '?'}" + return body + + if body.startswith("inflate#"): + parts = body[8:].split("#") + layout = parts[0] if parts else "?" + return f"inflate({layout})" + + if body.startswith("view#"): + fqn, method = _split_fqn_method(body[5:]) + cls = fqn.rsplit(".", 1)[-1] if fqn else "?" + return f"{cls}.{method or '?'}" + + if body.startswith("handler#"): + fqn_part = body[8:].split("#")[0] + fqn, method = _split_fqn_method(fqn_part) + cls = fqn.rsplit(".", 1)[-1] if fqn else fqn_part + return f"handler({cls}.{method or '?'})" + + if body.startswith("Activity.lifecycle"): + return "Activity生命周期" + + if body.startswith("Fragment.lifecycle"): + return "Fragment生命周期" + + # 默认 + fqn, method = _split_fqn_method(body) + cls = fqn.rsplit(".", 1)[-1] if fqn else body + return f"{cls}.{method or '?'}" + + +def _walk_parent_chain(slice_data: dict, slice_by_id: dict, max_depth: int = 5) -> list[str]: + """从 slice 数据沿 parent_id 向上回溯,构建调用链。 + + Returns: + 调用链 [root, ..., leaf],每项格式 "name [dur_ms]" + """ + chain = [] + visited = set() + current = slice_data + + for _ in range(max_depth): + sid = current.get("id") + if sid is None or sid in visited: + break + visited.add(sid) + + name = current.get("name", "") + dur_ms = current.get("dur_ms", 0) + chain.append(f"{name} [{dur_ms:.2f}ms]") + + parent_id = current.get("parent_id") + if not parent_id or parent_id not in slice_by_id: + break + current = slice_by_id[parent_id] + + chain.reverse() # root → leaf + return chain +``` + +#### 改动二:在 `extract_attributable_slices()` 中注入上下文 + +**文件**: `src/smartinspector/commands/attribution.py` + +在函数末尾(约第 707 行),去重之后、排序之前,注入 `call_context`: + +```python +def extract_attributable_slices(perf_summary_json: str, min_dur_ms: float = 1.0) -> list[dict]: + # ... 现有逻辑不变 ... + + # ── 新增:注入调用栈上下文 ── + parent_contexts = _build_parent_contexts(view_slices) + + for entry in seen.values(): + raw_name = entry.get("raw_name", "") + if raw_name in parent_contexts: + entry["call_context"] = parent_contexts[raw_name] + + # 对 RV 实例方法,补充 RV 上下文 + if entry.get("instance"): + # instance 格式: RV#viewId#AdapterName + entry["call_context"] = f"RV实例: {entry['instance']}" + + return sorted(seen.values(), key=lambda x: -x["dur_ms"]) +``` + +#### 改动三:`_build_group_prompt()` 传递上下文给 LLM + +**文件**: `src/smartinspector/agents/attributor.py` + +修改 `_build_group_prompt()` 函数(第 390-428 行),在 prompt 中加入调用栈上下文: + +```python +def _build_group_prompt(group: list[dict]) -> str: + """Build a search prompt for one group of issues.""" + from smartinspector.config import get_source_dir + + source_dir = get_source_dir() + + lines = [ + f"源码目录: {source_dir}\n", + ] + + for i, issue in enumerate(group, 1): + search_type = issue.get("search_type", "java") + cn = issue["class_name"] + line = f"{i}. {cn}.{issue['method_name']} ({issue['dur_ms']:.2f}ms, {search_type}" + if issue.get("count"): + line += f", count={issue['count']}" + + # ── 新增:调用栈上下文 ── + call_ctx = issue.get("call_context", "") + if call_ctx: + line += f", 调用链: {call_ctx}" + + # BlockMonitor 堆栈(保留原有逻辑) + if issue.get("stack_trace"): + line += f", 堆栈:{issue['stack_trace'][0]}" + + # 内部类提示(保留原有逻辑) + if "$" in cn: + outer = cn.split("$")[0] + line += f", 内部类:用Glob搜索外部类 {outer}" + line += f", RESULT行请用完整类名: {cn}.{issue['method_name']}" + + # XML 布局提示(保留原有逻辑) + if search_type == "xml": + line += f", xml布局:Glob **/{cn}.xml → Read完整文件, RESULT行请用: {cn}.{issue['method_name']}" + + line += ")" + lines.append(line) + + # ... 后续代码不变 ... +``` + +#### 改动四:更新 attributor prompt,增加调用链搜索指引 + +**文件**: `prompts/attributor.txt` + +在"核心原则"部分之后增加调用链上下文的使用指引: + +``` +## 调用链上下文(call_context) + +某些热点会附带调用链上下文信息,格式如: + `调用链: [帧渲染] → RV#recycler_orders#OrderAdapter.onCreateViewHolder` + +调用链的作用: +1. **缩小搜索范围**:如果调用链包含具体的 Activity/Fragment 名称, + 说明热点发生在该 UI 组件上下文中,优先搜索该组件相关的类 +2. **区分同名方法**:如果多个 Adapter 有同名方法(如 onBindViewHolder), + 调用链中的 RV 实例 ID 和 Adapter 名称可以精确定位到具体的 Adapter 类 +3. **理解性能阶段**:调用链中的 [measure阶段]/[layout阶段]/[draw阶段] + 标识说明热点发生在 UI 渲染的哪个子阶段 + +使用策略: +- 调用链是辅助信息,不需要额外搜索调用链中提到的类 +- 当 Glob 搜索到多个同名文件时,优先选择与调用链上下文匹配的文件 +- 当 Grep 搜索到方法在多个位置出现时,优先选择与调用链描述一致的调用点 +``` + +#### 改动五:`build_attribution_prompt()` 传递上下文给 explorer + +**文件**: `src/smartinspector/commands/attribution.py` + +在 `build_attribution_prompt()` 函数(第 744-792 行)中增加调用链上下文展示: + +```python +def build_attribution_prompt(attributable: list[dict]) -> str: + # ... 现有逻辑 ... + + for i, s in enumerate(attributable[:15], 1): + lines.append(f"### {i}. {s['class_name']}.{s['method_name']}") + lines.append(f" - 耗时: {s['dur_ms']:.2f}ms") + if s.get("instance"): + lines.append(f" - 实例: {s['instance']}") + if s.get("count"): + lines.append(f" - 调用次数: {s['count']}") + if s.get("total_ms"): + lines.append(f" - 总耗时: {s['total_ms']:.1f}ms") + + # ── 新增:调用链上下文 ── + if s.get("call_context"): + lines.append(f" - 调用链上下文: {s['call_context']}") + + if s.get("stack_trace"): + lines.append(f" - 堆栈采样 (BlockMonitor):") + for frame in s["stack_trace"][:12]: + lines.append(f" {frame}") + # ... 后续逻辑不变 ... +``` + +### 2.3 数据流设计 + +改进后的完整数据流: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Perfetto trace (.pb) │ +│ slice 表: id, name, ts, dur, depth, parent_id │ +└──────────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ collect_view_slices() │ +│ ① SQL 查询所有 SI$ slices + parent_id │ +│ ② 回溯缺失的 parent/ grandparent slices │ +│ ③ 构建 slice_by_id 映射 (id → slice dict) │ +│ ④ 构建 children_map (parent_id → children list) │ +│ ⑤ 构建 call_chains: top 10 slowest 的 parent chain │ +│ │ +│ 输出 view_slices JSON: │ +│ { │ +│ "slowest_slices": [...], ← 每项含 id + parent_id │ +│ "summary": [...], │ +│ "rv_instances": [...], │ +│ "call_chains": [...] ← 已构建的调用链 │ +│ } │ +└──────────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ extract_attributable_slices() [改动一 + 改动二] │ +│ ① 从 slowest_slices/summary/rv_instances 提取 class+method │ +│ ② 从 block_events 合并 stack_trace │ +│ ③ [新增] _build_parent_contexts(): │ +│ - 利用 call_chains 提取预构建的调用链 │ +│ - 对未覆盖的 slice,用 parent_id 回溯构建链 │ +│ - 过滤系统标签,提取 SI$ 上下文节点 │ +│ - 为 RV 实例方法附加 RV#viewId#Adapter 上下文 │ +│ ④ 将 call_context 注入每个 attributable entry │ +│ │ +│ 输出 attributable 列表: │ +│ [{class_name, method_name, dur_ms, call_context, ...}] │ +└──────────────────────────┬──────────────────────────────────────┘ + │ + ┌────┴────┐ + │ │ + ▼ ▼ +┌──────────────────┐ ┌──────────────────────────────────────────┐ +│ deterministic.py │ │ agents/attributor.py │ +│ compute_hints() │ │ _build_group_prompt() [改动三] │ +│ │ │ ① 在每个 issue 行末附加 call_context │ +│ 使用 call_chains │ │ ② LLM 看到调用链上下文 │ +│ 构建调用链分布 │ │ │ +│ (已有逻辑不变) │ │ prompts/attributor.txt [改动四] │ +│ │ │ ① 增加调用链上下文使用指引 │ +│ │ │ ② 增加同名方法区分策略 │ +└──────────────────┘ │ │ + │ 输出归因结果: │ + │ [{file_path, line_start, line_end, │ + │ source_snippet, call_context}] │ + └──────────────────────────────────────────┘ +``` + +### 2.4 调用链上下文的典型输出示例 + +**场景 A:RecyclerView onBindViewHolder 热点** + +``` +输入: SI$RV#recycler_list#com.example.app.OrderAdapter.onBindViewHolder +当前: class_name=OrderAdapter, method_name=onBindViewHolder + → Agent 只知道搜索 OrderAdapter.onBindViewHolder + +改进后: call_context = "RV#recycler_list#OrderAdapter.onBindViewHolder" + → Agent 知道是 recycler_list 这个 RV 实例的 OrderAdapter + → 如果项目中有多个 OrderAdapter(不同包名),可优先匹配 +``` + +**场景 B:View.measure 热点嵌套在 doFrame 中** + +``` +输入: SI$view#com.example.app.DetailView.measure +当前: class_name=DetailView, method_name=measure + → Agent 不知道发生在哪个渲染阶段 + +改进后: call_context = "[帧渲染] → [measure阶段] → DetailView.measure" + → Agent 知道是 measure 阶段的性能问题 + → 搜索时可以关联到 layout 布局分析 +``` + +**场景 C:Activity 生命周期中的耗时操作** + +``` +输入: SI$Activity.lifecycle +当前: class_name=Activity, method_name=lifecycle → system_class +改进后: call_context = "[帧渲染] → HomeActivity.onCreate" + → 可以精确定位到 HomeActivity 的 onCreate 方法 +``` + +**场景 D:Block 事件的堆栈补充** + +``` +输入: SI$block#com.example.app.Worker$1.run#250ms +当前: class_name=Worker, method_name=run, stack_trace=[at Worker$1.run(Worker.java:42)] +改进后: call_context = "handler(Worker.startWork)"(如果 parent 是 handler dispatch) + → Agent 知道是 startWork 方法中启动的匿名类 +``` + +## 3. 对现有代码的改动范围 + +| 文件 | 改动类型 | 改动内容 | 估计行数 | +|------|---------|---------|---------| +| `commands/attribution.py` | 新增函数 | `_build_parent_contexts()`, `_extract_context_from_chain()`, `_summarize_si_tag()`, `_walk_parent_chain()` | ~120 行 | +| `commands/attribution.py` | 修改函数 | `extract_attributable_slices()` 末尾注入 call_context | ~10 行 | +| `commands/attribution.py` | 修改函数 | `build_attribution_prompt()` 增加 call_context 展示 | ~3 行 | +| `agents/attributor.py` | 修改函数 | `_build_group_prompt()` 增加 call_context 传递 | ~4 行 | +| `prompts/attributor.txt` | 新增章节 | 调用链上下文使用指引 | ~25 行 | +| **总计** | | | **~160 行** | + +### 3.1 无需改动的部分 + +- `collector/perfetto.py`:**无需改动**。已完整采集 parent_id 和 call_chains +- `agents/deterministic.py`:**无需改动**。其 `_compute_call_chain_distribution()` 逻辑独立,不受影响 +- `graph/nodes/attributor.py`:**无需改动**。它是 orchestrator 层的胶水代码,数据透传 +- `tools/perfetto.py`:**无需改动** + +### 3.2 向后兼容性 + +所有改动都是**增量式**的: +- `call_context` 字段是可选的(`issue.get("call_context", "")`) +- 没有 `call_context` 时,行为与当前完全一致 +- 不影响 `deterministic.py`、`frame_analyzer.py` 等其他消费者 + +## 4. Token 节省预估 + +### 4.1 减少无效搜索 + +| 场景 | 当前 token 消耗 | 改进后 token 消耗 | 节省 | +|------|---------------|-----------------|------| +| 同名 Adapter 的 onBindViewHolder | Glob 返回 3 个文件,Read 3 次 | Glob 返回 3 个文件,利用上下文只 Read 1 次 | ~2000 tokens/次 | +| 同名方法多处调用 | Grep 返回 5+ 行,Read 5 次 | 利用上下文只 Read 1 次 | ~4000 tokens/次 | +| 系统类误搜索 | 搜索后才发现是系统类 | 调用链标识了阶段,跳过系统类搜索 | ~1500 tokens/次 | + +### 4.2 减少歧义消除的 LLM 推理 + +当前 LLM 在搜索到多个候选位置后,需要消耗 token 进行推理判断哪个是热点。改进后: +- 调用链上下文直接消除了歧义 +- 减少 LLM "猜测" 的 token 消耗 + +### 4.3 总体预估 + +假设一次典型归因有 5-8 个可归因 slice,其中 2-3 个存在调用歧义: +- **每次歧义消除节省**: ~1500-4000 tokens +- **单次归因节省**: ~3000-8000 tokens +- **占归因总 token 的比例**: 约 15%-30% + +增加的上下文信息 token 开销: +- 每个 slice 的 call_context: ~20-50 tokens +- 8 个 slice: ~160-400 tokens +- prompt 增加的指引: ~200 tokens(一次性) + +**净节省: ~2500-7000 tokens/次归因** + +## 5. 风险和边界情况 + +### 5.1 风险分析 + +#### 风险 1:call_chains 覆盖率不足 + +**现象**: `collect_view_slices()` 只为 top 10 slowest custom slices 构建 call_chains,但 `extract_attributable_slices()` 可能提取更多 slices。 + +**缓解**: 改动一中的 `_build_parent_contexts()` 设计了策略2(parent_id 回溯),即使 call_chains 未覆盖,也能从 slowest_slices 中已有的 parent_id 信息向上回溯。 + +**边界**: 如果 slice 的 parent_id 对应的 slice 不在 slowest_slices 中(因为 top 30 截断),回溯链会不完整。这是可接受的降级——此时 call_context 为空,退回到当前行为。 + +#### 风险 2:parent_id 在 JSON 传递中丢失 + +**现象**: `slowest_slices` 在构建时(`collect_view_slices()` 第 691-696 行)保留了 `id` 和 `parent_id`,但 `summary` 统计中没有这些字段。 + +**缓解**: 只对 `slowest_slices` 中的 slice 提取 parent_context。`summary` 和 `rv_instances` 中的 slice 不需要调用链(它们是聚合数据,不是具体的单次调用)。 + +#### 风险 3:调用链信息过长 + +**现象**: 如果调用链很长(>5层),传递给 LLM 的 context 字符串过长,反而浪费 token。 + +**缓解**: `_extract_context_from_chain()` 只保留 SI$ 自定义标签和阶段标识,过滤掉中间的系统标签。限制最大深度为 5 层。 + +#### 风险 4:LLM 误用调用链信息 + +**现象**: LLM 可能尝试搜索调用链中的其他类(如 parent chain 中的 Activity),导致额外的 Glob/Grep/Read 调用。 + +**缓解**: 在 `prompts/attributor.txt` 中明确说明"调用链是辅助信息,不需要额外搜索调用链中提到的类"。 + +### 5.2 边界情况 + +| 边界情况 | 处理方式 | +|---------|---------| +| slice 没有 parent_id | call_context 为空,退回当前行为 | +| parent_id 对应的 slice 不在结果集中 | 回溯链截断,只使用已知的链段 | +| call_chains 为空(trace 无 SI$ 数据) | `_build_parent_contexts()` 返回空 dict | +| atrace 截断了 SI$ tag 名称 | 调用链中可能出现不完整的 tag,_summarize_si_tag() 做了防御处理 | +| 多个 slowest_slice 有相同 name | context 以 name 为 key,后出现的会覆盖(可接受:相同 name 的调用链结构通常相似) | +| RV 实例方法的 instance 字段已包含上下文 | 优先使用 instance 作为上下文(见改动二中的优先级逻辑) | + +### 5.3 性能影响 + +- `_build_parent_contexts()` 的计算开销可忽略(O(n) 遍历 + O(n*log(n)) 回溯) +- 不增加任何 SQL 查询或文件 I/O +- 增加的 JSON 字段约 100-500 bytes,对 token 预算影响极小 + +## 6. 实施计划 + +### Phase 1: 基础设施(改动一 + 改动二) + +在 `commands/attribution.py` 中实现 `_build_parent_contexts()` 及其辅助函数,修改 `extract_attributable_slices()` 注入 call_context。 + +验证方式:构造包含 call_chains 的 view_slices JSON,确认 _build_parent_contexts() 输出正确。 + +### Phase 2: Agent 集成(改动三 + 改动四) + +修改 `_build_group_prompt()` 和 `prompts/attributor.txt`,让 LLM 利用调用链上下文。 + +验证方式:使用真实 trace 数据运行归因,检查 LLM 是否利用了调用链信息、是否减少了无效搜索。 + +### Phase 3: Explorer 集成(改动五) + +修改 `build_attribution_prompt()`,将上下文传递给 explorer agent。 + +验证方式:运行完整的 orchestrate 流程,检查最终报告是否包含更精确的归因信息。 + +### Phase 4: 效果评估 + +对比改进前后的: +1. 归因精确度(能否定位到具体调用点而非方法定义) +2. Token 消耗(归因阶段的 input/output token 总量) +3. 搜索效率(Glob/Grep/Read 调用次数) + +## 7. 后续优化方向 + +### 7.1 短期(本次改动基础上) + +- **调用链去重聚合**: 如果多个 slowest_slice 共享相同的 parent chain 前缀,在 prompt 中合并展示,避免重复 +- **上下文感知的 Grep 模式**: 当有 call_context 时,让 LLM 在 Grep 时使用更精确的 pattern(如类名+方法名+邻近上下文关键词) + +### 7.2 中期(需要额外数据采集) + +- **增强 collect_view_slices() 的 parent 回溯深度**: 当前只回溯到 grandparent(第 650-664 行),可考虑递归回溯到根节点(doFrame 级别) +- **利用 track_id 区分线程上下文**: 当前只关注 main thread 的 slices,可扩展到分析 worker thread 的调用上下文 + +### 7.3 长期(架构级改进) + +- **预计算调用链到源码行号的映射**: 在 deterministic.py 中预先匹配调用链中的 SI$ tag 到源码位置,减少 LLM 的搜索负担 +- **增量归因**: 对同一 RV 实例的多次调用,只搜索一次源码,后续复用结果 diff --git a/docs/hdc-command-line-research.md b/docs/hdc-command-line-research.md new file mode 100644 index 0000000..93b3de2 --- /dev/null +++ b/docs/hdc-command-line-research.md @@ -0,0 +1,963 @@ +# HarmonyOS NEXT hdc 命令行工具完整调研 + +> 调研日期: 2026-04-07 +> 来源: 华为官方文档 + 社区资料 + +--- + +## 目录 + +1. [hdc 核心命令对照表 (与 adb 对比)](#1-hdc-核心命令对照表) +2. [hitrace 完整用法](#2-hitrace-完整用法) +3. [hidumper 性能采集能力](#3-hidumper-性能采集能力) +4. [hilog 日志系统](#4-hilog-日志系统) +5. [SmartPerf / SP_daemon 命令行工具](#5-smartperf--sp_daemon) +6. [hiperf 性能剖析工具](#6-hiperf) +7. [aa / bm 等辅助工具](#7-辅助工具) + +--- + +## 1. hdc 核心命令对照表 + +### 1.1 hdc 架构概述 + +hdc (HarmonyOS Device Connector) 由三部分组成: +- **客户端 (client)**: 运行在电脑端,执行 hdc 命令时启动,命令结束后自动退出 +- **服务器 (server)**: 运行在电脑端的后台服务,管理客户端和设备端 daemon 之间的通信 +- **守护程序 (daemon)**: 运行在设备端,响应服务器请求 + +服务器默认监听电脑端 **8710 端口**,可通过环境变量 `OHOS_HDC_SERVER_PORT` 自定义 (1~65535)。 + +hdc 工具位于 `DevEco Studio/sdk/default/openharmony/toolchains` 路径下。 + +### 1.2 设备管理 + +| 功能 | hdc 命令 | adb 命令 | 说明 | +|------|---------|---------|------| +| 查看连接设备 | `hdc list targets` | `adb devices` | 列出所有已连接设备 | +| 指定设备执行 | `hdc -t ` | `adb -s ` | 多设备时指定目标 | +| 启动服务 | `hdc start` 或 `hdc kill -r` | `adb start-server` | 启动/重启 hdc 服务 | +| 停止服务 | `hdc kill` | `adb kill-server` | 终止 hdc 服务 | +| 查看版本 | `hdc -v` | `adb version` | 查看工具版本 | +| 查看帮助 | `hdc -h` | `adb help` | 帮助信息 | + +### 1.3 文件传输 + +| 功能 | hdc 命令 | adb 命令 | 说明 | +|------|---------|---------|------| +| 推送文件到设备 | `hdc file send ` | `adb push ` | 电脑 -> 设备 | +| 从设备拉取文件 | `hdc file recv ` | `adb pull ` | 设备 -> 电脑 | + +**注意**: hdc 使用 `file send/recv`,而非 adb 的 `push/pull`。 + +```bash +# 推送文件示例 +hdc file send ./test.hap /data/local/tmp/test.hap + +# 拉取文件示例 +hdc file recv /data/local/tmp/arkui.dump ./arkui.dump +``` + +### 1.4 端口转发 + +| 功能 | hdc 命令 | adb 命令 | 说明 | +|------|---------|---------|------| +| 正向端口转发 | `hdc fport tcp: tcp:` | `adb forward tcp: tcp:` | 主机端口 -> 设备端口 | +| 反向端口转发 | `hdc rport tcp: tcp:` | `adb reverse tcp: tcp:` | 设备端口 -> 主机端口 | +| 删除转发 | `hdc fport rm tcp: tcp:` | `adb forward --remove` | 删除指定端口转发 | + +```bash +# 正向转发:主机 8080 -> 设备 8080 +hdc fport tcp:8080 tcp:8080 + +# 反向转发:设备 8080 -> 主机 8080 +hdc rport tcp:8080 tcp:8080 + +# 删除转发 +hdc fport rm tcp:8080 tcp:8080 +``` + +### 1.5 无线连接 + +```bash +# 开启设备网络端口 (TCP 连接) +hdc tmode port 5555 + +# 通过网络连接设备 +hdc tconn :5555 + +# 关闭网络连接通道,恢复 USB +hdc tmode usb +``` + +### 1.6 Shell 执行 + +```bash +# 交互式 shell +hdc shell + +# 执行单条命令 +hdc shell + +# 执行带引号的复杂命令 +hdc shell "hidumper -s WindowManagerService -a '-a'" +``` + +### 1.7 应用安装/卸载 + +| 功能 | hdc 命令 | adb 命令 | 说明 | +|------|---------|---------|------| +| 安装应用 | `hdc install ` | `adb install ` | 安装 HAP 包 | +| 卸载应用 | `hdc uninstall ` | `adb uninstall ` | 卸载应用 | +| 指定设备安装 | `hdc -t install ` | `adb -s install ` | 多设备指定 | + +```bash +# 安装应用 +hdc install /path/to/app.hap + +# 卸载应用 (注意: 使用 bundleName 而非包路径) +hdc uninstall com.example.myapp + +# 拉起指定 UIAbility +hdc shell aa start -a -b + +# 强制停止应用 +hdc shell aa force-stop + +# 清除应用数据 +hdc shell bm clean -n -d +``` + +### 1.8 日志获取 + +| 功能 | hdc 命令 | adb 命令 | 说明 | +|------|---------|---------|------| +| 查看日志 | `hdc hilog` | `adb logcat` | 实时查看日志 | +| 清除日志 | `hdc shell hilog -r` | `adb logcat -c` | 清除日志缓冲区 | + +### 1.9 其他常用命令 + +```bash +# 获取设备 UDID +hdc shell bm get -u +hdc shell bm get --udid + +# 获取设备信息 +hdc shell param get const.product.model # 设备型号 +hdc shell param get const.product.devtype # 设备类型 +hdc shell param get const.ohos.apiversion # API 版本 + +# 设置系统参数 +hdc shell param set persist.ace.debug.enabled 1 # 开启 ArkUI debug +hdc shell param set persist.ace.testmode.enabled 1 # 开启 ArkUI test mode + +# 设备操作 +hdc shell reboot # 重启设备 +hdc shell reboot shutdown # 关机 +hdc shell power-shell wakeup # 唤醒设备 + +# 截屏 +hdc shell snapshot_display -f /data/local/tmp/snapshot.png +hdc file recv /data/local/tmp/snapshot.png ./ +``` + +--- + +## 2. hitrace 完整用法 + +hitrace 是 HarmonyOS 的系统级 trace 采集工具,类似 Android 的 atrace/systrace,但能力更强。 + +### 2.1 所有支持的 category (tag) 列表 + +执行 `hdc shell hitrace -l` 查看。完整列表如下: + +| Tag 名称 | 描述 | 性能分析相关性 | +|---------|------|-------------| +| **ace** | ACE development framework | **高** - ArkUI 框架核心 | +| **animation** | Animation | **高** - 动画性能 | +| **app** | APP Module | **高** - 应用层 | +| **ark** | ARK Module | **高** - ArkCompiler 运行时 | +| **graphic** | Graphic Module | **高** - 图形渲染 | +| **window** | Window Manager | **高** - 窗口管理 | +| **ability** | Ability Manager | 中 - 能力管理 | +| **ffrt** | ffrt tasks | **高** - 并发任务调度 | +| **sched** | CPU Scheduling | **高** - CPU 调度 | +| **freq** | CPU Frequency | **高** - CPU 频率 | +| **idle** | CPU Idle | 中 - CPU 空闲 | +| **load** | CPU Load | **高** - CPU 负载 | +| **binder** | Binder kernel Info | **高** - 进程间通信 | +| **zbinder** | HarmonyOS binder | **高** - 鸿蒙 binder | +| **disk** | Disk I/O | 中 - 磁盘 I/O | +| **memory** | Memory | 中 - 内存 | +| **memreclaim** | Kernel Memory Reclaim | 中 - 内存回收 | +| **membus** | Memory Bus Utilization | 中 - 内存总线 | +| **irq** | IRQ Events | 低 - 中断事件 | +| **irqoff** | IRQ-disabled code section | 低 | +| **preemptoff** | Preempt-disabled code section | 低 | +| **sync** | Synchronization | 低 - 同步 | +| **workq** | Kernel Workqueues | 低 - 内核工作队列 | +| **mmc** | eMMC commands | 低 | +| **ufs** | UFS commands | 低 | +| **pagecache** | Page cache | 低 | +| **regulators** | Voltage and Current Regulators | 低 | +| **ipa** | Thermal power allocator | 低 | +| accesscontrol | Access Control Module | 低 | +| accessibility | Accessibility Manager | 低 | +| account | Account Manager | 低 | +| bluetooth | Communication bluetooth | 低 | +| cloud | Cloud subsystem tag | 低 | +| commonlibrary | Commonlibrary subsystem | 低 | +| daudio | Distributed Audio | 低 | +| dcamera | Distributed Camera | 低 | +| deviceauth | Device Auth | 低 | +| devicemanager | Device Manager | 低 | +| dhfwk | Distributed Hardware FWK | 低 | +| dinput | Distributed Input | 低 | +| distributeddatamgr | Distributed Data Manager | 低 | +| dlpcre | Dlp Credential Service | 低 | +| drm | Digital Rights Management | 低 | +| dsched | Distributed Schedule | 低 | +| dscreen | Distributed Screen | 低 | +| dslm | Device security level | 低 | +| dsoftbus | Distributed Softbus | 低 | +| filemanagement | File management | 低 | +| gresource | Global Resource Manager | 低 | +| hdcd | hdcd | 低 | +| hdf | HDF subsystem | 低 | +| hmfs | HMFS commands | 低 | +| huks | Universal KeyStore | 低 | +| i2c | I2C Events | 低 | +| interconn | Interconnection subsystem | 低 | +| mdfs | Mobile Distributed File System | 低 | +| misc | Misc Module | 低 | +| msdp | Multimodal Sensor Data Platform | 低 | +| multimodalinput | Multimodal Input | 中 | +| musl | musl module | 低 | +| net | Net | 低 | +| notification | Notification Module | 低 | +| nweb | NWEB Module | 中 - WebView | +| ohos | HarmonyOS | 低 | +| power | Power Manager | 低 | +| push | Push subsystem | 低 | +| rpc | RPC and IPC | 低 | +| samgr | SAMGR | 低 | +| security | Security subsystem | 低 | +| sensors | Sensors Module | 低 | +| usb | USB subsystem | 低 | +| useriam | User IAM | 低 | +| virse | Virtualization Service | 低 | +| zaudio | HarmonyOS Audio Module | 低 | +| zcamera | HarmonyOS Camera Module | 低 | +| zimage | HarmonyOS Image Module | 低 | +| zmedia | HarmonyOS Media Module | 低 | + +**性能分析常用 tag 组合**: +```bash +# UI 渲染分析 (帧率/绘制) +hitrace -t 10 ace graphic window app + +# CPU 性能分析 +hitrace -t 10 sched freq load idle + +# 全面性能分析 +hitrace -t 10 ace graphic window app ark sched freq binder ffrt + +# 内存分析 +hitrace -t 10 memory memreclaim membus + +# I/O 分析 +hitrace -t 10 disk mmc ufs pagecache +``` + +### 2.2 采集命令格式 + +#### 基础采集 (指定时长 + 文本格式) + +```bash +# 格式 +hitrace -t <秒> -b <缓冲区KB> [tags...] [-o <输出文件>] + +# 采集 10 秒,缓冲区 200MB,tag 为 ace + graphic + app +hdc shell "hitrace -t 10 -b 204800 ace graphic app" + +# 保存到设备文件 +hdc shell "hitrace -t 10 -b 204800 ace graphic app -o /data/local/tmp/trace.ftrace" + +# 拉取到本地 +hdc file recv /data/local/tmp/trace.ftrace ./trace.ftrace +``` + +#### 二进制格式采集 (用 SmartPerf_Host 可视化) + +```bash +# --raw 参数采集二进制格式,固定保存到 /data/log/hitrace/ +hdc shell "hitrace -t 10 -b 204800 ace graphic app --raw" + +# 输出示例: +# /data/log/hitrace/record_trace_20250604102116@590322-695861087.sys +``` + +#### 快照模式 (手动控制起停) + +```bash +# 开始采集 +hdc shell "hitrace --trace_begin -b 204800 ace graphic app" + +# 导出当前数据 +hdc shell "hitrace --trace_dump -o /data/local/tmp/snapshot.ftrace" + +# 停止并导出 +hdc shell "hitrace --trace_finish -o /data/local/tmp/final.ftrace" + +# 停止不导出 +hdc shell "hitrace --trace_finish_nodump" +``` + +#### 录制模式 (长时间采集 + 自动落盘) + +```bash +# 开始录制模式 +hdc shell "hitrace --trace_begin --record -b 204800 --file_size 102400 ace graphic" + +# 停止录制 (自动输出文件列表) +hdc shell "hitrace --trace_finish --record" +``` + +#### 快照模式 (二进制 - bgsrv) + +```bash +# 开启 +hdc shell "hitrace --start_bgsrv" + +# 导出 +hdc shell "hitrace --dump_bgsrv" + +# 关闭 +hdc shell "hitrace --stop_bgsrv" +``` + +#### 压缩输出 + +```bash +hdc shell "hitrace -z -b 102400 -t 10 sched freq idle disk -o /data/local/tmp/test.ftrace" +``` + +### 2.3 命令参数汇总 + +| 参数 | 说明 | +|------|------| +| `-h` / `--help` | 查看帮助 | +| `-l` / `--list_categories` | 查看支持的 tag 列表 | +| `-t N` / `--time N` | 采集时长(秒),默认 5s | +| `-b N` / `--buffer_size N` | 缓冲区大小(KB),最小 512,默认 18432 | +| `-o file` / `--output file` | 输出文件路径(文本格式),建议 /data/local/tmp | +| `--raw` | 二进制格式输出,固定保存到 /data/log/hitrace/ | +| `--text` | 文本格式输出(默认) | +| `-z` | 压缩捕获的 trace | +| `--trace_begin` | 开始捕获 | +| `--trace_finish` | 停止捕获并输出 | +| `--trace_finish_nodump` | 停止捕获不输出 | +| `--trace_dump` | 导出当前缓冲区数据 | +| `--record` | 录制模式(长时间采集+落盘) | +| `--overwrite` | 缓冲区满时丢弃最新数据(默认丢弃最老) | +| `--file_size N` | 录制模式下单个文件大小(KB),默认 102400 | +| `--trace_clock ` | 时钟类型: boot(默认)/global/mono/uptime/perf | +| `--start_bgsrv` | 开启快照模式 | +| `--dump_bgsrv` | 导出快照数据 | +| `--stop_bgsrv` | 关闭快照模式 | +| `--trace_level ` | 设置级别阈值: D/I/C/M | +| `--get_level` | 查询级别阈值 | + +### 2.4 输出格式 (文本) + +``` +# tracer: nop +# _-----=> irqs-off +# / _----=> need-resched +# | / _---=> hardirq/softirq +# || / _--=> preempt-depth +# ||| / delay +# TASK-PID TGID CPU# |||| TIMESTAMP FUNCTION +# | | | | |||| | | +KstateRecvThrea-1132 ( 952) [003] .... 589942.951387: tracing_mark_write: B|952|H:CheckMsgFromNetlink|I62 +KstateRecvThrea-1132 ( 952) [003] .... 589942.951554: tracing_mark_write: E|952|I62 +``` + +- `B|pid|H:name|label` = Begin (开始打点) +- `E|pid|label` = End (结束打点) +- 时间戳为 boot time (从开机算起的秒数) + +### 2.5 hitrace 与 atrace 的差异 + +| 对比项 | hitrace (HarmonyOS) | atrace (Android) | +|--------|--------------------|--------------------| +| tag 命名 | ace, ark, graphic, window 等 | gfx, view, dalvik, hwui 等 | +| 输出格式 | 文本 (.ftrace) 和二进制 (.sys) | 文本 (.atrace) 和二进制 (.perfetto) | +| 长时间录制 | 支持 `--record` 模式 | 通过 perfetto 录制 | +| 快照模式 | `--start_bgsrv/dump_bgsrv/stop_bgsrv` | 无内置 | +| 可视化 | SmartPerf_Host | Perfetto UI / Systrace | +| 缓冲区控制 | `-b` 参数,最小 512KB | `-b` 参数 | +| 压缩 | `-z` 内置 | 需外部 gzip | + +--- + +## 3. hidumper 性能采集能力 + +HiDumper 是 HarmonyOS 统一系统信息导出的命令行工具,支持 CPU、内存、存储等资源分析。 + +### 3.1 命令行参数总览 + +| 选项 | 说明 | +|------|------| +| `-h` | 帮助 | +| `-lc` | 列出系统信息簇 | +| `-ls` | 列出正在运行的系统能力 (SA) | +| `-c` | 获取全量系统信息 (设备/内核/环境变量) | +| `-c [base\|system]` | 获取指定信息簇 | +| `-s` | 获取所有系统能力详细信息 | +| `-s [SA0 SA1]` | 获取指定 SA 的信息 | +| `-s [SA] -a ["option"]` | 执行 SA 的特定选项 | +| `-e` | 获取故障日志 (CppCrash/JSCrash/AppFreeze) | +| `--net [pid]` | 获取网络信息 | +| `--storage [pid]` | 获取存储信息 | +| `-p [pid]` | 获取进程信息 | +| `--cpuusage [pid]` | 获取 CPU 使用率 | +| `--cpufreq` | 获取 CPU 各核真实频率 (kHz) | +| `--mem` | 获取整机内存 | +| `--mem [pid]` | 获取进程内存 | +| `--mem --prune` | 获取精简整机内存 | +| `--mem [pid] --show-ashmem` | 显示 ashmem 详情 | +| `--mem [pid] --show-dmabuf` | 显示 DMA 内存详情 | +| `--mem-smaps [pid] [-v]` | 获取 smaps 内存统计 (仅 debug 应用) | +| `--mem-jsheap [pid] [--gc] [--leakobj] [--raw]` | 导出 JS 堆快照 | +| `--zip` | 输出压缩到 /data/log/hidumper/ | +| `--ipc [pid]` / `--start-stat` / `--stat` / `--stop-stat` | IPC 统计 | + +### 3.2 系统信息采集 + +```bash +# 列出所有系统信息簇 +hdc shell hidumper -lc + +# 列出所有系统能力 (SA) +hdc shell hidumper -ls + +# 获取全量系统信息 +hdc shell hidumper -c + +# 获取基础系统信息 +hdc shell hidumper -c base + +# 获取系统信息 +hdc shell hidumper -c system + +# 获取进程信息 +hdc shell hidumper -p + +# 获取网络信息 +hdc shell hidumper --net +hdc shell hidumper --net + +# 获取存储信息 +hdc shell hidumper --storage +hdc shell hidumper --storage + +# 获取 IPC 信息 +hdc shell "hidumper --ipc --start-stat" +hdc shell "hidumper --ipc --stat" +hdc shell "hidumper --ipc --stop-stat" + +# 压缩导出所有信息 +hdc shell "hidumper --zip --cpuusage --mem" +``` + +### 3.3 内存信息 + +```bash +# 整机内存 +hdc shell hidumper --mem + +# 整机内存 (精简) +hdc shell hidumper --mem --prune + +# 进程内存 +hdc shell hidumper --mem + +# 进程内存 + ashmem 详情 +hdc shell hidumper --mem --show-ashmem + +# 进程内存 + DMA 详情 +hdc shell hidumper --mem --show-dmabuf + +# 进程 smaps 详情 +hdc shell hidumper --mem-smaps +hdc shell hidumper --mem-smaps -v + +# JS 堆内存快照 +hdc shell hidumper --mem-jsheap + +# 仅触发 GC (不导出快照) +hdc shell hidumper --mem-jsheap --gc + +# 获取泄露对象列表 +hdc shell hidumper --mem-jsheap --leakobj + +# rawheap 格式导出 +hdc shell hidumper --mem-jsheap --raw +``` + +**内存输出关键字段解读**: + +| 字段 | 含义 | +|------|------| +| PSS Total | 实际使用物理内存 (Proportional Set Size) | +| VSS | 虚拟内存 (Virtual Set Size) | +| RSS | 驻留物理内存 (Resident Set Size) | +| USS | 独占物理内存 (Unique Set Size) | +| GL | GPU 内存 | +| Graph | 图形内存 (DMA) | +| ark ts heap | ArkUI 堆内存 | +| native heap | Native 堆内存 | +| AdjLabel | 内存回收优先级 [-1000, 1000] | + +### 3.4 CPU 信息 + +```bash +# 整机 CPU 使用率 +hdc shell hidumper --cpuusage + +# 指定进程 CPU 使用率 +hdc shell hidumper --cpuusage + +# CPU 各核频率 +hdc shell hidumper --cpufreq +``` + +**CPU 输出关键字段**: +- Total Usage: 总 CPU 使用率 +- User Space: 用户空间使用率 +- Kernel Space: 内核空间使用率 + +### 3.5 ArkUI 组件树 (重点) + +#### 步骤 1: 开启 debug 模式 + +```bash +hdc shell param set persist.ace.debug.enabled 1 +# 然后需要重启目标应用 +``` + +#### 步骤 2: 获取窗口列表和 WinId + +```bash +hdc shell hidumper -s WindowManagerService -a '-a' +``` + +输出示例: +``` +WindowName DisplayId Pid WinId Type Mode Flag ZOrd Orientation [ x y w h ] +ScreenLockWindow 0 1274 2 2110 1 0 4 0 [ 0 0 720 1280 ] +SystemUi_StatusBar 0 1274 4 2108 102 1 2 0 [ 0 0 720 72 ] +settings0 0 10733 11 1 1 1 1 0 [ 0 72 720 1136 ] +``` + +**常见 WindowName 映射**: + +| WindowName | 说明 | +|-----------|------| +| EntryView | 桌面 | +| RecentView | 最近任务 | +| SystemUi_NavigationBar | 三键导航 | +| SystemUi_StatusBar | 状态栏 | +| ScreenLockWindow | 锁屏 | + +#### 步骤 3: 获取组件树 + +```bash +# 获取指定窗口的组件树 +hdc shell "hidumper -s WindowManagerService -a '-w -element'" + +# 示例: WinId 为 11 +hdc shell "hidumper -s WindowManagerService -a '-w 11 -element'" +``` + +输出示例: +``` +|-> RootElement childSize:1 + | ID: 0 + | elmtId: -1 + | Active: Y + |-> StackElement childSize:2 + |-> StageElement childSize:1 + |-> PageElement childSize:1 + |-> Column childSize:3 + |-> Text childSize:0 + ID: 5 + FrameRect: RectT (0.00, 0.00) - [720.00 x 50.00] + BackgroundColor: #FF0000FF +``` + +#### 步骤 4: 获取指定 Node 的组件信息 + +```bash +hdc shell "hidumper -s WindowManagerService -a '-w -element -lastpage '" +``` + +#### 步骤 5: 获取 Inspector 树 (与 DevEco Studio ArkUI Inspector 匹配) + +```bash +# 先开启 testmode +hdc shell param set persist.ace.testmode.enabled 1 + +# 获取 Inspector 树 +hdc shell "hidumper -s WindowManagerService -a '-w -inspector'" +``` + +输出示例: +``` +|-> Column childSize:1 +| ID: 128 +| compid: +| text: +| top: 72.000000 +| left: 0.000000 +| width: 720.000000 +| height: 1136.000000 +| visible: 1 +| clickable: 0 +| checkable: 0 +``` + +#### 步骤 6: 获取应用路由栈信息 + +```bash +hdc shell "hidumper -s WindowManagerService -a '-w -router'" +``` + +#### 步骤 7: 获取完整组件树 dump 文件 + +```bash +# 生成 dump 文件 +hdc shell "hidumper -s WindowManagerService -a '-w -element -c'" + +# 查找文件路径 +hdc shell find /data/ -name arkui.dump + +# 拉取到本地 +hdc file recv /data/app/el2/100/base//haps/entry/files/arkui.dump ./ +``` + +### 3.6 帧率信息 + +HiDumper 本身不直接提供帧率数据。帧率采集需要通过以下方式: + +1. **hitrace**: 采集 `ace` + `graphic` tag 分析帧时间 +2. **SP_daemon**: 使用 `-f` 参数采集 FPS +3. **Graphics Profiler**: DevEco Studio 内置工具 + +--- + +## 4. hilog 日志系统 + +### 4.1 日志格式 + +hilog 日志格式: +``` +<时间> <级别>: <消息> +``` + +示例: +``` +01-01 12:00:00.000 I A03200[1234/5678]: This is an info log +``` + +**格式详解**: +- `I` = Info 级别 +- `A03200` = A 表示应用日志, 3200 是 domainId (十六进制) +- `[1234/5678]` = 进程号/线程号 + +### 4.2 日志级别 + +| 级别 | 字母 | 说明 | +|------|------|------| +| Debug | D | 调试信息 | +| Info | I | 一般信息 | +| Warn | W | 警告 | +| Error | E | 错误 | +| Fatal | F | 致命错误 | + +### 4.3 命令行用法 + +```bash +# 实时查看日志 +hdc shell hilog + +# 清除日志缓冲区 +hdc shell hilog -r + +# 按级别过滤 (D=Debug, I=Info, W=Warn, E=Error, F=Fatal) +hdc shell hilog -l D # Debug 及以上 +hdc shell hilog -l E # Error 及以上 + +# 按进程 PID 过滤 +hdc shell hilog -p + +# 组合过滤 +hdc shell "hilog -p -l D" + +# 按标签过滤 +hdc shell "hilog | grep " + +# 设置日志级别 (只输出指定级别及以上) +hdc shell hilog -b D # Debug 及以上 +hdc shell hilog -b I # Info 及以上 +hdc shell hilog -b W # Warn 及以上 +hdc shell hilog -b E # Error 及以上 +hdc shell hilog -b F # Fatal +``` + +### 4.4 hilog 与 logcat 的差异 + +| 对比项 | hilog (HarmonyOS) | logcat (Android) | +|--------|-------------------|------------------| +| 日志域 | domainId (十六进制) | tag (字符串) | +| 级别格式 | 单字母在行首 `I A03200` | 级别字符后跟 tag `I/tag:` | +| 进程标识 | `[pid/tid]` | `pid tid` | +| 单条最大长度 | 4096 字节 | 4068 字节 | +| 隐私标识 | 支持隐私参数格式化 `{private}` | 无内置 | +| 过滤方式 | `-l` 级别, `-p` PID | `-s` tag, `*:level` | +| 缓冲区 | ring buffer | log buffer (main/system/radio/events) | +| 环境变量 | `HDC_SERVER_PORT` | `ANDROID_LOG_TAGS` | + +--- + +## 5. SmartPerf / SP_daemon + +SmartPerf Device 是 OpenHarmony 预置的性能功耗测试工具 (bin 名称: SP_daemon),从 3.2.5.1 版本开始预制。 + +### 5.1 支持采集的指标 + +| 参数 | 说明 | 输出字段 | +|------|------|---------| +| `-c` | CPU 频率和负载 | cpuNfreq, cpuNload | +| `-g` | GPU 频率和负载 | gpufreq, gpuload | +| `-f` | FPS 和帧抖动 | fps, fps_jitters | +| `-t` | 温度 | soc-thermal, gpu-thermal | +| `-p` | 电流和电压 | current_now, voltage_now | +| `-r` | 内存 (需 -PID) | ram(pss) | +| `-snapshot` | 截图 | - | + +### 5.2 命令格式 + +```bash +# 基本格式 +hdc shell "SP_daemon -N <次数> -PKG <包名> [选项]" + +# 必选参数 +# -N: 采集次数 (必选) +# 可选参数: +# -PKG: 包名 +# -PID: 进程 PID (对 RAM 采集适用) +# -OUT: CSV 输出路径 +``` + +### 5.3 使用示例 + +```bash +# 采集 100 次全部性能指标 +hdc shell "SP_daemon -N 100 -PKG com.example.app -c -g -t -p -f" + +# 仅采集 CPU 和 FPS +hdc shell "SP_daemon -N 50 -c -f" + +# 采集内存 (需指定 PID) +hdc shell "SP_daemon -N 20 -PID -r" + +# 指定 CSV 输出路径 +hdc shell "SP_daemon -N 50 -PKG com.example.app -c -f -OUT /data/local/tmp/perf.csv" + +# 查看帮助 +hdc shell "SP_daemon --help" +``` + +### 5.4 输出格式 + +实时打印示例: +``` +----------------------------------Print START------------------------------------ +order:0 cpu0freq=1992000 +order:1 cpu0load=23.469387 +order:2 cpu1freq=1992000 +order:3 cpu1load=26.262627 +order:8 current_now=-1000.000000 +order:9 gpu-thermal=48333.000000 +order:10 gpufreq=200000000 +order:11 gpuload=0.000000 +order:12 soc-thermal=48888.000000 +order:13 timestamp=1501925596847 +order:14 voltage_now=4123456.000000 +----------------------------------Print END-------------------------------------- +``` + +CSV 输出 (默认保存到 `/data/local/tmp/data.csv`): +``` +cpu0freq,cpu0load,...,gpuload,soc-thermal,timestamp,voltage_now +1992000,23.469387,...,0.000000,48888.000000,1501925596847,4123456.000000 +``` + +--- + +## 6. hiperf + +hiperf 是 HarmonyOS 的性能剖析工具 (类似 Android 的 simpleperf)。 + +```bash +# 采集指定进程的 CPU 剖析 +hdc shell "hiperf record -p " + +# 采集系统级剖析 +hdc shell "hiperf record -a" + +# 采集指定时长 +hdc shell "hiperf record -p -d 10" + +# 查看统计 +hdc shell "hiperf stat -p " + +# 查看帮助 +hdc shell "hiperf --help" +``` + +--- + +## 7. 辅助工具 + +### 7.1 aa 工具 (Ability Assistant) + +```bash +# 启动 Ability +hdc shell aa start -a -b + +# 强制停止应用 +hdc shell aa force-stop + +# dump Ability 信息 +hdc shell aa dump -a # 全部 +hdc shell aa dump -l # 列表 +``` + +### 7.2 bm 工具 (Bundle Manager) + +```bash +# 查看已安装应用列表 +hdc shell bm dump -a + +# 查看指定应用信息 +hdc shell bm dump -n + +# 获取设备 UDID +hdc shell bm get -u +hdc shell bm get --udid + +# 清除应用数据 +hdc shell bm clean -n -d + +# 卸载应用 +hdc shell bm uninstall -n +``` + +### 7.3 param 工具 (系统参数) + +```bash +# 获取参数 +hdc shell param get <参数名> + +# 设置参数 +hdc shell param set <参数名> <值> + +# 常用参数 +hdc shell param get const.product.model # 设备型号 +hdc shell param get const.product.devtype # 设备类型 +hdc shell param get const.ohos.apiversion # API 版本 +hdc shell param get persist.ace.debug.enabled # ArkUI debug 开关 +hdc shell param set persist.ace.debug.enabled 1 # 开启 ArkUI debug +hdc shell param set persist.ace.testmode.enabled 1 # 开启 ArkUI test mode +``` + +### 7.4 power-shell 工具 + +```bash +# 唤醒设备 +hdc shell power-shell wakeup + +# 休眠设备 +hdc shell power-shell suspend + +# 查看屏幕状态 +hdc shell hidumper -s 3301 -a "查询手机屏幕状态" +``` + +--- + +## 附录: 性能采集命令速查表 + +### 场景: 应用卡顿/掉帧分析 + +```bash +# 1. 采集 UI 渲染 trace (10秒) +hdc shell "hitrace -t 10 -b 204800 ace graphic window app -o /data/local/tmp/trace.ftrace" +hdc file recv /data/local/tmp/trace.ftrace ./ + +# 2. 采集 FPS +hdc shell "SP_daemon -N 100 -PKG <包名> -f -c" + +# 3. 获取组件树 +hdc shell "hidumper -s WindowManagerService -a '-a'" # 获取 WinId +hdc shell "hidumper -s WindowManagerService -a '-w -element -c'" # 获取组件树 +``` + +### 场景: 内存泄漏分析 + +```bash +# 1. 获取进程内存 +hdc shell hidumper --mem + +# 2. 获取 JS 堆快照 +hdc shell hidumper --mem-jsheap + +# 3. 获取泄露对象 +hdc shell hidumper --mem-jsheap --leakobj + +# 4. 获取内存详细映射 +hdc shell hidumper --mem-smaps -v +``` + +### 场景: CPU 热点分析 + +```bash +# 1. CPU 使用率 +hdc shell hidumper --cpuusage + +# 2. CPU 频率 +hdc shell hidumper --cpufreq + +# 3. CPU trace +hdc shell "hitrace -t 10 sched freq load" + +# 4. hiperf 剖析 +hdc shell "hiperf record -p -d 10" +``` + +### 场景: 完整性能数据采集 + +```bash +# 一键采集 SP_daemon 全量数据 +hdc shell "SP_daemon -N 200 -PKG <包名> -c -g -t -p -f -r -PID " + +# 一键采集 hitrace +hdc shell "hitrace -t 10 -b 204800 ace graphic window app ark sched freq binder ffrt --raw" + +# 一键导出 hidumper +hdc shell "hidumper --zip --cpuusage --mem" +hdc file recv /data/log/hidumper/ ./ +``` diff --git a/docs/perfetto-comparison-analysis.md b/docs/perfetto-comparison-analysis.md new file mode 100644 index 0000000..576ae16 --- /dev/null +++ b/docs/perfetto-comparison-analysis.md @@ -0,0 +1,381 @@ +# SI改造方案 — 基于Perfetto+AI方案的对比分析 + +> 生成日期:2026-04-22 +> 参考方案:基于Perfetto与AI的Android性能自动化诊断方案(Shell脚本方案) +> 对照项目:AppSmartInspector(SI,LangGraph Agent方案) + +--- + +## 一、两方案全景对比 + +| 维度 | Shell脚本方案(参考文章) | SI Agent方案(当前项目) | +|------|--------------------------|------------------------| +| **架构** | 线性Pipeline:Shell入口 → Python SQL → Shell源码注入 → 远端LLM | LangGraph StateGraph:Orchestrator → Collector → Analyzer → Attributor → Reporter | +| **采集** | `1_ai_sampler.sh` 手动构造perfetto textproto config | `PerfettoCollector.pull_trace_from_device()` Python构造config,支持ADB直连 | +| **特征提取** | `2_trace_filter.py` 纯SQL查询 | `PerfettoCollector` 11个collect方法 + `deterministic.py` 预计算层 | +| **源码关联** | `3_ai_reporter.sh` Shell脚本grep/find定位源码后直接拼入Prompt | Attributor Agent(LangChain agent + manual tool-call loop)动态 Glob→Grep→Read | +| **AI诊断** | 单次远端LLM调用,全量源码注入Prompt | 多Agent协作:PerfAnalyzer(预计算+LLM)→ Attributor(LLM+工具)→ Reporter(LLM) | +| **用户交互** | `run_profiler.sh` 入口,无交互 | REPL交互式CLI + Perfetto UI插件 + WebSocket实时通信 | +| **可扩展性** | 添加新SQL查询需改脚本 | 添加新collect方法 + 新Agent即可,模块化 | +| **依赖** | Shell + Python + Perfetto CLI | Python + LangGraph + LangChain + Perfetto SDK + WebSocket | + +--- + +## 二、逐维度深度对比 + +### 2.1 Trace采集 + +**Shell方案:** +- 通过Shell脚本 `1_ai_sampler.sh` 构造Perfetto textproto配置 +- 使用 `perfetto -c - --txt` 命令行模式,通过cat管道绕过SELinux限制 +- 采集类别:sched freq idle am wm gfx view binder_driver hal dalvik memory(11个atrace类别) +- 增加了 `linux.perf`(CPU函数采样)+ Frame Timeline + `linux.process_stats` +- `target_cmdline` 限定只对目标应用展开调用栈解析 +- 自动降级:config模式失败时降级到命令行模式 + +**SI方案:** +- `PerfettoCollector.pull_trace_from_device()` 在Python中构造完整textproto配置 +- 同样包含atrace类别 + Frame Timeline + `linux.perf` + `linux.process_stats` +- 额外支持 `android.java_hprof`(Java堆内存分析)和 `android.log`(logcat采集) +- 通过ADB直接执行perfetto命令 +- 自动检测前台应用包名(`adb shell dumpsys`) + +**对比评估:** + +| 采集能力 | Shell方案 | SI方案 | 优势方 | +|---------|-----------|--------|--------| +| 基础atrace | 11类别 | 11类别 | 持平 | +| CPU函数采样 | linux.perf | linux.perf | 持平 | +| Frame Timeline | 支持 | 支持 | 持平 | +| Java堆分析 | 未提及 | java_hprof | **SI** | +| Logcat采集 | 未提及 | android.log | **SI** | +| SELinux绕过 | cat管道方案 | 未专门处理 | **Shell** | +| 自动降级 | 有 | 无 | **Shell** | +| 包名检测 | 手动传入 | 自动检测 | **SI** | +| 进程过滤 | target_cmdline | target_cmdline | 持平 | + +**SI可借鉴:** +1. **SELinux绕过策略**:参考Shell方案的cat管道方式(`cat config.pb | perfetto -c -`),增加在受限设备上的兼容性 +2. **自动降级机制**:当config模式启动perfetto失败时,自动降级到命令行模式继续采集 +3. **UID获取策略**:Shell方案通过 `linux.process_stats` 扫描 `/proc` 获取UID,解决冷启动阶段 `process` 表无数据时的包名匹配问题。SI目前依赖 `process` 表,可能在冷启动场景遗漏 + +--- + +### 2.2 特征提取(SQL查询) + +**Shell方案:** +- `2_trace_filter.py` 核心是直接的SQL查询 +- 查询维度覆盖:慢切片、线程状态、帧时间线、CPU采样、调用栈 +- 关键特性: + - 使用 `thread_state` 表区分"代码慢"还是"线程被挂起"(Running vs R/S/D) + - `actual_frame_timeline_slice` 帧级归因,区分App/SF问题 + - `perf_sample` + `stack_profile_callsite` CPU调用栈链表(Android 15+) + - `args` 表读取Slice附加参数(view_type, adapter_position) + - `package_list` 冷启动场景的反查表 + +**SI方案:** +- `PerfettoCollector` 的11个collect方法 +- `deterministic.py` 预计算层(纯Python,无LLM): + - 空场景检测(FPS=0, 无帧, CPU低) + - 严重度分类(P0/P1/P2,按设备帧预算动态阈值) + - 调用链时间分布(百分比 + 树形缩进) + - RV热点排名(max_ms, avg_ms) + - 卡顿帧关联(帧-Slice-输入事件三方关联) + - CPU热点识别 +- SI$自定义Tag系统:block/RV/inflate/view/handler/db/net/img/touch +- Block事件与Perfetto Slice通过timestamp bisect关联 + +**对比评估:** + +| 特征提取能力 | Shell方案 | SI方案 | 优势方 | +|-------------|-----------|--------|--------| +| 慢切片SQL | 直接查询 | collect_view_slices | 持平 | +| 线程状态分析 | thread_state表区分R/S/D | collect_sched (end_state) | **Shell** | +| 帧时间线 | actual_frame_timeline_slice | collect_frame_timeline + expected | **SI** | +| CPU采样调用栈 | stack_profile_callsite链表 | collect_cpu_hotspots + 调用链重建 | 持平 | +| Slice附加参数 | args表 (view_type, adapter_position) | collect_view_slices (已解析) | 持平 | +| 冷启动场景 | package_list反查 | 未专门处理 | **Shell** | +| 确定性预计算 | 无 | deterministic.py 六大模块 | **SI** | +| 动态阈值 | 固定阈值 | 按设备帧预算自动调整 | **SI** | +| 帧级归因 | jank_type区分App/SF | USER_JANK_TYPES分类 | 持平 | +| 自定义Tag | 依赖atrace原生tag | SI$自定义Tag体系(覆盖8类场景) | **SI** | + +**SI可借鉴:** +1. **thread_state深度分析**:Shell方案利用 `thread_state` 表区分线程在Running(代码慢)和 S/D(被挂起/IO等待)状态的时间分布。SI的 `collect_sched` 只有end_state,缺少per-slice级别的线程状态分析。建议增加 `thread_state` 查询,将每个慢Slice关联其执行期间的线程状态分布,帮助区分"代码需要优化"和"线程被系统挂起"两种不同根因 +2. **package_list冷启动支持**:在冷启动场景中 `process` 表可能为空,Shell方案用 `package_list` 反查。SI应在 `pull_trace_from_device` 的config中确保 `linux.process_stats` 开启,并在包名匹配失败时 fallback 到 `package_list` 查询 +3. **CPU采样调用栈深度重建**:Shell方案通过 `stack_profile_callsite.parent_id` 递归重建完整调用链。SI的 `collect_cpu_hotspots` 已实现类似逻辑(callsite_map + 递归回溯),但可增加对 `stack_profile_mapping` 的查询,关联so/apk的映射信息,使native方法调用栈更可读 + +--- + +### 2.3 源码关联 + +**Shell方案:** +- `3_ai_reporter.sh` 纯Shell脚本实现 +- 流程:从trace_filter输出中提取类名 → 用find/grep在源码中定位文件 → 用sed/awk提取方法体 → 直接拼入AI Prompt +- 源码搜索范围:当前class文件 + import依赖 + 关联XML布局 +- 特点: + - **静态绑定**:在Prompt构造阶段一次性完成所有源码搜索 + - **确定性**:不依赖LLM做搜索决策,纯Shell脚本逻辑 + - **广度**:搜索依赖引用和关联XML布局,提供更完整的上下文 + +**SI方案:** +- `Attributor Agent` — LangChain agent with manual tool-call loop +- 流程:提取SI$切片 → 分类(java/xml/system)→ 分组 → 对每组执行 Glob→Grep→Read 三步搜索 +- 特点: + - **动态搜索**:LLM决定搜索策略,可以处理模糊/不规则的类名 + - **工具调用循环**:最多8轮迭代,LLM自主决定何时搜索完成 + - **Call Stack上下文**:利用parent chain和BlockMonitor堆栈辅助定位 + - **内部类处理**:匿名内部类($1/$2) → 搜索外部类 → 堆栈提取真实方法名 + - **LRU缓存**:避免重复读取文件 + - **Early termination**:连续3次搜索失败则终止 + - **结构化输出**:尝试with_structured_output,失败时fallback到RESULT行解析 + +**对比评估:** + +| 源码关联能力 | Shell方案 | SI方案 | 优势方 | +|-------------|-----------|--------|--------| +| 搜索策略 | 静态find/grep/sed | 动态LLM Glob→Grep→Read | **SI** | +| 搜索准确性 | 依赖类名匹配规则 | LLM理解模糊/不规则名称 | **SI** | +| 搜索范围 | 当前类+import依赖+XML | 当前类+内部类+堆栈提示 | **Shell**(依赖链更广) | +| 搜索效率 | 一次性Shell脚本,快速 | 多轮LLM调用,较慢 | **Shell** | +| 匿名内部类 | 未专门处理 | 完整处理(外部类+堆栈方法名) | **SI** | +| 错误容忍 | Shell脚本出错即中断 | Early termination + 缓存 + 重试 | **SI** | +| Token消耗 | 无(不使用LLM搜索) | 高(每轮LLM调用消耗token) | **Shell** | +| 上下文完整性 | 包含import依赖和关联XML | 仅搜索目标类和方法体 | **Shell** | +| BlockMonitor堆栈 | 未提及 | 堆栈关联+bisect时间匹配 | **SI** | +| 调用链上下文 | 无 | parent chain回溯+上下文摘要 | **SI** | + +**SI可借鉴:** +1. **依赖引用搜索**:当前SI只搜索目标类本身的方法体。Shell方案会额外搜索该类的import依赖和关联XML布局,为AI诊断提供更完整的上下文。建议在Attributor Agent中增加一步"关联搜索":Read目标文件后,提取import列表中的项目内类和关联的XML布局ID,一并读取 +2. **搜索效率优化**:Shell方案一次性完成所有源码搜索,而SI需要多轮LLM调用。对于类名和包名完整的简单场景,可以引入"快速路径":跳过LLM搜索决策,直接用确定性代码执行Glob→Grep→Read。只在模糊/匿名内部类场景才走LLM搜索路径 +3. **静态源码绑定模式**:提供一种"预处理模式"选项,在调用LLM之前先用确定性代码完成源码搜索,将结果直接注入Prompt,类似Shell方案的做法。好处是减少token消耗和LLM调用轮次 + +--- + +### 2.4 AI诊断 + +**Shell方案:** +- 单次LLM调用 +- 将SQL查询结果+源码片段一起注入Prompt +- LLM直接输出Markdown诊断报告 +- 优点:简单直接,一次调用完成 +- 缺点:LLM需要同时处理数据分析和报告生成两个任务,可能导致质量下降 + +**SI方案:** +- 三阶段Agent协作: + 1. **PerfAnalyzer**:接收deterministic预计算结论 + 原始数据 → 组织性能分析报告 + 2. **Attributor**:根据SI$切片搜索源码 → 返回归因结果(文件路径+方法体+行号) + 3. **Reporter**:综合性能分析 + 源码归因 + 待归因热点 → 生成最终报告 +- **混合架构**:确定性计算(deterministic.py)+ LLM语言生成 +- **Token控制**:消息窗口裁剪、LRU文件缓存、结构化输出探测、Early termination +- **多Provider支持**:结构化输出失败时自动fallback到文本解析 + +**对比评估:** + +| AI诊断能力 | Shell方案 | SI方案 | 优势方 | +|-----------|-----------|--------|--------| +| LLM调用次数 | 1次 | 3-5次(per_analyzer + attributor迭代 + reporter) | **Shell**(简单) | +| 分析精度 | 依赖LLM做算术 | 确定性预计算+LLM语言组织 | **SI** | +| 源码上下文 | 预注入完整源码 | 动态搜索+精准方法体 | **SI**(灵活) | +| 报告质量 | 单次输出 | 分层生成+格式化 | **SI** | +| Token消耗 | 低 | 高(多轮对话) | **Shell** | +| 错误恢复 | 无 | 重试+fallback+graceful degrade | **SI** | +| 可扩展性 | 硬编码Prompt | 模块化Agent+独立Prompt文件 | **SI** | +| 结果一致性 | 完全依赖LLM | 确定性部分保证一致 | **SI** | + +**SI可借鉴:** +1. **轻量模式**:参考Shell方案的单次调用模式,为SI增加一个"快速诊断"模式。跳过Attributor阶段,直接将perf_summary中的类名/方法名/耗时信息交给Reporter LLM,由LLM基于经验推测优化建议。适用场景:不需要精确源码定位的快速初筛 +2. **Token消耗优化**:Shell方案的Prompt是静态构造的,token消耗可控。SI的Attributor Agent每轮迭代都会增加消息历史。当前的消息窗口裁剪(keep last 12 messages)和max 8 iterations是好的防护措施,但可进一步优化:对于确定性高的场景(FQN完整、方法名明确),直接执行工具调用不经过LLM决策 + +--- + +### 2.5 系统架构 + +**Shell方案:** +``` +run_profiler.sh + ├── 1_ai_sampler.sh # Trace采集 + ├── 2_trace_filter.py # SQL特征提取 + └── 3_ai_reporter.sh # 源码注入 + AI诊断 +``` +- 线性Pipeline,阶段间通过文件传递数据 +- 每个阶段独立可执行 +- 依赖少:只需Shell + Python + curl/LLM API + +**SI方案:** +``` +LangGraph StateGraph + orchestrator → collector → analyzer → attributor → reporter + android_expert perf_analyzer + explorer +``` +- 状态机架构,通过AgentState传递数据 +- 条件路由:根据用户意图分发到不同处理路径 +- REPL交互式CLI + Perfetto UI WebSocket集成 +- 依赖多:LangGraph + LangChain + WebSocket + Perfetto SDK + +**对比评估:** + +| 架构特性 | Shell方案 | SI方案 | 优势方 | +|---------|-----------|--------|--------| +| 复杂度 | 低(3个脚本) | 高(多Agent系统) | **Shell**(简单) | +| 可维护性 | Shell脚本维护困难 | Python模块化,可维护 | **SI** | +| 可扩展性 | 修改脚本,耦合高 | 添加Agent/Node即可 | **SI** | +| 部署难度 | 低(仅需Shell环境) | 高(Python环境+依赖) | **Shell** | +| 交互性 | 无交互 | REPL + Perfetto UI | **SI** | +| 状态管理 | 文件传递 | AgentState TypedDict | **SI** | +| 错误处理 | 脚本失败即中断 | node_error_handler + 全局try/except | **SI** | +| 并发安全 | 无 | thread-safe LLM singleton + Lock | **SI** | +| 实时反馈 | 无 | WebSocket进度推送 | **SI** | + +--- + +### 2.6 用户体验 + +**Shell方案:** +- 命令行执行 `run_profiler.sh <包名>` +- 无交互,全自动化 +- 输出:Markdown诊断报告文件 +- 适用场景:CI/CD集成、批量测试 + +**SI方案:** +- 交互式REPL(`smartinspector`命令) +- 21个Slash命令(/full、/trace、/frame、/open等) +- Perfetto UI集成:浏览器中选择时间范围 → Agent实时分析 +- WebSocket实时进度推送 +- 多种交互模式:自然语言、命令、UI选择 +- 适用场景:开发阶段调试、性能问题深入分析 + +**对比评估:** + +| 用户体验 | Shell方案 | SI方案 | 优势方 | +|---------|-----------|--------|--------| +| 上手难度 | 低(一条命令) | 中(需学习命令和交互) | **Shell** | +| 灵活性 | 低(固定Pipeline) | 高(多种入口和交互方式) | **SI** | +| CI/CD集成 | 天然适合 | 需封装 | **Shell** | +| 深度分析 | 浅(一次性报告) | 深(交互式追问、时间范围选择) | **SI** | +| 可视化 | 无 | Perfetto UI集成 | **SI** | +| 实时反馈 | 无 | 进度条 + 工具调用展示 | **SI** | + +**SI可借鉴:** +1. **Headless/CI模式**:参考Shell方案的"一条命令全流程"设计,为SI增加非交互模式。例如 `smartinspector --headless --package com.example.app --duration 10` 直接完成采集→分析→报告,适合CI/CD集成 +2. **输出标准化**:Shell方案的输出是一个完整的Markdown文件,可直接作为测试报告。SI的报告已很完善(含header tables + 问题列表),可增加机器可读的JSON输出选项,方便CI系统解析 + +--- + +## 三、SI可借鉴的改进点汇总 + +按优先级排序(P0=高优先级,P1=中优先级,P2=低优先级): + +### P0:核心能力提升 + +#### 1. thread_state深度分析 — 区分"代码慢"vs"被挂起" +- **现状**:SI的 `collect_sched` 只查询调度统计和blocked_reason,没有per-slice级别的线程状态分析 +- **改进**:新增 `collect_thread_state` 方法,查询 `thread_state` 表,对每个SI$慢Slice关联其执行期间的状态分布(Running/S/D) +- **价值**:帮助开发者区分"代码需要优化"和"线程被IO/锁阻塞"两种根本不同的根因 +- **参考SQL**: + ```sql + SELECT ts, dur, state + FROM thread_state + WHERE utid = (SELECT utid FROM thread WHERE name = 'main') + AND ts >= {slice_ts} AND ts + dur <= {slice_ts} + {slice_dur} + ``` + +#### 2. 源码搜索"快速路径" — 确定性搜索减少LLM调用 +- **现状**:所有源码搜索都走Attributor Agent的LLM工具调用循环 +- **改进**:对于FQN完整、方法名明确的场景(非匿名内部类、非模糊匹配),直接用Python代码执行Glob→Grep→Read,不经过LLM决策 +- **价值**:减少50%+的token消耗和响应时间 +- **实现**:在 `_search_group()` 前增加确定性搜索判断 + ```python + if all(issue.get("search_type") == "java" and "$" not in issue["class_name"] for issue in group): + results = _deterministic_search(group, file_cache) + if all(r["reason"] == "found" for r in results): + return results # 跳过LLM + ``` + +#### 3. Headless/CI模式 — 一条命令完成全流程 +- **现状**:SI只支持交互式REPL +- **改进**:增加CLI参数 `--headless`,跳过交互直接执行 full pipeline +- **价值**:适合CI/CD集成和批量测试 + +### P1:体验和完整性提升 + +#### 4. 依赖引用搜索 — 扩展源码上下文 +- **现状**:Attributor只搜索目标类的方法体 +- **改进**:Read目标文件后,提取import列表中的项目内类和XML布局ID,一并搜索 +- **价值**:为AI诊断提供更完整的上下文,提升诊断准确度 +- **实现**:在Attributor Agent的Read后增加一步"关联分析" + +#### 5. package_list冷启动支持 +- **现状**:SI依赖 `process` 表获取包名,冷启动场景可能匹配失败 +- **改进**:在包名匹配失败时,fallback到 `package_list` 表反查 +- **价值**:支持冷启动性能分析场景 +- **实现**:修改 `pull_trace_from_device` 确保config包含 `linux.process_stats`,在 `collect_view_slices` 中增加 `package_list` fallback + +#### 6. SELinux兼容性 — cat管道绕过 +- **现状**:SI直接执行 `perfetto -c -` 命令 +- **改进**:增加cat管道方案作为fallback(`cat config.pb | perfetto -c -`) +- **价值**:在受限设备上正常采集 + +#### 7. Perfetto采集自动降级 +- **现状**:config模式失败时报错 +- **改进**:自动降级到命令行模式(类似Shell方案) +- **价值**:提高采集成功率 + +### P2:长期演进 + +#### 8. stack_profile_mapping关联 — Native调用栈可读性 +- **现状**:`collect_cpu_hotspots` 只重建函数调用链,不关联so/apk映射 +- **改进**:增加 `stack_profile_mapping` 查询,关联native库信息 +- **价值**:提升native方法调用栈的可读性 + +#### 9. 轻量快速诊断模式 +- **现状**:每次诊断都走完整Attributor流程 +- **改进**:增加"快速模式"跳过Attributor,由LLM基于经验推测 +- **价值**:快速初筛,减少等待时间 + +#### 10. JSON格式机器可读输出 +- **现状**:只输出Markdown报告 +- **改进**:增加JSON输出选项,包含结构化的问题列表和源码定位结果 +- **价值**:CI系统可解析,支持自动化处理 + +--- + +## 四、SI架构优势总结 + +SI相比Shell脚本方案的核心优势: + +1. **混合确定性+LLM架构**:算术和阈值分类由deterministic.py保证准确性,LLM只负责语言组织和因果分析。这是最关键的架构优势 +2. **SI$自定义Tag体系**:覆盖block/RV/inflate/view/handler/db/net/img/touch八大场景,远比依赖atrace原生tag精准 +3. **动态源码搜索**:Attributor Agent的LLM工具调用循环可以处理匿名内部类、模糊类名等复杂场景,比Shell的find/grep灵活得多 +4. **交互式分析**:REPL + Perfetto UI集成,支持深度追问和时间范围选择 +5. **模块化可扩展**:LangGraph状态机 + 独立Agent + 独立Prompt文件,添加新功能只需添加新Node +6. **BlockMonitor集成**:主线程卡顿检测 + 堆栈关联 + bisect时间匹配,填补Hook覆盖的盲区 + +SI当前的不足: +1. **Token消耗高**:多轮LLM调用,Attributor尤其消耗token +2. **响应时间长**:LLM搜索+多Agent串行执行 +3. **CI集成困难**:缺少非交互模式 +4. **线程状态分析浅**:缺少per-slice级别的Running/S/D区分 + +--- + +## 五、实施建议 + +### 阶段一:核心能力补强 +1. 新增 `collect_thread_state` 方法(P0-1) +2. 实现源码搜索快速路径(P0-2) +3. 新增Headless CLI模式(P0-3) + +### 阶段二:鲁棒性提升 +4. package_list冷启动fallback(P1-5) +5. SELinux兼容性改进(P1-6) +6. Perfetto采集自动降级(P1-7) +7. 依赖引用搜索扩展(P1-4) + +### 阶段三:长期演进 +8. Native调用栈可读性(P2-8) +9. 快速诊断模式(P2-9) +10. JSON机器可读输出(P2-10) + +每个改进点都可以独立实现和测试,不影响现有功能。建议按阶段推进,每个阶段完成后进行回归测试。 diff --git a/docs/perfetto-ui-bridge-design.md b/docs/perfetto-ui-bridge-design.md new file mode 100644 index 0000000..dd5f747 --- /dev/null +++ b/docs/perfetto-ui-bridge-design.md @@ -0,0 +1,545 @@ +# SI Agent x Perfetto UI 联动分析:可行性评估与设计方案 + +> 日期:2026-04-15 +> 状态:可行性调研完成,待实施 + +## 一、需求概述 + +用户在 Perfetto UI 中打开 trace 文件,浏览并选中耗时帧后,SI Agent 自动获取选中帧的详细数据,调用 LLM 进行深度分析(包括调用链归因、源码定位),并将分析结果实时展示给用户。 + +**核心价值**:将"全量自动分析"升级为"用户驱动的交互式分析",用户可聚焦关心的帧,获得更精准的分析结果。 + +## 二、现有架构概览 + +### 2.1 核心数据流 + +``` +用户输入 -> orchestrator(路由) -> collector(采集trace) -> analyzer(LLM分析) + -> attributor(源码归因) -> reporter(生成报告) +``` + +- **collector** 调用 `PerfettoCollector.pull_trace_from_device()` 通过 `adb shell perfetto` 采集 `.pb` trace 文件 +- 使用 `trace_processor_shell` (Python API `perfetto.TraceProcessor`) 执行 SQL 查询 +- 产出 `PerfSummary` JSON,包含 `frame_timeline`、`view_slices`、`block_events` 等 + +### 2.2 WebSocket 现状 + +`ws/server.py` 中 `SIServer` 是单例 WebSocket 服务器,与 Android App 通信: + +| 方向 | 消息类型 | 用途 | +| ---------- | ------------------------------------ | ---------------- | +| App->Server | `config_sync`, `block_events`, `ack` | 配置同步、事件上报 | +| Server->App | `config_update`, `start_trace`, `get_block_events` | 推送配置、触发采集 | + +**协议特点**:JSON 消息、UUID msg_id、ACK 确认、threading.Event 阻塞等待。扩展新消息类型只需在 `_dispatch()` 中添加 `elif` 分支。 + +### 2.3 帧数据能力 + +- `collect_frame_timeline()`:查询 `actual/expected_frame_timeline_slice`,检测 jank、计算 FPS、返回 top10 最慢帧 +- `collect_view_slices()`:查询 SI$ 自定义 slice,重建调用链(parent->grandparent),输出 slowest_slices / call_chains / rv_instances +- `compute_hints()`:确定性预计算(P0/P1/P2 严重度、jank 帧关联、CPU 热点) +- `extract_attributable_slices()`:从 view_slices 中过滤出可归因的 SI$ slice(排除系统类) + +## 三、Perfetto UI 扩展能力调研 + +### 3.1 Plugin 系统(最强大) + +Perfetto UI 拥有完整的 Plugin API: + +| 扩展点 | API | 描述 | +| ---------------- | ---------------------------------------- | ------------------------------------ | +| 自定义 Track | `trace.tracks.registerTrack()` | 注册 slice/counter/自定义 canvas track | +| 时间线 Overlay | `trace.tracks.registerOverlay()` | 在时间线上绘制箭头、标注、竖线 | +| Tab 面板 | `trace.registerTab()` | 在详情面板添加标签页 | +| 命令 | `app.commands.registerCommand()` | 注册命令面板操作+快捷键 | +| 侧边栏 | `trace.sidebar.addMenuItem()` | 添加侧边栏菜单项 | +| 区域选择 Tab | `trace.selection.registerAreaSelectionTab()` | 选择时间范围时显示自定义面板 | +| 时间线标注 | `trace.notes.addSpanNote()` | 添加高亮时间范围 | +| 编程式选择 | `trace.selection.selectTrackEvent()` | 程序化选中某个 slice | + +**关键限制**:所有 Plugin 必须是 in-tree(提交到 google/perfetto 仓库),或自行 fork 托管。不支持动态加载外部插件。 + +### 3.2 URL Deep Linking + +``` +https://ui.perfetto.dev/#!/?url=&visStart=&visEnd=&ts=&dur= +``` + +- `url`:自动打开远程 trace 文件(需 HTTPS + CORS) +- `visStart/visEnd`:初始视口范围(纳秒) +- `ts/dur`:定位并选中特定 slice +- `startupCommands`:JSON 数组,自动执行命令(PinTracks、AddDebugTrack、RunQuery 等) +- `embed`:嵌入模式,隐藏侧边栏 + +### 3.3 iframe + postMessage + +```javascript +// 父窗口向 iframe 中的 Perfetto UI 发送 trace +iframe.contentWindow.postMessage({ + perfetto: { buffer: arrayBuffer, title: 'My Trace' } +}, 'https://ui.perfetto.dev'); + +// 控制视口 +iframe.contentWindow.postMessage({ + perfetto: { timeStart: 123.456, timeEnd: 123.789 } +}, 'https://ui.perfetto.dev'); +``` + +**限制**:postMessage 只接受来自 `window.opener`、`window.parent` 或 opener 关系窗口的消息,background script 发送的消息会被忽略。 + +### 3.4 trace_processor_shell HTTP 模式 + +```bash +trace_processor_shell server http trace_file.pb --port 9001 +``` + +| 端点 | 用途 | +| ----------- | ------------------------------------------------- | +| `/websocket` | Protobuf-over-WebSocket,Perfetto UI 的主要通信通道 | +| `/rpc` | HTTP POST + Protobuf,Python API 使用 | +| `/query` | 执行 SQL 查询 | +| `/status` | Trace processor 状态 | + +Perfetto UI 自动检测 localhost:9001 并启用 "Trace Processor native acceleration"。 + +### 3.5 Chrome Extension + +**可行性低**。content script 运行在隔离世界,无法访问 Perfetto UI 内部 JS API。只能通过 iframe+postMessage 间接交互,或操作 DOM。 + +## 四、技术方案设计 + +### 4.1 方案对比 + +| 方案 | 优点 | 缺点 | 推荐度 | +| --------------------------------- | ------------------------------------------ | --------------------------------- | ------ | +| A: Fork Perfetto + 自定义 Plugin | 最强控制力,可注册自定义 Tab/命令/Overlay | 需维护 fork,构建复杂 | 3/5 | +| B: 本地 HTTP Server + iframe 嵌入 | 自托管 UI,绕过跨域限制,可注入 JS | 需要构建和部署 UI | 3/5 | +| C: trace_processor HTTP + Chrome Extension | 不修改 Perfetto,独立扩展 | Extension 能力受限,交互不自然 | 2/5 | +| **D: 本地 Web Server 桥接(推荐)** | **最小侵入,利用现有 WS 架构,独立前端页面** | **需开发桥接前端** | **5/5**| + +### 4.2 推荐方案:本地 Web Server 桥接 + +**核心思路**:不修改 Perfetto UI,在 SI Agent 侧构建一个轻量 Web Server 作为桥接层。 + +``` ++-----------------------------------------------------------+ +| 浏览器 | +| +------------------------+ +--------------------------+ | +| | 桥接页面 (localhost) | | Perfetto UI (iframe) | | +| | - JS 拦截用户选中 | | - 打开 trace | | +| | - WS 发送到 Agent | | - postMessage 控制 | | +| | - 显示分析结果 | | | | +| +----------+-------------+ +--------------------------+ | +| | WS (localhost:9877) | ++-------------+----------------------------------------------+ + | ++-------------+----------------------------------------------+ +| SI Agent (Python) | +| +----------+-------------+ +--------------------------+ | +| | Web Bridge Server | | trace_processor_shell | | +| | (aiohttp/websockets) | | HTTP mode :9001 | | +| | - 接收帧选中事件 | | - SQL 查询 | | +| | - 调用 Agent 分析 | | - 与 Perfetto UI 直连 | | +| +----------+-------------+ +--------------------------+ | +| | | +| +----------+-------------------------------------------+ | +| | LangGraph Pipeline (现有架构) | | +| | collector -> analyzer -> attributor -> reporter | | +| +------------------------------------------------------+ | ++-----------------------------------------------------------+ +``` + +### 4.3 关键技术细节 + +#### 4.3.1 trace_processor_shell HTTP 模式启动 + +```python +# collector/perfetto.py 扩展 +import subprocess + +class TraceServer: + """管理 trace_processor_shell HTTP 服务""" + + def __init__(self, trace_path: str, port: int = 9001): + self.trace_path = trace_path + self.port = port + self.process: subprocess.Popen | None = None + + def start(self): + self.process = subprocess.Popen( + [SHELL_BIN, "server", "http", self.trace_path, + "--port", str(self.port), "--ip-address", "127.0.0.1"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + # 等待就绪 + import urllib.request + for _ in range(50): + try: + urllib.request.urlopen(f"http://127.0.0.1:{self.port}/status") + return True + except Exception: + time.sleep(0.1) + return False + + def query(self, sql: str) -> list[dict]: + """通过 HTTP RPC 执行 SQL""" + from perfetto.trace_processor import TraceProcessor + tp = TraceProcessor(addr=f'localhost:{self.port}') + result = tp.query(sql) + return result.as_dict()['rows'] + + def stop(self): + if self.process: + self.process.terminate() +``` + +#### 4.3.2 桥接 Web Server + +```python +# ws/bridge_server.py (新增) +import asyncio +import json +from aiohttp import web + +class BridgeServer: + """桥接浏览器与 SI Agent 的 Web Server""" + + def __init__(self, port: int = 9877, agent_callback=None): + self.port = port + self.agent_callback = agent_callback # 回调:帧选中 -> Agent 分析 + self.ws_clients: set[web.WebSocketResponse] = set() + + async def handle_websocket(self, request): + ws = web.WebSocketResponse() + await ws.prepare(request) + self.ws_clients.add(ws) + try: + async for msg in ws: + data = json.loads(msg.data) + if data['type'] == 'frame_selected': + # 用户在 Perfetto UI 中选中了一个帧/slice + result = await self.agent_callback(data['payload']) + await ws.send_json({ + 'type': 'analysis_result', + 'payload': result + }) + finally: + self.ws_clients.remove(ws) + + async def handle_index(self, request): + """返回桥接页面 HTML""" + return web.Response(text=BRIDGE_HTML, content_type='text/html') + + def start(self): + app = web.Application() + app.router.add_get('/', self.handle_index) + app.router.add_get('/ws', self.handle_websocket) + web.run_app(app, port=self.port) +``` + +#### 4.3.3 桥接前端页面核心逻辑 + +```javascript +// 桥接页面 (嵌入 Perfetto UI iframe + 拦截选中事件) +const PERFETTO_URL = 'https://ui.perfetto.dev'; + +// 1. 通过 URL 打开 trace(trace_processor 在 localhost:9001) +const iframe = document.getElementById('perfetto'); +iframe.src = `${PERFETTO_URL}/#!/?url=http://127.0.0.1:9001`; + +// 2. 监听 iframe 消息 + 定时查询选中状态 +// Perfetto UI 没有直接暴露"选中变化"事件, +// 需要通过以下策略之一获取选中信息: + +// 策略A:用户手动触发(推荐 MVP) +// 用户选中 slice 后点击"分析"按钮,页面读取 URL hash 中的 ts/dur +document.getElementById('analyzeBtn').addEventListener('click', () => { + const hash = iframe.contentWindow.location.hash; + const params = new URLSearchParams(hash.split('?')[1] || ''); + const ts = params.get('ts'); + const dur = params.get('dur'); + if (ts && dur) { + ws.send(JSON.stringify({ type: 'frame_selected', payload: { ts, dur } })); + } +}); + +// 策略B:MutationObserver 监听 DOM 变化(高级) +// Perfetto UI 选中 slice 后会更新详情面板 DOM +const observer = new MutationObserver(() => { + const details = iframe.contentDocument.querySelector( + 'details-panel .slice-details' + ); + if (details) { + const ts = details.dataset.ts; + const dur = details.dataset.dur; + // 发送到 Agent... + } +}); + +// 策略C:轮询 URL hash 变化(最简单可靠) +let lastHash = ''; +setInterval(() => { + const hash = iframe.contentWindow.location.hash; + if (hash !== lastHash) { + lastHash = hash; + const params = new URLSearchParams(hash.split('?')[1] || ''); + if (params.get('ts')) { + // 用户导航到了新的位置,可以自动触发分析 + } + } +}, 500); +``` + +#### 4.3.4 Agent 分析回调 + +```python +# agents/frame_analyzer.py (新增) +async def analyze_selected_frame(payload: dict, state: AgentState) -> dict: + """分析用户在 Perfetto UI 中选中的帧""" + ts_ns = int(payload['ts']) + dur_ns = int(payload['dur']) + + # 1. 从 trace_processor 查询该帧的详细数据 + tp = TraceProcessor(addr='localhost:9001') + + # 查询该时间点的所有 slice + slices = tp.query(f""" + SELECT id, name, ts, dur, depth, track_id, cat + FROM slice + WHERE ts <= {ts_ns + dur_ns} AND ts + dur >= {ts_ns} + ORDER BY dur DESC + LIMIT 50 + """).as_dict()['rows'] + + # 查询关联的 frame timeline + frames = tp.query(f""" + SELECT * FROM actual_frame_timeline_slice + WHERE ts <= {ts_ns + dur_ns} AND ts + dur >= {ts_ns} + """).as_dict()['rows'] + + # 查询调用链 + call_chain = _build_call_chain(tp, slices[0]['id']) if slices else [] + + # 2. 构建分析上下文 + frame_context = { + 'selected_slice': slices[0] if slices else None, + 'overlapping_slices': slices[1:20], + 'frame_timeline': frames, + 'call_chain': call_chain, + 'existing_perf_summary': state.get('perf_summary'), + } + + # 3. 调用 LLM 分析(复用现有 analyzer prompt) + analysis = await _llm_analyze_frame(frame_context) + + return { + 'type': 'frame_analysis', + 'slice': slices[0] if slices else None, + 'analysis': analysis, + 'suggestions': _generate_suggestions(frame_context), + } +``` + +### 4.4 用户交互流程 + +``` +1. SI Agent 采集 trace -> 启动 trace_processor_shell HTTP 模式 +2. 自动打开浏览器 -> 桥接页面加载 -> iframe 嵌入 Perfetto UI +3. Perfetto UI 自动连接 localhost:9001(native acceleration) +4. 用户在 Perfetto UI 中浏览 trace,选中耗时帧 +5. 用户点击"分析此帧"按钮(或自动检测选中变化) +6. 桥接页面通过 WebSocket 将 {ts, dur, track_id} 发送给 Agent +7. Agent 通过 trace_processor HTTP API 查询该帧详细数据 +8. Agent 调用 LLM 分析(可复用现有 analyzer/attributor 能力) +9. 分析结果通过 WebSocket 回传给桥接页面展示 +``` + +## 五、与现有 Graph Pipeline 的集成可行性 + +### 5.1 集成方式评估 + +| 集成点 | 可行性 | 说明 | +| --------------------------- | ------ | --------------------------------------------------------- | +| 复用 PerfSummary | 5/5 | 帧分析可直接读取 `state['perf_summary']` 作为上下文 | +| 复用 analyzer prompt | 4/5 | 现有 `prompts/perf_analysis.txt` 可适配帧级分析 | +| 复用 attributor | 4/5 | `extract_attributable_slices()` + `run_attribution()` 可直接对选中帧归因 | +| 复用 deterministic.py | 5/5 | `compute_hints()` 的子函数(severity、call chain)可直接复用 | +| 新增 graph 节点 | 5/5 | LangGraph 架构支持新增 `frame_analyzer` 节点 | +| 复用 trace_processor | 5/5 | HTTP 模式与 Python API 查询结果格式一致 | +| 复用 WS 架构 | 4/5 | `SIServer` 可扩展,或独立 `BridgeServer` 并行运行 | + +### 5.2 推荐的 Graph 集成方案 + +**不修改现有 pipeline,新增独立交互路径:** + +```python +# state.py 扩展 +class AgentState(TypedDict): + # ... 现有字段 ... + selected_frame: dict | None # 新增:用户选中的帧信息 {ts, dur, track_id} + frame_analysis: str # 新增:帧分析结果 + +# builder.py 扩展 +def create_graph(): + # ... 现有节点 ... + builder.add_node("frame_analyzer", frame_analyzer_node) + # orchestrator 新增路由 + # 或者:frame_analyzer 作为独立入口,不经过 orchestrator +``` + +**两种集成模式:** + +1. **对话式**:用户在 REPL 中说"分析我选中的帧",orchestrator 路由到 `frame_analyzer` 节点 +2. **实时式**:用户在 Perfetto UI 中点击,通过 WS 直接触发分析,结果推送到前端页面(不经过 LangGraph) + +**推荐 MVP 用模式 2(实时式)**,因为交互更自然。后续可扩展为模式 1 实现对话式帧分析。 + +### 5.3 数据复用度分析 + +``` +现有 Pipeline 数据 -> 帧分析可复用: ++-- perf_summary (全量) -> 作为帧分析的上下文背景 ++-- view_slices.slowest -> 快速匹配选中帧 ++-- frame_timeline.jank -> 判断选中帧是否为 jank 帧 ++-- compute_hints() -> 复用 severity/classify 逻辑 ++-- extract_attributable_slices() -> 复用归因逻辑 ++-- attributor agent -> 复用源码搜索能力 ++-- prompts/*.txt -> 复用/扩展分析 prompt +``` + +## 六、关键技术风险与应对 + +| 风险 | 级别 | 应对方案 | +| --------------------------------- | ---- | --------------------------------------------------------------------- | +| Perfetto UI iframe 跨域限制 | 中 | 使用 `trace_processor_shell HTTP` 模式,Perfetto UI 自动连接本地服务 | +| 无法直接获取 Perfetto UI 选中事件 | 高 | URL hash 轮询(MVP)-> MutationObserver(进阶)-> Fork + Plugin API(终极方案) | +| trace_processor_shell HTTP 稳定性 | 低 | 官方支持的功能,Python API 底层就是用 HTTP 协议 | +| 大 trace 文件加载性能 | 中 | HTTP 模式下 Perfetto UI 使用 native acceleration(WASM -> native) | +| 桥接页面 UI 复杂度 | 中 | MVP 只做"选中+分析按钮+结果展示",后续迭代优化 | + +## 七、实现步骤(MVP) + +### Phase 1:基础设施(3 个文件改动 + 2 个新文件) + +1. **扩展 `collector/perfetto.py`**:新增 `TraceServer` 类,管理 `trace_processor_shell server http` 的生命周期 +2. **新增 `ws/bridge_server.py`**:aiohttp Web Server,提供桥接页面 + WebSocket 端点 +3. **新增 `ws/bridge.html`**(或内嵌在 bridge_server.py 中):桥接前端页面,嵌入 Perfetto UI iframe + +### Phase 2:帧分析核心(2 个新文件) + +4. **新增 `agents/frame_analyzer.py`**:接收帧选中信息,查询 trace_processor,调用 LLM 分析 +5. **新增 `graph/nodes/frame_analyzer.py`**(可选):如果需要集成到 LangGraph pipeline + +### Phase 3:端到端串联 + +6. **修改 `graph/cli.py`**:采集 trace 后自动启动 TraceServer + 打开浏览器 +7. **修改 `commands/trace.py`**:新增 `/open` 命令,手动打开 Perfetto UI 桥接页面 + +### Phase 4:体验优化 + +8. 自动检测选中变化(MutationObserver) +9. 分析结果面板美化(Markdown 渲染、源码高亮) +10. 支持多次选中分析、分析历史 + +## 八、插件系统深度调研(2026-04-15 补充) + +### 8.1 核心发现:必须 Fork + +Perfetto UI 插件 API 非常强大,但 **不支持外部加载**: + +> "All plugins are currently in-tree... There is no way, currently, to side-load closed-source plugins." + +插件位于 `ui/src/plugins/`,编译时打包。使用自定义插件只有两条路: +1. **向上游贡献** — PR 到 `google/perfetto`,Apache-2.0 开源(SI Agent 场景不现实) +2. **Fork + 自托管** — fork 仓库,添加自定义插件,构建部署(推荐) + +### 8.2 插件 API 能力 + +| 扩展点 | API | 说明 | +|--------|-----|------| +| 自定义 Track | `trace.tracks.registerTrack()` | slice/counter track,支持 SQL 驱动 | +| Tab 面板 | `trace.tabs.registerTab()` | 详情面板标签页,Mithril 渲染 | +| 命令+快捷键 | `trace.commands.registerCommand()` | 命令面板操作 | +| 区域选择 Tab | `trace.selection.registerAreaSelectionTab()` | 框选时间范围时显示自定义面板 | +| Overlay | `trace.tracks.registerOverlay()` | 时间线上画箭头、标注 | +| SQL 查询 | `trace.engine.query()` | 插件内执行 SQL | +| 持久状态 | `trace.mountStore()` | permalink 安全的状态 | + +### 8.3 选中事件响应方式 + +- `trace.selection.selection` 可读取当前选中状态 +- 无显式 `onSelectionChanged` 回调 +- **响应方式 1**:`registerAreaSelectionTab` — 用户框选区域时 Tab 的 `render()` 被调用,接收 `AreaSelection {start, end}` +- **响应方式 2**:自定义 Track 的 `detailsPanel()` — 用户点击 slice 时展示自定义详情面板 + +### 8.4 原方案 D 的跨域问题 + +原方案 D(iframe 嵌入 Perfetto UI)存在 **跨域限制**: +- Perfetto UI 在 `ui.perfetto.dev`,桥接页面在 `localhost` +- 跨域 iframe 无法读取 `contentWindow.location.hash` +- `MutationObserver` 无法访问跨域 `contentDocument` +- URL hash 轮询策略在跨域场景下不可行 + +### 8.5 修正方案:Fork + 自托管 + 插件 + +由于插件 API 原生支持区域选择 Tab 和自定义命令,Fork + 自托管是最佳方案: + +``` +1. Fork google/perfetto +2. 新建 com.smartinspector.Bridge 插件(~150 行 TS) +3. 插件通过 WebSocket 连接 SI Agent +4. 用户框选帧 → 点击分析 → WS 发送 ts/dur → Agent 分析 → 结果回传显示 +5. 构建静态文件 → 本地 HTTP Server 托管(同源,无跨域问题) +``` + +## 九、修正后的实施计划 + +### Phase 1:命令行验证(已完成) + +- `/frame ts=X dur=Y` 命令,复用现有 REPL 架构 +- `query_frame_slices()` + `analyze_frame()` agent +- 零前端开发,验证后端分析逻辑 + +### Phase 2:Fork + 自托管 Perfetto UI 插件 + +1. **创建 SI Bridge 插件** `perfetto-plugin/com.smartinspector.Bridge/index.ts` + - `registerAreaSelectionTab`: 用户框选帧 → 显示"分析"按钮 + 结果面板 + - `registerCommand`: 快捷键 `Ctrl+Shift+A` 触发分析 + - WebSocket 客户端连接 `ws://127.0.0.1:9877/bridge` +2. **构建脚本** `perfetto-plugin/build.sh`:clone + 复制插件 + 构建 + +### Phase 3:端到端串联 + +3. **Bridge Server** `ws/bridge_server.py`:aiohttp 服务,端口 9877 + - 托管 Perfetto UI 静态文件 + - WebSocket `/bridge` 端点接收插件消息 + - 调用 `frame_analyzer` agent 并回传结果 +4. **`/open` 命令**:启动 TraceServer + BridgeServer + 打开浏览器 +5. **自动启动**:`/trace` 完成后自动启动 TraceServer + +### Phase 4:体验优化(未来) + +6. 分析结果 Markdown 渲染 +7. 多次选中分析历史 +8. attributor 集成(源码定位直接显示在插件面板中) + +## 十、结论 + +**可行性评估:可行,推荐 Fork + 自托管方案。** + +- **Phase 1(已完成)**:命令行 `/frame` 验证后端分析逻辑 +- **Phase 2**:Fork Perfetto + 自定义插件,原生 UI 集成质量最高 +- **Phase 3**:Bridge Server 端到端串联,`/open` 一键打开 +- **关键技术路径**:Perfetto 插件 API → WebSocket → SI Agent frame_analyzer → 结果回传 +- **最大优势**:插件 API 原生支持区域选择 Tab,用户交互自然流畅 + +## 参考资料 + +- [Perfetto UI Plugin 文档](https://perfetto.dev/docs/contributing/ui-plugins) +- [Perfetto UI Deep Linking](https://perfetto.dev/docs/visualization/deep-linking-to-perfetto-ui) +- [Perfetto Commands Automation Reference](https://perfetto.dev/docs/visualization/commands-automation-reference) +- [Perfetto Plugin API (plugin.ts)](https://github.com/google/perfetto/blob/master/ui/src/public/plugin.ts) +- [Perfetto Trace API (trace.ts)](https://github.com/google/perfetto/blob/master/ui/src/public/trace.ts) +- [Perfetto Selection API (selection.ts)](https://github.com/google/perfetto/blob/master/ui/src/public/selection.ts) +- [Example Plugin: com.example.Tracks](https://github.com/google/perfetto/blob/main/ui/src/plugins/com.example.Tracks/index.ts) +- [trace_processor_shell HTTP 源码](https://github.com/google/perfetto/blob/master/src/trace_processor/rpc/httpd.cc) diff --git a/docs/sql-summarizer-and-verifier-spec.md b/docs/sql-summarizer-and-verifier-spec.md new file mode 100644 index 0000000..bd572a4 --- /dev/null +++ b/docs/sql-summarizer-and-verifier-spec.md @@ -0,0 +1,128 @@ +# SQL Summarizer & Analysis Verifier 设计文档 + +> 基于 SmartPerfetto 对比分析,引入两个核心优化 + +## 一、SQL Summarizer + +### 问题 +当前 `perf_analyzer.py` 和 `frame_analyzer.py` 直接将 SQL 查询结果(可能数千行)传给 LLM,导致: +- Token 消耗高(一次分析可能 10k+ tokens) +- LLM 在大量数据中容易遗漏关键信息或产生幻觉 +- 分析速度慢 + +### 方案 +在 `deterministic.py` 中新增 `summarize_sql_result()` 函数,对 SQL 结果进行压缩: + +```python +def summarize_sql_result( + rows: list[dict], + metric_col: str, + top_n: int = 10, + threshold_pct: float = 2.0, +) -> str: + """将 SQL 查询结果压缩为统计摘要 + 异常采样。 + + Args: + rows: SQL 查询结果列表 + metric_col: 用于统计的数值列名 + top_n: 异常行采样数量 + threshold_pct: 异常阈值(平均值的倍数) + + Returns: + 压缩后的文本摘要 + """ +``` + +### 压缩策略 +1. **统计值**:count, min, max, avg, p95, p99(一行搞定) +2. **分布直方图**:将值分桶(<16ms, 16-32ms, 32-64ms, >64ms),统计每桶数量 +3. **异常采样**:超过 avg * threshold_pct 的 top N 行 +4. **去重聚合**:相同 class.method 的多行聚合为一条(总耗时、调用次数) + +### 应用点 +1. `perf_analyzer.analyze_perf()` — perf_json 传入前压缩 +2. `frame_analyzer._build_frame_hints()` — frame_data 的 slices 列表压缩 +3. `frame_analyzer._run_source_attribution()` — attributable 列表压缩 +4. `deterministic.py` 的各分析函数中的数据预处理 + +### 预期收益 +- Token 消耗降低 60-80% +- LLM 分析速度提升 30%+ +- 减少幻觉(数据更聚焦) + +## 二、Analysis Verifier + +### 问题 +当前 SI 的 LLM 分析是单次调用,没有验证机制。分析结果可能: +- 含糊其辞,没有具体数据支撑 +- 遗漏重要问题 +- 格式不统一 + +### 方案 +在 `agents/` 下新增 `verifier.py`,实现分层验证: + +```python +def verify_analysis( + analysis_text: str, + raw_hints: str, + expected_fields: list[str] = None, +) -> VerificationResult: + """验证 LLM 分析结果的质量。 + + Returns: + VerificationResult(score, issues, passed) + """ +``` + +### 验证层级(3层) + +#### L1: Heuristic Check(纯规则,0 token) +- 结果是否包含具体数字(至少1个数值) +- 结果是否包含具体方法名或类名(至少1个) +- 结果长度是否合理(>100字符,<10000字符) +- 是否包含 P0/P1/P2 分级 + +#### L2: Consistency Check(纯规则,0 token) +- L1 预计算结论中标记为 P0 的问题,分析结果中是否提及 +- L1 预计算结论中的关键数据点(如帧率、CPU使用率),分析结果中的数值是否一致(±20%) +- 异常采样中的热点方法,分析结果中是否覆盖 + +#### L3: Depth Check(可选,1次 LLM 调用) +- 根因是否追溯到具体原因(而非"建议进一步排查") +- 是否给出可操作的优化建议 +- 仅在 L2 不通过时触发 + +### 验证结果处理 +- **L1+L2 全通过**:直接返回结果 +- **L2 不通过**:将缺失的关键信息和原始提示词重新组装,让 LLM 补充分析(最多1次重试) +- **L1 不通过**:标记为低质量,返回结果但附带警告 + +### 集成点 +1. `perf_analyzer.analyze_perf()` — 返回前调用 verify +2. `frame_analyzer.analyze_frame()` — 返回前调用 verify +3. `graph/nodes/reporter.py` — 报告生成前统一验证 + +## 三、实施计划 + +### Step 1: SQL Summarizer +1. 在 `deterministic.py` 中实现 `summarize_sql_result()` +2. 在 `perf_analyzer.py` 中集成:对 perf_json 中的 slices/block_events 列表应用压缩 +3. 在 `frame_analyzer.py` 中集成:对 frame_data 的 slices 和 attributable 列表应用压缩 +4. 测试:对比压缩前后的 token 消耗和 LLM 输出质量 + +### Step 2: Analysis Verifier +1. 新建 `agents/verifier.py`,实现 L1+L2 验证 +2. 在 `perf_analyzer.py` 和 `frame_analyzer.py` 中集成 +3. L3 暂不实现,作为后续优化项 +4. 测试:构造正常和异常的 LLM 输出,验证检测准确率 + +### Step 3: 文档更新 +1. 更新 CLAUDE.md Commands 章节 +2. 更新 README.md 功能列表 +3. 更新 docs/architecture-improvement-spec.md + +## 四、设计约束 +- **不改变现有 API 接口**:`analyze_perf()` 和 `analyze_frame()` 的签名不变 +- **不引入新依赖**:纯 Python 实现,用标准库 statistics 模块 +- **向后兼容**:压缩和验证都是内部优化,对外透明 +- **遵循 LangGraph Pipeline Architecture Rule**:如果涉及图节点变更,复用现有链路 diff --git a/docs/thread-state-blocking-analysis-design.md b/docs/thread-state-blocking-analysis-design.md new file mode 100644 index 0000000..acc1a24 --- /dev/null +++ b/docs/thread-state-blocking-analysis-design.md @@ -0,0 +1,471 @@ +# thread_state 阻塞原因分析 — 改造设计文档 + +> 生成日期:2026-04-23 +> 状态:方案设计,待实施 + +--- + +## 一、问题背景 + +### 1.1 当前 thread_state 的局限 + +`collect_thread_state` 目前基于 `sched` 表手动推算线程状态分布: + +1. 计算 `sched` 表中与 slice 窗口重叠的 Running 时间 +2. 用 `slice_duration - running_time` 推算 blocked 时间 +3. 用最近的 `sched.end_state` 分类为 Sleeping 或 DiskSleep + +**问题**:只输出 `{Running: 100%}` 或 `{Sleeping: 100%}`,无法回答「为什么阻塞」。 + +### 1.2 用户反馈 + +> "thread_state 的作用是什么,我从报告中没有感受到" + +测试 trace 中所有 SI$ 慢切片都是 CPU 密集型(100% Running),thread_state 的结论与源码归因结论完全重复。即使遇到真正的阻塞切片,当前实现也只能给出笼统的 "Sleeping" 标签,无法提供可操作的优化建议。 + +--- + +## 二、Perfetto 中可用的阻塞详情数据 + +通过实际查询 trace 文件,确认 `__intrinsic_thread_state` 表包含以下关键字段: + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| `state` | TEXT | 线程状态 | `Running`, `S`, `D`, `R+` | +| `blocked_function` | TEXT | **内核阻塞函数名** | `folio_wait_bit_common` | +| `io_wait` | INTEGER | **是否在等待IO** | `1` = IO等待 | +| `waker_utid` | INTEGER | **唤醒者的线程ID** | 可关联 `thread.name` | +| `irq_context` | INTEGER | 是否在中断上下文被唤醒 | `0`/`1` | +| `ucpu` | INTEGER | 用户态CPU时间 | nanoseconds | + +### 2.1 实际数据验证 + +以测试 trace(20s, com.smartinspector.hook)为例: + +**blocked_function 分布(跨所有线程):** + +| blocked_function | 含义 | 出现次数 | 总耗时 | +|-----------------|------|---------|--------| +| `worker_thread` | 工作线程等待 | 11177 | 745.5s | +| `rcu_gp_fqs_loop` | RCU内核周期 | 3879 | 15.1s | +| `msleep` | 内核主动睡眠 | 6 | 10.3s | +| `sde_encoder_helper_wait_for_irq` | 等待显示硬件中断 | 1017 | 3.5s | +| `_sde_encoder_cesta_update` | 显示编码器更新 | 1372 | 3.1s | +| `spi_geni_transfer_one` | SPI总线传输(触控IC) | 489 | 633ms | +| `folio_wait_bit_common` | **等待磁盘IO页缓存** | 1130 | 282ms | +| `rpmh_write_batch` | 硬件资源电源管理 | — | — | + +**waker(唤醒关系):** + +| 唤醒者 | 被唤醒者 | 次数 | +|--------|---------|------| +| Jit thread pool | Profile Saver | 49286 | +| swapper | TPP_MAIN | 10893 | +| surfaceflinger | RelBufCB | 1957 | +| tinspector.hook | logd.writer | 1794 | +| TimerDispatch | app (主线程) | 1312 | +| tinspector.hook | SI-BlockWatchdo | 1195 | + +**主线程 D 状态(DiskSleep):** +``` +RenderThread: blocked_function=rpmh_write_batch (0.04ms) + → 等待硬件资源电源管理器完成写入 +``` + +### 2.2 核心洞察 + +当前 `collect_thread_state` 用 `sched` 表推算,**丢弃了 `blocked_function`、`waker_utid`、`io_wait` 三个关键字段**。改造后可以直接回答: + +- **为什么阻塞?** → `folio_wait_bit_common` = 等待磁盘IO页缓存 +- **阻塞了多久?** → 45ms +- **谁唤醒的?** → `binder:1801_3` (Binder IPC 调用方) +- **是IO等待吗?** → `io_wait=1` 确认 + +--- + +## 三、改造方案 + +### 3.1 数据层:`collect_thread_state` 改用 `__intrinsic_thread_state` + +**位置**:`src/smartinspector/collector/perfetto.py` 的 `collect_thread_state()` 方法 + +#### 当前实现(sched 推算) + +```python +# 从 sched 表手动计算 Running 时间 +# blocked_time = slice_duration - running_time +# 无法获取 blocked_function, waker, io_wait +``` + +#### 改造后实现 + +```python +def collect_thread_state(self) -> list[dict]: + tp = self._open() + + # 获取主线程 utid + main_utid = self._resolve_main_utid(tp) + if main_utid is None: + return [] + + # 获取 SI$ 慢切片 + slice_rows = tp.query(""" + SELECT name, ts, dur + FROM slice + WHERE name LIKE 'SI$%' + AND name NOT LIKE 'SI$net#%' + AND name NOT LIKE 'SI$db#%' + AND dur > 1000000 + ORDER BY dur DESC + LIMIT 20 + """) + + results = [] + for sr in slice_rows: + slice_ts = sr.ts + slice_end = sr.ts + sr.dur + slice_name = sr.name + dur_ms = round(sr.dur / 1e6, 2) + + # 查询 __intrinsic_thread_state 获取阻塞详情 + state_rows = tp.query(f""" + SELECT + state, + SUM(dur) AS total_ns, + blocked_function, + io_wait, + waker_utid, + CASE WHEN waker_utid IS NOT NULL + THEN (SELECT name FROM thread WHERE utid = waker_utid) + ELSE NULL END AS waker_name + FROM __intrinsic_thread_state + WHERE utid = {main_utid} + AND ts < {slice_end} + AND ts + dur > {slice_ts} + GROUP BY state, blocked_function, io_wait, waker_utid + ORDER BY total_ns DESC + """) + + # 如果没有 thread_state 覆盖,推断为 Running + # (sleeping 线程无法执行产生 slice 的代码) + state_entries = list(state_rows) + if not state_entries: + results.append({ + "slice_name": slice_name, + "dur_ms": dur_ms, + "state_distribution": {"Running": 100.0}, + "dominant_state": "Running", + "blocked_function": None, + "io_wait": False, + "waker_name": None, + }) + continue + + total_ns = sum(r.total_ns for r in state_entries) + pct_dist = {} + blocked_fn = None + io_wait = False + waker_name = None + + for r in state_entries: + # 映射状态名 + state_label = _map_state_label(r.state) + pct = round(r.total_ns / total_ns * 100, 1) + pct_dist[state_label] = pct_dist.get(state_label, 0) + pct + + # 记录第一个非 Running 状态的阻塞详情 + if state_label != "Running" and blocked_fn is None: + blocked_fn = r.blocked_function + io_wait = bool(r.io_wait) + waker_name = r.waker_name + + dominant = max(pct_dist, key=pct_dist.get) + results.append({ + "slice_name": slice_name, + "dur_ms": dur_ms, + "state_distribution": pct_dist, + "dominant_state": dominant, + "blocked_function": blocked_fn, + "io_wait": io_wait, + "waker_name": waker_name, + }) + + return results +``` + +#### 状态映射函数 + +```python +def _map_state_label(raw_state: str) -> str: + """Map kernel thread state to human-readable label.""" + mapping = { + "Running": "Running", + "R": "Running", + "R+": "Running", + "S": "Sleeping", + "D": "DiskSleep", + "D+": "DiskSleep", + "T": "Stopped", + "t": "Traced", + "X": "Dead", + "Z": "Zombie", + } + return mapping.get(raw_state, raw_state) +``` + +#### 新增返回字段 + +```python +{ + "slice_name": "SI$RV#...", + "dur_ms": 73.0, + "state_distribution": {"Running": 80.0, "Sleeping": 20.0}, + "dominant_state": "Running", + # ↓ 新增字段 + "blocked_function": "futex_wait_queue_me", # 阻塞的内核函数 + "io_wait": False, # 是否IO等待 + "waker_name": "WorkerThread-2", # 谁唤醒了此线程 +} +``` + +### 3.2 预计算层:`_analyze_thread_state` 增加阻塞原因解读 + +**位置**:`src/smartinspector/agents/deterministic.py` 的 `_analyze_thread_state()` + +#### 改造内容 + +在现有 Running/Sleeping/DiskSleep 分类基础上,增加 `blocked_function` 解读: + +```python +# blocked_function 到人类可读原因的映射 +BLOCKED_FN_MEANING = { + "futex_wait_queue_me": "等待锁释放 (futex)", + "futex_wait": "等待锁释放 (futex)", + "folio_wait_bit_common": "等待磁盘IO (页缓存)", + "wait_woken": "等待被唤醒", + "msleep": "内核主动睡眠", + "rpmh_write_batch": "等待硬件资源电源管理", + "sde_encoder_helper_wait_for_irq": "等待显示硬件中断", + "spi_geni_transfer_one": "等待SPI总线传输", + "do_writepages": "等待磁盘写入", + "journal_commit": "等待文件系统日志提交", + "bio_wait": "等待块IO完成", + "pipe_wait": "等待管道数据", + "unix_stream_recvmsg": "等待Unix Socket数据", + " binder_thread_read": "等待Binder IPC回复", +} + +IO_WAIT_MEANING = { + True: "IO等待 (磁盘/网络/设备)", + False: "", # 非IO等待时省略 +} +``` + +#### 输出格式 + +**阻塞切片(Sleeping/DiskSleep 主导):** + +``` +[线程状态分析] + 以下切片主要处于阻塞状态: + + SharedPreferencesImpl.awaitLoadedLocked (45.2ms): + Sleeping 100% | 原因: futex_wait_queue_me (等待锁释放) + 唤醒者: main + + SQLiteDatabase.query (120.5ms): + DiskSleep 85%, Running 15% | 原因: folio_wait_bit_common (等待磁盘IO) + IO等待: 是 | 唤醒者: binder:1801_3 +``` + +**Running 切片(不变,但补充「无阻塞」说明):** + +``` + 以下切片主要在执行用户代码(无IO/锁阻塞): + + DemoAdapter.onBindViewHolder (73.0ms): Running 100% + CpuBurnWorker.startMainThreadWork (129.0ms): Running 100% +``` + +### 3.3 报告格式层:`format_perf_sections` 增加阻塞详情展示 + +**位置**:`src/smartinspector/graph/nodes/reporter/formatter.py` 的 `format_perf_sections()` + +#### 改造内容 + +```python +thread_states = perf_data.get("thread_state", []) +if thread_states: + ts_lines = ["## 线程状态分析\n"] + ts_lines.append("区分\"代码慢\"(Running)和\"被阻塞\"(Sleeping/DiskSleep):") + + # 分两组展示:先阻塞,后 Running + blocked = [ts for ts in thread_states + if ts.get("dominant_state") in ("Sleeping", "DiskSleep")] + running = [ts for ts in thread_states + if ts.get("dominant_state") == "Running"] + + if blocked: + ts_lines.append("") + ts_lines.append("**阻塞切片**(线程被IO/锁挂起,优化方向不是代码本身):") + for ts in blocked[:5]: + short = ts["slice_name"].replace("SI$", "") + dist_str = ", ".join(f"{k} {v:.0f}%" for k, v in ts["state_distribution"].items()) + ts_lines.append(f"- {short} ({ts['dur_ms']:.1f}ms): {dist_str}") + # 新增:阻塞原因 + bf = ts.get("blocked_function") + if bf: + meaning = BLOCKED_FN_MEANING.get(bf, bf) + ts_lines.append(f" 阻塞原因: {meaning}") + if ts.get("io_wait"): + ts_lines.append(f" 类型: IO等待") + if ts.get("waker_name"): + ts_lines.append(f" 唤醒者: {ts['waker_name']}") + + if running: + ts_lines.append("") + ts_lines.append("**Running切片**(代码执行慢,需优化算法或异步化):") + for ts in running[:5]: + short = ts["slice_name"].replace("SI$", "") + ts_lines.append(f"- {short} ({ts['dur_ms']:.1f}ms): Running 100%") + + user_parts.append("\n".join(ts_lines)) +``` + +### 3.4 报告生成层:LLM Prompt 增加 blocked_function 上下文 + +**位置**:`src/smartinspector/graph/nodes/reporter/` 的 prompt 模板 + +在预计算结论 section 中,blocked_function 信息将帮助 LLM 生成**针对性的建议**,而非模板化建议: + +| blocked_function | LLM 应生成的建议 | +|-----------------|----------------| +| `futex_wait_queue_me` | 「主线程在等待锁释放,检查是否有后台线程持有锁(如 SharedPreferences 同步提交)」 | +| `folio_wait_bit_common` | 「等待磁盘IO完成,考虑使用异步IO或增加缓存命中率」 | +| `binder_thread_read` | 「等待 Binder IPC 回复,考虑将同步 Binder 调用改为异步」 | +| `spi_geni_transfer_one` | 「等待SPI总线传输(通常为触控IC),属于硬件瓶颈,应用层无法优化」 | +| `NULL` + Running 100% | 「代码执行慢,需优化算法复杂度或将耗时操作移至后台线程」 | + +--- + +## 四、涉及文件清单 + +| 文件 | 改动类型 | 说明 | +|------|---------|------| +| `src/smartinspector/collector/perfetto.py` | **重写** | `collect_thread_state` 改用 `__intrinsic_thread_state` 表 | +| `src/smartinspector/agents/deterministic.py` | **增强** | `_analyze_thread_state` 增加 `blocked_function` 解读和分组展示 | +| `src/smartinspector/graph/nodes/reporter/formatter.py` | **增强** | `format_perf_sections` 展示阻塞原因、IO等待、唤醒者 | +| `src/smartinspector/graph/nodes/reporter/prompts.py` | **微调** | Reporter prompt 增加阻塞原因相关的分析指引(可选) | + +--- + +## 五、降级策略 + +`__intrinsic_thread_state` 表可能在某些 Perfetto 版本中不可用。需要 fallback: + +```python +# 1. 先尝试 __intrinsic_thread_state +try: + state_rows = tp.query("SELECT 1 FROM __intrinsic_thread_state LIMIT 1") + has_intrinsic_ts = True +except: + has_intrinsic_ts = False + +# 2. 如果不可用,回退到当前 sched 推算逻辑(保持现有行为) +if not has_intrinsic_ts: + return self._collect_thread_state_fallback() +``` + +--- + +## 六、预期效果 + +### 改造前报告 + +``` +### P0 [DemoAdapter.onBindViewHolder] + +现象:onBindViewHolder 耗时 73ms。 + +原因:同步解码 Bitmap 导致主线程阻塞。 + +建议: +1. 使用异步图片加载库 +2. 在后台线程预解码 +3. 增大缓存容量 +``` + +→ 所有建议都是模板化的「通用优化方案」,无法区分问题类型。 + +### 改造后报告 + +**场景 A:Running 100%(代码慢)** + +``` +### P0 [DemoAdapter.onBindViewHolder] + +现象:onBindViewHolder 耗时 73ms。 +线程状态:Running 100%(纯代码执行,无IO/锁阻塞) + +原因:BitmapFactory.decodeResource 同步解码 200x200 图片,在主线程执行IO密集操作。 +虽然 thread_state 显示 Running,但 decodeResource 底层调用了磁盘读取, +只是因为读取速度快未触发 D 状态。建议改用异步图片加载库。 + +建议: +1. 使用 Coil/Glide 异步加载,主线程只负责设置 placeholder +2. 增大 bitmapCache 容量至可见 item 数量的 2 倍 +``` + +**场景 B:Sleeping 主导(锁等待)— 改造后新增的识别能力** + +``` +### P0 [SharedPreferencesImpl.awaitLoadedLocked] + +现象:awaitLoadedLocked 耗时 45ms。 +线程状态:Sleeping 100% +阻塞原因:futex_wait_queue_me(等待锁释放) +唤醒者:SharedPreferencesImpl.writeThread + +原因:主线程在 awaitLoadedLocked 中通过 futex 等待 SharedPreferences 文件加载完成。 +writeThread 在后台加载 XML 文件,但主线程的 getSharedPreferences() 调用发起了同步等待。 +在启动阶段,如果 SP 文件较大(>100KB),这个等待可能超过一帧(16ms)。 + +建议: +1. 使用 commit() 替代 apply() 避免阻塞(已废弃) +2. 将 SP 读取移到 Application.onCreate 之前(Multidex 期间并行加载) +3. 考虑迁移到 DataStore(基于 Protocol Buffer,支持异步) +``` + +**场景 C:DiskSleep 主导(磁盘IO)— 改造后新增的识别能力** + +``` +### P0 [SQLiteDatabase.query] + +现象:query 耗时 120ms。 +线程状态:DiskSleep 85%, Running 15% +阻塞原因:folio_wait_bit_common(等待磁盘IO页缓存) +IO等待:是 +唤醒者:kworker/2:1 + +原因:SQL 查询触发了磁盘页缓存缺失,需要从闪存读取数据。 +120ms 中 102ms 花在等待磁盘IO,仅 18ms 在执行查询逻辑。 +属于磁盘IO瓶颈而非查询语句效率问题。 + +建议: +1. 增加 WAL 模式的 checkpoint 频率,减少读阻塞 +2. 对热点表建立索引减少扫描量 +3. 考虑使用 Room 的分页查询(Paging 3)避免一次加载过多数据 +``` + +--- + +## 七、实施计划 + +| 步骤 | 内容 | 预计改动量 | +|------|------|-----------| +| 1 | `perfetto.py` 重写 `collect_thread_state`,新增 `_map_state_label` | ~80 行 | +| 2 | `deterministic.py` 增强 `_analyze_thread_state`,新增 `BLOCKED_FN_MEANING` 映射 | ~40 行 | +| 3 | `formatter.py` 增强 thread_state 展示,增加阻塞原因/IO等待/唤醒者 | ~25 行 | +| 4 | 添加 fallback 逻辑(`__intrinsic_thread_state` 不可用时回退到 sched 推算) | ~15 行 | +| 5 | 用现有 trace 验证 Running 100% 场景不变,验证降级逻辑 | 测试 | +| 6 | 构造 Sleeping/DiskSleep 测试场景验证新增数据路径 | 测试 | diff --git a/feat-spec-2026-04-24 b/feat-spec-2026-04-24 new file mode 100644 index 0000000..7497b61 --- /dev/null +++ b/feat-spec-2026-04-24 @@ -0,0 +1,819 @@ +# SmartInspector 功能完善与架构优化 Spec + +> **版本**: v1.0 +> **日期**: 2026-04-24 +> **范围**: Android 平台优先 + 全平台扩展规划 +> **目标读者**: 开源贡献者 +> **里程碑**: 1 个月 + +--- + +## 1. 项目现状评估 + +### 1.1 架构概览 + +``` +┌─────────────────────────────────────────────────────────┐ +│ CLI (graph/cli.py) │ +│ LangGraph REPL + Slash Commands │ +├──────────┬──────────┬──────────┬────────────────────────┤ +│Orchestr- │ Explorer │ Android │ Pipeline Chain │ +│ator │ (grep/ │ Expert │ Collector → Analyzer │ +│(LLM路由) │ glob/ │ (adb/ │ → Attributor → Reporter│ +│ │ read) │ perfetto)│ │ +├──────────┴──────────┴──────────┴────────────────────────┤ +│ Collector (collector/perfetto.py) │ +│ SQL → PerfSummary JSON (~2KB) │ +├─────────────────────────────────────────────────────────┤ +│ Bridge Server (ws/bridge_server.py) │ +│ Perfetto UI Plugin ↔ Frame Analyzer │ +├─────────────────────────────────────────────────────────┤ +│ Android Hook Layer (tracelib) │ +│ TraceHook (Pine AOP) + BlockMonitor + SIClient │ +└─────────────────────────────────────────────────────────┘ +``` + +### 1.2 已实现能力 + +| 层级 | 能力 | 实现质量 | +|------|------|----------| +| **Android Hook** | Activity/Fragment 生命周期追踪 | 完整,含动态子类发现 | +| | RecyclerView 全管线追踪 | 完整,含 setAdapter 动态 hook | +| | LayoutInflate 追踪 | 完整,含 layout 资源名 | +| | View measure/layout/draw | 完整,仅自定义 View | +| | Handler dispatch | 完整,仅主线程 | +| | BlockMonitor 卡顿检测 | 完整,API 29+ Observer + Printer fallback | +| | Network IO (OkHttp/HttpURLConn) | 代码已实现,默认关闭 | +| | Database IO (SQLite/Room) | 代码已实现,默认关闭 | +| | Image Load (Glide/Coil) | 代码已实现,默认关闭 | +| | Input Event (dispatchTouchEvent) | 完整 | +| **Python Pipeline** | Perfetto trace 采集 | 完整,含 SELinux bypass | +| | 帧时间线分析 (FPS/Jank) | 完整,含 SurfaceFlinger jank type | +| | 自定义切片分析 (SI$ tags) | 完整,含 parent_id 调用链重建 | +| | CPU 热点分析 | 完整,含 callchain 重建 | +| | 线程状态分析 (Running/Sleeping/D) | 完整,overlap-based SQL | +| | 确定性预计算 | 完整,P0/P1/P2 严重度分类 | +| | 源码归因 (Attributor) | 完整,fast path + LLM fallback + 依赖搜索 | +| | 报告生成 | 完整,流式输出 | +| **Perfetto UI** | Bridge Server + 自定义插件 | 完整,frame_selected 交互分析 | +| | TraceServer (HTTP SQL 查询) | 完整 | + +### 1.3 关键差距(基于代码分析) + +#### A. Android Hook 层差距 + +1. **IO hooks 虽已实现但未启用且未经测试** ~~→ ✅ P0-1 已修复:默认开启,Python 端 IO 切片收集和归因已实现~~ +2. **无 Compose 支持**:Jetpack Compose 的重组(recomposition)追踪完全缺失,而 Compose 已是 Android UI 主流。 +3. **无 Coroutine 追踪**:协程的线程切换和挂起无法追踪。 +4. **无冷启动专项分析** ~~→ ✅ P0-2 已修复:`collector/startup.py` + `graph/nodes/startup.py` 已实现~~ +5. **ExtraHook 参数推断简陋**:`hookExtraClasses()` 只尝试无参签名(`TraceHook.java:782-783`),无法精确匹配方法参数。 +6. **无 GPU 渲染分析**:`HookConfig.collectGpuMem` 默认关闭,无 GPU pipeline 追踪。 + +#### B. Python 分析管线差距 + +7. **无内存分配追踪**:`PerfSummary` 有 `memory` 字段但 `summarize()` 中未实现 `collect_memory()` — 只采集了 RSS 计数器,无对象级分配分析。 +8. **IO slices 未纳入归因** ~~→ ✅ P0-5 已修复:IO 切片归因已实现,`commands/attribution.py` 支持 SI$net#/SI$db#/SI$img# 解析~~ +9. **无跨进程分析**:`_resolve_target_process()` 只关注单进程(`perfetto.py:92-173`),无法分析多进程交互。 +10. **headless/CI 模式缺失** ~~→ ✅ P0-3 已修复:`headless.py` + `--ci` CLI 参数已实现~~ +11. **报告仅 Markdown** ~~→ ✅ P0-4 已修复:`json_formatter.py` 已实现 JSON 格式输出~~ +12. **无历史对比**:多次分析结果无法对比趋势。 + +#### C. 开发者体验差距 + +13. **一键分析不够简单**:需要 5+ 步才能完成一次完整分析(连设备 → 启动 app → 采集 trace → 分析 → 查看报告)。 +14. **配置管理分散**:Android 端 `SharedPreferences`、CLI 端环境变量、WS 协议三套配置。 +15. **错误恢复差**:`node_error_handler` 只捕获异常返回错误消息,不重试。 + +--- + +## 2. 分阶段路线图 + +### P0: 核心体验完善(第 1-2 周) + +> 目标:让现有功能真正好用,解决"能用但不好用"的问题 + +| # | 任务 | 影响文件 | 优先级理由 | 状态 | +|---|------|----------|-----------|------| +| P0-1 | **IO hooks 验证与启用** | `TraceHook.java`, `HookConfig.java`, `collector/perfetto.py` | 代码已写好,只需测试+默认开启 | ✅ 已完成 | +| P0-2 | **冷启动专项分析模式** | `orchestrator.py`, `collector_node`, `TraceHook.java`, 新增 `collect_startup.py` | 移动端最高频优化场景 | ✅ 已完成 | +| P0-3 | **Headless/CI 模式** | `cli.py`, 新增 `headless.py`, `reporter/formatter.py` | 开源贡献者基本需求 | ✅ 已完成 | +| P0-4 | **JSON 报告格式** | `reporter/`, 新增 `json_formatter.py` | CI 集成 + 自动化 | ✅ 已完成 | +| P0-5 | **IO slices 归因** | `commands/attribution.py`, `agents/attributor.py` | 将已有数据纳入完整分析 | ✅ 已完成 | + +### P1: 分析能力扩展(第 3-4 周) + +> 目标:扩展分析深度,覆盖更多 Android 性能场景 + +| # | 任务 | 影响文件 | 优先级理由 | 状态 | +|---|------|----------|-----------|------| +| P1-1 | **Compose 重组追踪** | 新增 `ComposeHook.kt`, `collector/perfetto.py` | Android UI 现代化需求 | ✅ 已完成 | +| P1-2 | **内存分配分析** | `collector/perfetto.py` (heap_graph), 新增 `collect_memory.py` | OOM 和内存泄漏是第二大类问题 | ✅ 已完成 | +| P1-3 | **历史对比与趋势** | 新增 `storage/`, `commands/compare.py` | 持续性能监控 | ✅ 已完成 | +| P1-4 | **智能一键分析** | `cli.py`, `commands/quick.py` | 降低使用门槛 | ✅ 已完成 | +| P1-5 | **ExtraHook 参数自动推断** | `TraceHook.java` | 自定义 hook 可用性 | ✅ 已完成 | + +### P2: 生态扩展(后续) + +> 目标:多平台支持 + 高级分析能力 + +| # | 任务 | 说明 | 状态 | +|---|------|------|------| +| P2-1 | **HarmonyOS 平台支持** | hdc + hiperf/hitrace,已有设计文档 | 规划中 | +| P2-2 | **iOS Instruments 集成** | instruments CLI + .trace 文件解析 | 规划中 | +| P2-3 | **实时监控模式** | streaming adb logcat + 持续分析 | 规划中 | +| P2-4 | **ANR 自动捕获与分析** | ANR trace 文件解析 + 归因 | 规划中 | +| P2-5 | **网络瀑布图** | OkHttp 事件追踪 + 时间线可视化 | 规划中 | +| P2-6 | **源码归因增强(借鉴claude-context)** | .gitignore过滤、函数级分块搜索、轻量BM25索引 | 规划中 | +| P2-7 | **Coroutine追踪** | 协程线程切换和挂起追踪 | 规划中 | + +--- + +## 3. P0 详细设计 + +### 3.1 P0-1: IO Hooks 验证与启用 + +**现状**:`TraceHook.java` 中 `hookNetworkIo()` (L632)、`hookDatabaseIo()` (L659)、`hookImageLoad()` (L709) 已完整实现,Python 端 `collect_io_slices()` (L977) 已有查询。只是 `HookConfig` 默认关闭且未经集成测试。 + +**变更清单**: + +``` +platform/android/tracelib/src/main/java/com/smartinspector/tracelib/HookConfig.java + - networkIo: false → true + - databaseIo: false → true + - imageLoad: false → true + +src/smartinspector/commands/attribution.py + - extract_attributable_slices() 增加对 SI$net#/SI$db#/SI$img# 的解析 + +src/smartinspector/agents/deterministic.py + - 新增 _analyze_io_slices() helper + - 对 IO 切片按类型分类统计 + 按耗时排序 + +src/smartinspector/graph/nodes/reporter/formatter.py + - IO 分析结果格式化为报告章节 +``` + +**验证方式**: +1. Demo App 增加 IO 场景(网络请求、数据库读写、图片加载) +2. `/full` 分析验证 IO slices 出现在报告中 +3. 确认 IO hooks 对主线程 ANR 无影响 + +### 3.2 P0-2: 冷启动专项分析模式 + +**现状**:`orchestrator.py:104-113` 已检测冷启动关键词并设置 `skip_wait=True`。但缺少: +- 启动阶段自动切分(pre-main → main → first-frame → full-frame) +- Application.onCreate 追踪(已 hook `TraceHook.java:263-278`,但无下游分析) +- 启动链路可视化 + +**设计**: + +```python +# 新增 collector/startup.py + +class StartupAnalyzer: + """冷启动分析器:从 trace 中提取启动链路""" + + def analyze_cold_start(self, collector: PerfettoCollector) -> StartupResult: + # Phase 1: 定位 app 进程启动时间 + # SELECT MIN(ts) FROM process_track WHERE ... + # 或 package_list 表中 uid 首次出现 + + # Phase 2: 切分启动阶段 + # pre_main: fork → Application.attachBaseContext + # init: Application.onCreate → Activity.onCreate + # first_frame: Activity.onCreate → 首帧 doFrame + # full_draw: 首帧 → 首帧完成后绘制完毕 + + # Phase 3: 每个阶段的 SI$ 切片聚合 + # 与 view_slices 查询复用,按时间窗口过滤 + + # Phase 4: 关键路径提取 + # 主线程上 dur 最长的切片链 +``` + +**CLI 集成**: +``` +用户输入: "分析冷启动" / "分析启动耗时" +→ orchestrator → collector (skip_wait=True, duration=15s) + → startup_analyzer → 报告 +``` + +**报告输出格式**: +```markdown +## 冷启动分析 + +总耗时: 1.23s + +| 阶段 | 耗时 | 占比 | +|------|------|------| +| pre-main (进程启动) | 120ms | 10% | +| Application.onCreate | 350ms | 28% | +| Activity.onCreate → 首帧 | 580ms | 47% | +| 首帧渲染完成 | 180ms | 15% | + +### 关键瓶颈 +1. **Application.onCreate (350ms)** + - DataRepository.initialize: 200ms (SI$inflate#splash_layout) + - ... + +### 优化建议 +- 延迟初始化: DataRepository.initialize 可移至后台线程 +- 布局优化: splash_layout 层级过深 (4 层 LinearLayout) +``` + +### 3.3 P0-3: Headless/CI 模式 + +**设计**: + +```bash +# 非交互模式 +smartinspector --ci \ + --target com.example.app \ + --duration 10000 \ + --source-dir /path/to/source \ + --output reports/report.json \ + --format json + +# 分析已有 trace +smartinspector --ci \ + --trace /path/to/trace.pb \ + --source-dir /path/to/source \ + --output reports/report.md \ + --format markdown +``` + +**新增文件**: +``` +src/smartinspector/headless.py # HeadlessRunner 类 +src/smartinspector/commands/quick.py # /quick 命令实现 +``` + +**核心逻辑**: +```python +class HeadlessRunner: + """非交互式分析运行器,跳过 REPL 直接执行完整管线""" + + def run(self, trace_path: str = None, + collect: bool = True, target: str = None, + source_dir: str = ".", output: str = None, + format: str = "markdown") -> str: + + if collect: + trace_path = PerfettoCollector.pull_trace_from_device(...) + collector = PerfettoCollector(trace_path, target_process=target) + summary = collector.summarize() + + # 确定性分析 + hints = compute_hints(summary.to_json()) + + # LLM 分析 + analysis = analyze_perf(summary.to_json()) + + # 源码归因 + attribution = run_attribution(attributable_slices) + + # 生成报告 + if format == "json": + report = format_json(summary, analysis, attribution) + else: + report = format_markdown(summary, analysis, attribution) + + if output: + Path(output).write_text(report) + return report +``` + +### 3.4 P0-4: JSON 报告格式 + +**Schema 设计**: + +```json +{ + "version": "1.0", + "timestamp": "2026-04-24T10:30:00Z", + "target": { + "package": "com.example.app", + "device": "Pixel 7, Android 14" + }, + "trace": { + "path": "/tmp/si_trace_xxx.pb", + "duration_ms": 10000, + "start_ts": 1234567890 + }, + "summary": { + "fps": 58.3, + "total_frames": 580, + "jank_frames": 12, + "cpu_usage_pct": 45.2 + }, + "issues": [ + { + "severity": "P0", + "category": "ui_thread_block", + "title": "DemoAdapter.onBindViewHolder 耗时 20ms", + "duration_ms": 20.0, + "frame_budget_pct": 120, + "source": { + "file": "adapter/DemoAdapter.java", + "line_start": 45, + "line_end": 52, + "snippet": "...", + "finding": "Thread.sleep(20) 在主线程执行" + }, + "recommendation": "将 Thread.sleep 替换为异步加载" + } + ], + "metrics": { + "frame_timeline": { ... }, + "cpu_hotspots": [ ... ], + "thread_state": [ ... ], + "io_slices": { ... }, + "memory": { ... } + } +} +``` + +### 3.5 P0-5: IO Slices 归因 + +**变更点**: + +在 `commands/attribution.py` 的 `extract_attributable_slices()` 中增加 IO 切片解析: + +```python +# 现有: 只处理 SI$ 开头且排除 SI$net#/SI$db#/SI$img# +# 变更: 对 IO 切片也进行类名/方法名提取和归因 + +def _parse_io_slice_name(name: str) -> dict: + """解析 IO slice 名称为 class.method + + SI$net#okhttp3.internal.connection.RealCall.execute + SI$db#com.example.data.DatabaseHelper.query#users + SI$img#com.bumptech.glide.request.RequestBuilder.into + """ + # 去掉 SI$prefix# 得到 FQN + for prefix in ("SI$net#", "SI$db#", "SI$img#"): + if name.startswith(prefix): + body = name[len(prefix):] + # 可能有 #table 后缀 (DB query) + parts = body.split("#") + fqn = parts[0] + # 解析 class.method + if "." in fqn: + last_dot = fqn.rfind(".") + return { + "class_name": fqn[:last_dot], + "method_name": fqn[last_dot+1:], + "io_type": prefix[3:-1], # "net"/"db"/"img" + } + return None +``` + +在 `agents/deterministic.py` 新增 `_analyze_io_slices()`: + +```python +def _analyze_io_slices(data: dict) -> str: + """分析 IO 切片:按类型聚合,标记主线程 IO""" + io_slices = data.get("io_slices") or {} + if not io_slices: + return "" + + summary = io_slices.get("summary") or [] + lines = ["[IO 分析]"] + + # 按类型聚合 + by_type = {} + for s in summary: + io_type = s.get("io_type", "unknown") + if io_type not in by_type: + by_type[io_type] = {"count": 0, "total_ms": 0, "max_ms": 0, "items": []} + by_type[io_type]["count"] += s["count"] + by_type[io_type]["total_ms"] += s["total_ms"] + by_type[io_type]["max_ms"] = max(by_type[io_type]["max_ms"], s["max_ms"]) + by_type[io_type]["items"].append(s) + + for io_type, stats in sorted(by_type.items(), key=lambda x: -x[1]["total_ms"]): + lines.append( + f" {io_type}: {stats['count']}次, " + f"总耗时{stats['total_ms']:.1f}ms, " + f"最大{stats['max_ms']:.1f}ms" + ) + + return "\n".join(lines) +``` + +--- + +## 4. P1 详细设计 + +### 4.1 P1-1: Compose 重组追踪 + +**技术方案**: + +Compose 编译器会在编译期为每个可组合函数生成代码,并使用 `Composer` 进行追踪。Android 已在 `androidx.compose.runtime` 中提供了 `TraceInformation` 接口(API 31+)。 + +**Hook 方案**: + +```kotlin +// 新增 ComposeHook.kt +object ComposeHook { + + fun hook() { + // 方案 A: Hook ComposeRuntime 的 trace 系统 + // compose.runtime.internal.TracerImpl.begin / end + try { + val tracer = Class.forName( + "androidx.compose.runtime.internal.TracerImpl" + ) + safeHookMethod(tracer, "beginSection", ...) + safeHookMethod(tracer, "endSection", ...) + } catch (e: Exception) { + // Fallback: 方案 B + } + + // 方案 B: Hook Compose 的 remember 和 recompose + // 追踪每个 Composable 的调用次数和耗时 + try { + val composer = Class.forName( + "androidx.compose.runtime.ComposerImpl" + ) + // startRestartGroup / endRestartGroup + safeHookMethod(composer, "startRestartGroup", ...) + } catch (e: Exception) { + Log.w(TAG, "Compose hook failed: ${e.message}") + } + } +} +``` + +**Perfetto 集成**: +- 切片前缀:`SI$compose#` +- Tag 格式:`SI$compose#ComposableName#recompose` / `SI$compose#ComposableName#first` +- 下游 Python 端新增 `collect_compose_slices()` 查询 + +### 4.2 P1-2: 内存分配分析 + +**现状**:`PerfettoCollector` 已采集 `collectJavaHeap=true`(在 Perfetto 配置中),但 `summarize()` 中 `memory` 字段只从 `process_counter_track` 获取 RSS,未利用 `heap_graph` 表。 + +**新增分析**: + +```python +def collect_memory(self, target_upid: int) -> dict: + """分析 Java 堆内存分配和对象分布""" + tp = self._open() + + # 1. Java 堆对象统计 (heap_graph) + # 按类名聚合对象数量和总大小 + heap_stats = tp.query(""" + SELECT + o.type_name, + COUNT(*) AS obj_count, + SUM(o.self_size) AS total_size + FROM heap_graph_object o + JOIN heap_graph_class c ON o.type_name = c.name + WHERE o.upid = {upid} + AND o.reachable = 1 + GROUP BY o.type_name + ORDER BY total_size DESC + LIMIT 20 + """) + + # 2. 内存增长趋势 (process_counter_track) + # RSS / anon 内存随时间变化 + + # 3. Activity/Fragment 泄漏检测 + # 查找已 destroy 但仍被引用的 Activity/Fragment + leak_suspects = tp.query(""" + SELECT o.type_name, o.self_size + FROM heap_graph_object o + WHERE o.type_name LIKE '%Activity%' + OR o.type_name LIKE '%Fragment%' + ORDER BY o.self_size DESC + LIMIT 10 + """) + + return { + "heap_objects": [...], + "memory_trend": [...], + "leak_suspects": [...], + } +``` + +### 4.3 P1-3: 历史对比 + +**存储设计**: + +``` +reports/ + ├── 2026-04-24_103000_report.json # JSON 格式报告 + ├── 2026-04-24_103000_trace.pb # 原始 trace + └── baseline.json # 基线报告 (可选) +``` + +**对比命令**: + +```bash +/compare + +# 输出: +## 性能对比报告 + +| 指标 | 报告 A (4/20) | 报告 B (4/24) | 变化 | +|------|--------------|--------------|------| +| FPS | 55.2 | 58.3 | +5.6% ↑ | +| Jank 帧 | 18 | 12 | -33% ↓ | +| 平均帧耗时 | 18.5ms | 16.2ms | -12% ↓ | +| CPU | 52% | 45% | -13% ↓ | + +### 回归项 +- DemoAdapter.onBindViewHolder: 15ms → 20ms (+33%) ⚠️ + +### 改善项 +- MainThread IO: 80ms → 10ms (-87%) ✓ +``` + +### 4.4 P1-4: 智能一键分析 + +**目标**:`/quick` 命令,30 秒内完成轻量分析,不需要 LLM 调用。 + +```python +# commands/quick.py + +def quick_analysis(trace_path: str, source_dir: str) -> str: + """快速分析:纯确定性,不调用 LLM""" + collector = PerfettoCollector(trace_path) + summary = collector.summarize() + hints = compute_hints(summary.to_json()) + + # 运行归因 (fast path only) + attributable = extract_attributable_slices(...) + results = run_attribution(attributable) # 只用 fast path + + # 拼接报告 + return format_quick_report(hints, results) +``` + +### 4.5 P1-5: ExtraHook 参数自动推断 + +**现状**:`hookExtraClasses()` 只尝试无参签名。 + +**改进**: + +```java +private static void hookExtraClasses() { + for (ExtraHook eh : HookConfigManager.getExtraHooks()) { + try { + Class clazz = Class.forName(eh.className); + for (String methodName : eh.methods) { + // 自动推断所有可能的重载签名 + for (Method m : clazz.getDeclaredMethods()) { + if (m.getName().equals(methodName)) { + Pine.hook(m, new MethodHook() { ... }); + } + } + } + } catch (Exception e) { + Log.w(TAG, "Extra hook failed: " + e.getMessage()); + } + } +} +``` + +--- + +## 5. 架构改进 + +### 5.1 配置统一 + +**现状**:三套配置系统 +- Android: `SharedPreferences` + `HookConfig` JSON +- CLI: 环境变量 + `config.py` 全局变量 +- WS 协议: `config_sync` / `config_update` 消息 + +**改进方案**:引入统一配置文件 `si.yaml` + +```yaml +# si.yaml - 项目级配置 +android: + hooks: + network_io: true + database_io: true + perfetto: + duration_ms: 10000 + buffer_size_kb: 65536 + cpu_sampling_interval_ms: 1 + +analysis: + source_dir: "./src" + frame_budget_ms: 16.67 # 0 = auto detect + jank_threshold_ms: 16.0 + +llm: + model: "deepseek-chat" + base_url: "https://api.deepseek.com" + # api_key from env SI_API_KEY + +output: + format: "markdown" # markdown | json + reports_dir: "./reports" +``` + +CLI 和 Android 端都从这份配置同步。 + +### 5.2 错误恢复增强 + +**现状**:`node_error_handler` 只捕获异常,返回错误消息。 + +**改进**: + +```python +def node_error_handler(node_name: str, retry: int = 0): + def decorator(func): + @functools.wraps(func) + def wrapper(state): + last_error = None + for attempt in range(retry + 1): + try: + return func(state) + except Exception as e: + last_error = e + if attempt < retry: + print(f" [{node_name}] retry {attempt+1}/{retry}...", flush=True) + # All retries failed + return { + "messages": [AIMessage(content=f"[{node_name}] Error: {last_error}")], + **_pass_through(state), + } + return wrapper + return decorator +``` + +### 5.3 Collector 拆分 + +**现状**:`collector/perfetto.py` 是 1340+ 行的大文件。 + +**拆分方案**: + +``` +collector/ + __init__.py + perfetto.py # PerfettoCollector 核心 (trace 加载, summarize) + queries/ + __init__.py + frame_timeline.py # collect_frame_timeline() + cpu_hotspots.py # collect_cpu_hotspots(), collect_cpu_usage() + view_slices.py # collect_view_slices(), collect_io_slices() + thread_state.py # collect_thread_state() + block_events.py # collect_block_events() + memory.py # collect_memory() (P1-2) + startup.py # StartupAnalyzer (P0-2) + input_events.py # collect_input_events() + scheduling.py # collect_sched() + trace_server.py # TraceServer + device.py # pull_trace_from_device(), adb 操作 +``` + +--- + +## 6. 贡献者指南 + +### 6.1 如何贡献 + +每个 P0/P1 任务都是独立的 PR 单元,贡献者可以: + +1. 从 issue 列表中认领任务 +2. 在对应分支上开发(`feat/p0-io-hooks`, `feat/p0-startup`, etc.) +3. 确保通过 `pytest tests/` 测试 +4. Demo App 场景验证 +5. 提交 PR + 填写 PR 模板 + +### 6.2 代码规范 + +- **Python**: 类型注解 + docstring,`debug_log()` 用于数据检查日志 +- **Java/Kotlin**: `SI$` 前缀标签,`shortenFqn()` 处理 127 字符限制 +- **SQL**: 查询放在 `collector/queries/` 独立文件中 +- **LLM prompt**: 放在 `prompts/` 目录,使用 `load_prompt()` 加载 + +### 6.3 测试要求 + +- 每个 P0 变更需要同步更新 Demo App 场景 +- Python 测试使用 synthetic trace(参考 `tests/` 现有模式) +- Android 测试:Demo App 手动验证 + 截图 + +--- + +## 附录 A: 当前 SI$ Tag 体系 + +| 前缀 | 类别 | 格式 | 示例 | +|------|------|------|------| +| `SI$` | Activity 生命周期 | `SI$Class.method` | `SI$MainActivity.onCreate` | +| `SI$` | Fragment 生命周期 | `SI$Class.method` | `SI$DetailFragment.onCreateView` | +| `SI$` | RV 管线 | `SI$RV#id#Adapter.method` | `SI$RV#recycler#DemoAdapter.onBindViewHolder` | +| `SI$` | Layout 膨胀 | `SI$inflate#layout#parent` | `SI$inflate#item_complex#RecyclerView` | +| `SI$` | View 遍历 | `SI$view#Class.method` | `SI$view#HeavyDrawView.draw` | +| `SI$` | Handler | `SI$handler#callbackClass` | `SI$handler#Worker$1` | +| `SI$block#` | 卡顿 | `SI$block#class#NNms` | `SI$block#DemoAdapter$1.run#250ms` | +| `SI$touch#` | 触摸事件 | `SI$touch#activity#ACTION` | `SI$touch#MainActivity#DOWN` | +| `SI$net#` | 网络 IO | `SI$net#class.method` | `SI$net#RealCall.execute` | +| `SI$db#` | 数据库 IO | `SI$db#class.method#table` | `SI$db#DatabaseHelper.query#users` | +| `SI$img#` | 图片加载 | `SI$img#class.method` | `SI$img#RequestBuilder.into` | +| `SI$inflate#` | 布局膨胀 | `SI$inflate#name#parent` | `SI$inflate#item_complex#RecyclerView` | + +## 附录 B: LangGraph 管线流程 + +``` +用户输入 → orchestrator (LLM 路由) + ├─ full_analysis → collector → analyzer → attributor → reporter → END + ├─ android → android_expert → [analyzer] → END + ├─ analyze → perf_analyzer → END + ├─ explorer → explorer → END + └─ end → fallback → END + +Slash Commands: + /trace [dur] [pkg] → collector → analyzer → END + /full [pkg] → collector → analyzer → attributor → reporter → END + /frame ts=X dur=Y → frame_analyzer (直接) + /open [path] → bridge_server + browser + /config → 查看配置 + /hooks → 查看 hook 状态 +``` + +## 附录 C: WebSocket 协议 + +| 方向 | 消息类型 | 用途 | +|------|----------|------| +| App → CLI | `config_sync` | App 连接时发送当前配置 | +| CLI → App | `config_update` | CLI 推送配置变更 | +| CLI → App | `start_trace` | 请求 App 确认 hook 就绪 | +| App → CLI | `ack` | 确认 start_trace | +| CLI → App | `get_block_events` | 请求缓存的 block 事件 | +| App → CLI | `block_events` | 返回 block 事件 JSON | +| Bridge → CLI | `frame_selected` | Perfetto UI 插件选帧事件 | +| CLI → Bridge | `analysis_result` | 帧分析结果 | +| CLI → Bridge | `analysis_progress` | 分析进度 | + +## 4. P2-6 详细设计:源码归因增强(借鉴 claude-context) + +### 背景 +参考 [zilliztech/claude-context](https://github.com/zilliztech/claude-context) 的设计,提升SI项目源码归因的搜索效率和精度。 + +### 当前问题 +1. **无文件过滤**:搜索全目录,包含build/vendor/node_modules等无关文件 +2. **按文件搜索**:搜索结果粒度为整个文件,无法精确到函数/类级别 +3. **大项目慢**:grep在100+文件项目中效率低 +4. **无模糊匹配**:方法名不完全匹配时(如混淆代码)搜索失败 + +### 改进方案 + +#### 1. .gitignore 文件过滤 +```python +# commands/attribution.py 或新建 source_search.py +import pathspec + +def load_gitignore(source_dir: str) -> pathspec.PathSpec: + gitignore_path = os.path.join(source_dir, '.gitignore') + if os.path.exists(gitignore_path): + with open(gitignore_path) as f: + return pathspec.PathSpec.from_lines('gitwildmatch', f) + return pathspec.PathSpec.from_lines('gitwildmatch', []) + +def should_ignore(path: str, spec: pathspec.PathSpec) -> bool: + return spec.match_file(path) +``` + +#### 2. 函数级分块 +```python +# 基于 AST 或正则的轻量分块 +import re + +def chunk_by_function(source: str) -> list[dict]: + \"\"\"将源码按函数/类切分为块,每块带行号范围。\"\"\" + chunks = [] + # 匹配函数/类定义 + pattern = re.compile(r'^(class |def |public |private |protected |@\\w+).*$', re.MULTILINE) + ... + return chunks +``` + +#### 3. 轻量BM25索引(可选) +```python +# 对大项目(>100文件)预建索引 +from rank_bm25 import BM25Okapi + +class SourceIndex: + def __init__(self, source_dir: str): + self.chunks = self._index_source(source_dir) + self.bm25 = BM25Okapi([c['tokens'] for c in self.chunks]) + + def search(self, query: str, top_k: int = 5) -> list[dict]: + scores = self.bm25.get_scores(query.split()) + return sorted(zip(self.chunks, scores), key=lambda x: -x[1])[:top_k] +``` + +### 变更文件 +- `src/smartinspector/commands/attribution.py` — 添加文件过滤和函数级分块 +- `src/smartinspector/agents/attributor.py` — 使用增强搜索替代现有grep +- `requirements.txt` — 可选:添加 `pathspec`, `rank_bm25` + +### 不引入的 +- ❌ 向量数据库(Milvus/Zilliz)— 太重,CLI工具不应依赖外部服务 +- ❌ Embedding模型 — 增加延迟和成本,方法名匹配场景不需要语义搜索 diff --git a/img/perfetto_ui.png b/img/perfetto_ui.png new file mode 100644 index 0000000..3659dce Binary files /dev/null and b/img/perfetto_ui.png differ diff --git a/perfetto-plugin/build.sh b/perfetto-plugin/build.sh new file mode 100755 index 0000000..67a3689 --- /dev/null +++ b/perfetto-plugin/build.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# build.sh — Build self-hosted Perfetto UI with SI Bridge plugin +# +# Usage: +# ./perfetto-plugin/build.sh # Full build +# ./perfetto-plugin/build.sh --skip-clone # Skip git clone (already cloned) +# +# Prerequisites: +# - Node.js >= 18 +# - npm +# - git +# +# Note: Automatically removes Android NDK from PATH to prevent strip(1) +# conflicts on macOS. If you need proxy, set http_proxy/https_proxy first. +# +# Output: +# perfetto-build/ui/out/dist/ — static files to serve via bridge_server.py + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PERFETTO_DIR="$PROJECT_ROOT/perfetto-build" +PLUGIN_SRC="$SCRIPT_DIR/com.smartinspector.Bridge" +OUTPUT_DIR="$PERFETTO_DIR/ui/out/dist" + +SKIP_CLONE=false +if [[ "${1:-}" == "--skip-clone" ]]; then + SKIP_CLONE=true +fi + +echo "=== SI Bridge Perfetto UI Builder ===" +echo "" + +# ── Step 1: Clone Perfetto ─────────────────────────────────────── +if [[ "$SKIP_CLONE" == false ]]; then + if [[ -d "$PERFETTO_DIR/.git" ]]; then + echo "[1/5] Updating Perfetto repo..." + cd "$PERFETTO_DIR" + git pull --ff-only origin master || { + echo "WARNING: git pull failed. Using existing checkout." + } + else + echo "[1/5] Cloning Perfetto repo (shallow)..." + git clone --depth 1 https://github.com/google/perfetto.git "$PERFETTO_DIR" + fi +else + echo "[1/5] Skipping clone (--skip-clone)" + if [[ ! -d "$PERFETTO_DIR/.git" ]]; then + echo "ERROR: perfetto-build/ not found. Run without --skip-clone first." + exit 1 + fi +fi + +cd "$PERFETTO_DIR" + +# ── Step 2: Copy plugin ───────────────────────────────────────── +echo "[2/5] Copying SI Bridge plugin..." +PLUGIN_DIR="ui/src/plugins/com.smartinspector.Bridge" +mkdir -p "$PLUGIN_DIR" +cp "$PLUGIN_SRC/index.ts" "$PLUGIN_DIR/index.ts" +echo " Plugin copied to $PLUGIN_DIR" + +# ── Step 3: Register plugin in default_plugins.ts ─────────────── +echo "[3/5] Registering plugin in default_plugins..." +DEFAULT_PLUGINS="ui/src/core/embedder/default_plugins.ts" + +if ! grep -q "com.smartinspector.Bridge" "$DEFAULT_PLUGINS"; then + # The file is a simple array of plugin ID strings: + # export const defaultPlugins = [ + # 'com.android.AndroidAnr', + # ... + # 'org.kernel.Wattson', + # ]; + # We add our plugin ID before the closing '];' + python3 -c " +import re +with open('$DEFAULT_PLUGINS', 'r') as f: + content = f.read() +# Insert before the closing '];' +content = content.replace( + \"'org.kernel.Wattson',\n];\", + \"'org.kernel.Wattson',\n 'com.smartinspector.Bridge',\n];\", +) +with open('$DEFAULT_PLUGINS', 'w') as f: + f.write(content) +" + echo " Plugin ID added to defaultPlugins array." +else + echo " Plugin already registered." +fi + +# ── Step 4: Build (includes npm install + TypeScript compile + WASM) ── +echo "[4/5] Building Perfetto UI (includes dependency install, this takes a few minutes)..." + +# Remove Android NDK strip from PATH — it overrides macOS /usr/bin/strip +# and cannot handle Mach-O arm64 files, breaking npm postinstall scripts. +export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "android.*strip\|ndk.*bin" | tr '\n' ':' | sed 's/:$//') + +ui/build + +echo "[5/5] Verifying output..." +if [[ -f "$OUTPUT_DIR/index.html" ]]; then + echo " OK: $OUTPUT_DIR/index.html found" +else + echo " WARNING: $OUTPUT_DIR/index.html not found. Build may have failed." +fi + +echo "" +echo "=== Build complete! ===" +echo "Static files: $OUTPUT_DIR" +echo "" +echo "To serve:" +echo " python3 -m http.server 8080 --directory $OUTPUT_DIR" +echo "" +echo "Or use the SI Agent bridge server:" +echo " /open (in SmartInspector CLI)" diff --git a/perfetto-plugin/com.smartinspector.Bridge/index.ts b/perfetto-plugin/com.smartinspector.Bridge/index.ts new file mode 100644 index 0000000..279f29f --- /dev/null +++ b/perfetto-plugin/com.smartinspector.Bridge/index.ts @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2026 SmartInspector Contributors + * + * SI Bridge Plugin for Perfetto UI. + * + * Provides interactive frame-level analysis by connecting the Perfetto UI + * to a local SI Agent instance via WebSocket. + * + * Features: + * - Area selection tab: select a time range → click "Analyze" → get results + * - Command + hotkey: Ctrl+Shift+A to analyze current selection + * - Result panel: displays Markdown analysis from SI Agent + */ + +import m from 'mithril'; +import {PerfettoPlugin} from '../../public/plugin'; +import {Trace} from '../../public/trace'; +import {AreaSelection, ContentWithLoadingFlag} from '../../public/selection'; + +// ── State ────────────────────────────────────────────────────────── + +interface AnalysisState { + status: 'idle' | 'analyzing' | 'done' | 'error'; + ts: number; + dur: number; + result: string; + error: string; + progressStep: string; + progressDetail: string; + progressLog: string[]; +} + +const state: AnalysisState = { + status: 'idle', + ts: 0, + dur: 0, + result: '', + error: '', + progressStep: '', + progressDetail: '', + progressLog: [], +}; + +let ws: WebSocket | null = null; +let reconnectTimer: ReturnType | null = null; +const BRIDGE_URL = 'ws://127.0.0.1:9877/bridge'; + +// ── WebSocket management ────────────────────────────────────────── + +function connectWS(): void { + if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { + return; + } + // Clear any pending reconnect timer + if (reconnectTimer !== null) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + try { + ws = new WebSocket(BRIDGE_URL); + ws.onopen = () => { + console.log('[SI Bridge] Connected to SI Agent'); + }; + ws.onmessage = (ev) => { + try { + const msg = JSON.parse(ev.data); + if (msg.type === 'analysis_result') { + state.status = 'done'; + state.result = msg.payload.analysis || msg.payload || ''; + state.progressStep = ''; + state.progressDetail = ''; + state.progressLog = []; + m.redraw(); + } else if (msg.type === 'analysis_error') { + state.status = 'error'; + state.error = msg.payload.error || 'Unknown error'; + state.progressStep = ''; + state.progressDetail = ''; + state.progressLog = []; + m.redraw(); + } else if (msg.type === 'analysis_progress') { + state.progressStep = msg.payload.step || ''; + state.progressDetail = msg.payload.detail || ''; + const line = msg.payload.detail || msg.payload.step || ''; + if (line) { + state.progressLog.push(line); + } + m.redraw(); + } + } catch { + // ignore non-JSON + } + }; + ws.onclose = () => { + console.log('[SI Bridge] Disconnected, reconnecting in 3s...'); + ws = null; + reconnectTimer = setTimeout(connectWS, 3000); + }; + ws.onerror = () => { + // onclose will fire after this + }; + } catch { + ws = null; + } +} + +function sendAnalysis(ts: number, dur: number): void { + if (!ws || ws.readyState !== WebSocket.OPEN) { + state.status = 'error'; + state.error = 'SI Agent not connected. Is /open running?'; + m.redraw(); + return; + } + + state.status = 'analyzing'; + state.ts = ts; + state.dur = dur; + state.result = ''; + state.error = ''; + state.progressStep = 'started'; + state.progressDetail = 'Sending to SI Agent...'; + state.progressLog = ['Sending to SI Agent...']; + m.redraw(); + + ws.send(JSON.stringify({ + type: 'frame_selected', + payload: {ts, dur}, + })); +} + +// ── Formatting helpers ──────────────────────────────────────────── + +function formatNs(ns: number): string { + if (ns >= 1_000_000) return `${(ns / 1_000_000).toFixed(2)}ms`; + if (ns >= 1_000) return `${(ns / 1_000).toFixed(2)}us`; + return `${ns}ns`; +} + +// ── Area Selection Tab ──────────────────────────────────────────── + +function renderAreaSelectionTab(selection: AreaSelection): ContentWithLoadingFlag | undefined { + const start = Number(selection.start); + const end = Number(selection.end); + const dur = end - start; + + return { + isLoading: false, + content: m('.si-bridge-panel', { + style: 'padding: 12px; font-family: monospace; font-size: 13px;', + }, [ + m('h4', { + style: 'margin: 0 0 8px 0; color: #e0e0e0;', + }, 'SI Frame Analysis'), + + m('.selection-info', { + style: 'color: #aaa; margin-bottom: 8px;', + }, `Selected: ${formatNs(start)} — ${formatNs(end)} (${formatNs(dur)})`), + + m('button', { + style: [ + 'background: #1a73e8', + 'color: white', + 'border: none', + 'padding: 6px 16px', + 'border-radius: 4px', + 'cursor: pointer', + 'font-size: 13px', + state.status === 'analyzing' ? 'opacity: 0.6; cursor: not-allowed;' : '', + ].join(';'), + disabled: state.status === 'analyzing', + onclick: () => sendAnalysis(start, dur), + }, state.status === 'analyzing' ? 'Analyzing...' : 'Analyze with SI Agent'), + + // Progress log + state.status === 'analyzing' && state.progressLog.length > 0 + ? m('.si-progress', { + style: [ + 'margin-top: 8px', + 'padding: 6px 8px', + 'background: #1a1a2e', + 'border: 1px solid #333', + 'border-radius: 4px', + 'color: #4fc3f7', + 'font-size: 12px', + 'white-space: pre-wrap', + 'max-height: 200px', + 'overflow-y: auto', + 'line-height: 1.6', + ].join(';'), + oncreate: (vnode: m.VnodeDOM) => { + (vnode.dom as HTMLElement).scrollTop = (vnode.dom as HTMLElement).scrollHeight; + }, + onupdate: (vnode: m.VnodeDOM) => { + (vnode.dom as HTMLElement).scrollTop = (vnode.dom as HTMLElement).scrollHeight; + }, + }, state.progressLog.join('\n')) + : undefined, + + // Result area + state.status === 'error' + ? m('.si-error', { + style: 'margin-top: 12px; color: #f44336; white-space: pre-wrap;', + }, state.error) + : undefined, + + state.status === 'done' && state.result + ? m('.si-result', { + style: [ + 'margin-top: 12px', + 'padding: 8px', + 'background: #1e1e1e', + 'border: 1px solid #333', + 'border-radius: 4px', + 'color: #ddd', + 'white-space: pre-wrap', + 'max-height: 400px', + 'overflow-y: auto', + 'line-height: 1.5', + ].join(';'), + }, state.result) + : undefined, + + // Connection status indicator + m('.si-status', { + style: 'margin-top: 8px; font-size: 11px; color: #666;', + }, ws && ws.readyState === WebSocket.OPEN + ? '\u25cf Connected to SI Agent' + : '\u25cb SI Agent not connected'), + ]), + }; +} + +// ── Plugin ──────────────────────────────────────────────────────── + +export default class SIBridgePlugin implements PerfettoPlugin { + static readonly id = 'com.smartinspector.Bridge'; + + async onTraceLoad(trace: Trace): Promise { + // Connect to SI Agent + connectWS(); + + // Register area selection tab + trace.selection.registerAreaSelectionTab({ + id: 'si_frame_analysis', + name: 'SI Frame Analysis', + render: (selection) => renderAreaSelectionTab(selection), + }); + + // Register keyboard command + trace.commands.registerCommand({ + id: 'com.smartinspector.Bridge#analyzeSelection', + name: 'SI Agent: Analyze Selected Area', + callback: () => { + const sel = trace.selection.selection; + if (sel.kind === 'area') { + const start = Number(sel.start); + const end = Number(sel.end); + if (start && end && end > start) { + sendAnalysis(start, end - start); + } else { + alert('Invalid area selection'); + } + } else { + alert('Please select an area on the timeline first (drag to select)'); + } + }, + }); + + // Register a sidebar menu item + trace.sidebar.addMenuItem({ + section: 'current_trace', + text: 'SI Agent Bridge', + action: () => { + // Show a persistent tab with connection info + const uri = 'com.smartinspector.Bridge#Info'; + trace.tabs.registerTab({ + uri, + content: { + render(): m.Children { + return m('.si-bridge-info', { + style: 'padding: 16px; font-family: monospace;', + }, [ + m('h3', 'SI Agent Bridge'), + m('p', 'Connect your Perfetto UI to SI Agent for interactive frame analysis.'), + m('p', {style: 'color: #aaa;'}, 'How to use:'), + m('ol', [ + m('li', 'Drag to select a time range on the timeline'), + m('li', 'Click "SI Frame Analysis" tab in the details panel'), + m('li', 'Click "Analyze with SI Agent"'), + ]), + m('p', {style: 'color: #666; margin-top: 16px;'}, [ + 'Bridge URL: ', + m('code', BRIDGE_URL), + ]), + m('p', {style: 'color: #666;'}, [ + 'Status: ', + m('span', { + style: `color: ${ws && ws.readyState === WebSocket.OPEN ? '#4caf50' : '#f44336'}`, + }, ws && ws.readyState === WebSocket.OPEN ? 'Connected' : 'Disconnected'), + ]), + ]); + }, + getTitle(): string { + return 'SI Bridge'; + }, + }, + }); + trace.tabs.showTab(uri); + }, + }); + } +} diff --git a/platform/android/app/build.gradle b/platform/android/app/build.gradle index 0590473..3548142 100644 --- a/platform/android/app/build.gradle +++ b/platform/android/app/build.gradle @@ -1,16 +1,17 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.compose' } android { namespace 'com.smartinspector.hook' - compileSdk 35 + compileSdk 36 defaultConfig { applicationId "com.smartinspector.hook" minSdk 24 - targetSdk 35 + targetSdk 36 versionCode 1 versionName "1.0" } @@ -29,6 +30,10 @@ android { kotlinOptions { jvmTarget = '11' } + + buildFeatures { + compose true + } } dependencies { @@ -37,6 +42,16 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'com.google.android.material:material:1.11.0' - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22' + implementation 'androidx.core:core-ktx:1.18.0' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.1.20' + + // Compose + def composeBom = platform('androidx.compose:compose-bom:2025.04.01') + implementation composeBom + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.foundation:foundation' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.activity:activity-compose:1.10.1' + debugImplementation 'androidx.compose.ui:ui-tooling' } diff --git a/platform/android/app/src/main/AndroidManifest.xml b/platform/android/app/src/main/AndroidManifest.xml index ed5471f..8f61067 100644 --- a/platform/android/app/src/main/AndroidManifest.xml +++ b/platform/android/app/src/main/AndroidManifest.xml @@ -19,5 +19,11 @@ + + + diff --git a/platform/android/app/src/main/java/com/smartinspector/hook/MainActivity.java b/platform/android/app/src/main/java/com/smartinspector/hook/MainActivity.java index 5561b09..aaee505 100644 --- a/platform/android/app/src/main/java/com/smartinspector/hook/MainActivity.java +++ b/platform/android/app/src/main/java/com/smartinspector/hook/MainActivity.java @@ -1,10 +1,11 @@ package com.smartinspector.hook; -import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.Log; +import android.widget.Button; import android.widget.FrameLayout; import androidx.fragment.app.FragmentActivity; @@ -14,6 +15,7 @@ import com.smartinspector.hook.adapter.DemoAdapter; import com.smartinspector.hook.model.Item; import com.smartinspector.hook.repository.DataRepository; +import com.smartinspector.hook.ui.ComposeDemoActivity; import com.smartinspector.hook.ui.DetailFragment; import com.smartinspector.hook.worker.CpuBurnWorker; @@ -35,6 +37,12 @@ protected void onCreate(Bundle savedInstanceState) { rv = findViewById(R.id.recycler_view); rv.setLayoutManager(new LinearLayoutManager(this)); + Button composeBtn = findViewById(R.id.btn_compose_demo); + composeBtn.setOnClickListener(v -> { + Intent intent = new Intent(this, ComposeDemoActivity.class); + startActivity(intent); + }); + cpuBurner.start(4); cpuBurner.startMainThreadWork(handler); diff --git a/platform/android/app/src/main/java/com/smartinspector/hook/ui/ComposeDemoActivity.kt b/platform/android/app/src/main/java/com/smartinspector/hook/ui/ComposeDemoActivity.kt new file mode 100644 index 0000000..3a67923 --- /dev/null +++ b/platform/android/app/src/main/java/com/smartinspector/hook/ui/ComposeDemoActivity.kt @@ -0,0 +1,443 @@ +package com.smartinspector.hook.ui + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlin.math.sin + +/** + * Compose demo page for testing TraceHook's Compose recomposition tracking. + * + * Contains intentional performance anti-patterns: + * - Unnecessary recomposition via unstable lambdas + * - Heavy Canvas drawing in composable + * - LazyColumn with expensive item composables + * - State-driven animations triggering recomposition + * - Eager computation in composable body + */ +class ComposeDemoActivity : ComponentActivity() { + + private val handler = Handler(Looper.getMainLooper()) + private var destroyed = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + ComposeDemoScreen() + } + } + } + + // Periodic state change to trigger recomposition + startPeriodicRecomposition() + } + + override fun onDestroy() { + super.onDestroy() + destroyed = true + handler.removeCallbacksAndMessages(null) + } + + private fun startPeriodicRecomposition() { + handler.postDelayed(object : Runnable { + override fun run() { + if (destroyed) return + // Trigger a global recomposition via tick counter + recompositionTick++ + handler.postDelayed(this, 200) + } + }, 200) + } + + companion object { + @Volatile + var recompositionTick: Int = 0 + } +} + +// ═══════════════════════════════════════════════════════════ +// Main screen composable +// ═══════════════════════════════════════════════════════════ + +@Composable +fun ComposeDemoScreen() { + var counter by remember { mutableIntStateOf(0) } + var showHeavyList by remember { mutableStateOf(false) } + var animationProgress by remember { mutableStateOf(0f) } + + // Read tick to force recomposition from external source + val tick = ComposeDemoActivity.recompositionTick + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + // Header + Text( + text = "Compose Demo (tick=$tick)", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Testing ComposeHook recomposition tracking", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Counter section — triggers recomposition on each click + CounterSection(counter = counter, onIncrement = { counter++ }) + + Spacer(modifier = Modifier.height(16.dp)) + + // Animation section — continuous recomposition + AnimatedSection(onProgressChange = { animationProgress = it }) + + Spacer(modifier = Modifier.height(16.dp)) + + // Heavy Canvas drawing + HeavyCanvasSection(progress = animationProgress) + + Spacer(modifier = Modifier.height(16.dp)) + + // Toggle to show expensive LazyColumn + Button(onClick = { showHeavyList = !showHeavyList }) { + Text(if (showHeavyList) "Hide Heavy List" else "Show Heavy List") + } + + if (showHeavyList) { + Spacer(modifier = Modifier.height(8.dp)) + HeavyLazyColumn(itemCount = 50) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Unstable lambda pattern — causes unnecessary recomposition + UnstableLambdaSection(tick = tick) + } +} + +// ═══════════════════════════════════════════════════════════ +// Counter — simple state-driven recomposition +// ═══════════════════════════════════════════════════════════ + +@Composable +fun CounterSection(counter: Int, onIncrement: () -> Unit) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Counter: $counter", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = onIncrement) { + Text("Increment") + } + } + } +} + +// ═══════════════════════════════════════════════════════════ +// Animated section — continuous recomposition via animateFloatAsState +// ═══════════════════════════════════════════════════════════ + +@Composable +fun AnimatedSection(onProgressChange: (Float) -> Unit) { + var target by remember { mutableStateOf(0f) } + val progress by animateFloatAsState( + targetValue = target, + animationSpec = tween(durationMillis = 1000, easing = LinearEasing), + label = "progress" + ) + + onProgressChange(progress) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Animation", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + + // Progress bar + Box( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(4.dp)) + ) { + Box( + modifier = Modifier + .fillMaxWidth(progress) + .height(8.dp) + .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(4.dp)) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button(onClick = { + target = if (target >= 1f) 0f else 1f + }) { + Text(if (target >= 1f) "Reset Animation" else "Start Animation") + } + } + } +} + +// ═══════════════════════════════════════════════════════════ +// Heavy Canvas — expensive drawing in Compose +// ═══════════════════════════════════════════════════════════ + +@Composable +fun HeavyCanvasSection(progress: Float) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Heavy Canvas Drawing", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + + Canvas(modifier = Modifier.fillMaxWidth().height(200.dp)) { + val w = size.width + val h = size.height + + // Concentric circles + for (i in 0 until 20) { + val radius = (w.coerceAtMost(h) / 2f) - i * 8f + if (radius <= 0) break + drawCircle( + color = Color.White.copy(alpha = 0.3f + (i * 0.02f)), + radius = radius, + center = center + ) + } + + // Wavy lines + for (line in 0 until 5) { + val path = Path() + path.moveTo(0f, h / 2f) + for (x in 0 until w.toInt() step 4) { + val y = h / 2f + + sin((x + line * 50 + progress * 360) * 0.02f) * h * 0.3f + path.lineTo(x.toFloat(), y) + } + drawPath( + path = path, + color = Color.White.copy(alpha = 0.5f), + style = Stroke(width = 2f) + ) + } + + // Gradient overlay + drawRect( + brush = Brush.linearGradient( + colors = listOf( + Color(0xFF6200EE).copy(alpha = 0.3f), + Color(0xFF03DAC5).copy(alpha = 0.3f) + ), + start = Offset.Zero, + end = Offset(w, h) + ) + ) + } + } + } +} + +// ═══════════════════════════════════════════════════════════ +// Heavy LazyColumn — expensive item composables +// ═══════════════════════════════════════════════════════════ + +@Composable +fun HeavyLazyColumn(itemCount: Int) { + val items = remember(itemCount) { + List(itemCount) { index -> + "Item #$index" to "Category ${(index % 5)} — payload data for stress testing" + } + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + ) { + items(items, key = { it.first }) { (title, subtitle) -> + HeavyListItem(title = title, subtitle = subtitle) + } + } +} + +@Composable +fun HeavyListItem(title: String, subtitle: String) { + // Simulate expensive computation during composition + val computedValue = remember(title) { + var sum = 0.0 + for (i in 0 until 10_000) { + sum += sin(i.toDouble()) * Math.sqrt(i.toDouble()) + } + sum + } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Colored box — simulates image placeholder + Box( + modifier = Modifier + .size(48.dp) + .background( + Color( + red = (title.hashCode() % 128 + 128), + green = (title.hashCode() * 37 % 128 + 128), + blue = (title.hashCode() * 71 % 128 + 128), + alpha = 255 + ), + RoundedCornerShape(8.dp) + ) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text(title, fontWeight = FontWeight.Bold, fontSize = 14.sp) + Text( + subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + "computed=$computedValue", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline + ) + } + } + } +} + +// ═══════════════════════════════════════════════════════════ +// Unstable lambda — causes unnecessary recomposition of children +// ═══════════════════════════════════════════════════════════ + +@Composable +fun UnstableLambdaSection(tick: Int) { + // This lambda is recreated on every recomposition (unstable), + // causing all children that receive it to recompose unnecessarily. + val onClick: () -> Unit = { + // no-op — but the lambda reference changes every time + } + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Unstable Lambda Section", style = MaterialTheme.typography.titleMedium) + Text( + "tick=$tick — each tick causes full recomposition", + style = MaterialTheme.typography.bodySmall + ) + Spacer(modifier = Modifier.height(8.dp)) + + // 10 items each receiving unstable lambda — all recompose every tick + for (i in 0 until 10) { + UnstableItem(index = i, onClick = onClick) + } + } + } +} + +@Composable +fun UnstableItem(index: Int, onClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("Unstable Item #$index") + Text( + "→ recomposes", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.labelSmall + ) + } +} diff --git a/platform/android/app/src/main/res/layout/activity_main.xml b/platform/android/app/src/main/res/layout/activity_main.xml index f61ae5e..ba6772b 100644 --- a/platform/android/app/src/main/res/layout/activity_main.xml +++ b/platform/android/app/src/main/res/layout/activity_main.xml @@ -3,10 +3,25 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_height="match_parent" + android:orientation="vertical"> + +