diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index 461cbdf75..a605a35c3 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -24,6 +24,7 @@ and the 6-trace scene regression gate. | MCP, memory, report, provider, session, or agent runtime | `cd backend && npm run test:scene-trace-regression` | | Skill YAML | `cd backend && npm run validate:skills` plus scene trace regression | | Strategy/template Markdown | `cd backend && npm run validate:strategies` plus scene trace regression | +| Trace corpus, Skill/Strategy coverage, or generator | `npm run trace:regression`; also run the focused Node corpus tests for tooling changes | | Frontend generated types | `cd backend && npm run generate:frontend-types` plus relevant tests | | AI plugin UI | Browser verification in `start-dev.sh`, relevant `perfetto/ui` tests/typecheck, then `./scripts/update-frontend.sh` | | Perfetto upstream sync, trace processor pin, SQL/stdlib index, or committed UI prebuild | Follow `.claude/rules/perfetto-sync.md`; normally `git diff --check`, `npm run check:frontend-prebuild`, `npm --prefix backend run cli:e2e`, scene trace regression, submodule remote reachability, and Skill/Strategy validation when those files changed | @@ -97,6 +98,8 @@ The regression uses 6 canonical traces: | Flutter TextureView | `Scroll-Flutter-327-TextureView.pftrace` | | Flutter SurfaceView | `Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace` | +The aliases above resolve through `Trace/catalog.json`; maintained source must not add paths to the retired flat fixture directory. Full release/corpus verification is `npm run trace:regression`. Its report distinguishes positive semantic execution from explicit graceful-empty, unavailable-prerequisite, and definition-only contracts; inventory assignment alone is not a semantic pass. + ## Focused Unit Tests Useful focused suites: @@ -128,7 +131,7 @@ Startup: ```bash cd backend npx tsx src/scripts/verifyAgentSseScrolling.ts \ - --trace ../test-traces/lacunh_heavy.pftrace \ + --trace ../Trace/real/android-startup-heavy/trace.pftrace \ --query "分析启动性能" \ --output test-output/e2e-startup.json \ --keep-session @@ -184,13 +187,13 @@ rendering pipelines differ: ```bash cd backend npx tsx src/scripts/verifyAgentSseScrolling.ts \ - --trace "../test-traces/Scroll-Flutter-327-TextureView.pftrace" \ + --trace "../Trace/real/flutter-scroll-texture-view/trace.pftrace" \ --query "分析 Flutter 滑动性能" \ --output test-output/e2e-flutter-textureview.json \ --keep-session npx tsx src/scripts/verifyAgentSseScrolling.ts \ - --trace "../test-traces/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace" \ + --trace "../Trace/real/flutter-scroll-surface-view/trace.pftrace" \ --query "分析 Flutter 滑动性能" \ --output test-output/e2e-flutter-surfaceview.json \ --keep-session @@ -202,13 +205,13 @@ Fast/full mode: cd backend npx tsx src/scripts/verifyAgentSseScrolling.ts \ --mode fast \ - --trace ../test-traces/scroll-demo-customer-scroll.pftrace \ + --trace ../Trace/real/android-scroll-customer/trace.pftrace \ --query "这个 trace 的应用包名和主要进程是什么?" \ --output test-output/e2e-fast.json npx tsx src/scripts/verifyAgentSseScrolling.ts \ --mode full \ - --trace ../test-traces/scroll-demo-customer-scroll.pftrace \ + --trace ../Trace/real/android-scroll-customer/trace.pftrace \ --query "分析滑动性能" \ --output test-output/e2e-full.json ``` diff --git a/.dockerignore b/.dockerignore index 4d7bcfbb0..5d1bad4c0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -32,7 +32,7 @@ backend/logs/ *.log # Test data & outputs -test-traces/ +Trace/ backend/test-output/ coverage/ diff --git a/.github/workflows/backend-agent-regression-gate.yml b/.github/workflows/backend-agent-regression-gate.yml index 6c149789e..52e743ee9 100644 --- a/.github/workflows/backend-agent-regression-gate.yml +++ b/.github/workflows/backend-agent-regression-gate.yml @@ -17,7 +17,7 @@ on: - ".dockerignore" - "docker-compose*.yml" - "scripts/trace-processor-pin.env" - - "test-traces/**" + - "Trace/**" - ".github/dependabot.yml" - ".github/workflows/**" push: @@ -37,7 +37,7 @@ on: - ".dockerignore" - "docker-compose*.yml" - "scripts/trace-processor-pin.env" - - "test-traces/**" + - "Trace/**" - ".github/dependabot.yml" - ".github/workflows/**" diff --git a/.gitignore b/.gitignore index 3bd0ebdbf..88cc4934f 100644 --- a/.gitignore +++ b/.gitignore @@ -178,3 +178,8 @@ providers.json # Local E2E evidence (may contain Trace-derived data and machine paths) /output/ +.gstack/ + +# Trace corpus private staging and materialized constructed outputs +/Trace/real/.private/ +/Trace/.generated/ diff --git a/README.md b/README.md index f5eccfdd5..d33856334 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,8 @@ smp report --open # Record an Android trace from a connected device, then analyze it. smp capture presets +smp capture suggest "Analyze Camera open-to-first-preview latency" --app com.example.camera +smp capture config --preset camera --app com.example.camera --duration 20 smp capture android --preset startup --app com.example.app --duration 10 --out launch.perfetto-trace smp capture android --preset cpu --app '*' --duration 30 --categories dalvikviktime my_custom_tag --out cpu-custom.perfetto-trace smp capture android --preset power --app com.example.app --duration 60 --out power.perfetto-trace @@ -318,6 +320,14 @@ smp capture android --config ~/tools/perfetto_shell/perfetto.config --out ~/tool smp repl ``` +The `camera` preset collects Camera/HAL/vendor atrace candidates, Binder and +scheduler context, FrameTimeline, and DMA-BUF or legacy ION ftrace events. The +atrace candidates and memory ftrace events are optional; availability depends +on the Android release, device/vendor implementation, and kernel support. A +trace may still lack portable Camera open, request/result, buffer, or preview +presentation anchors; SmartPerfetto reports that evidence gap instead of +fabricating an open-to-first-frame number. + The npm CLI package is the supported standalone terminal product. It does not start or bundle the Web UI launcher; use Docker or a GitHub portable package when you need the browser experience. The first analysis uses the bundled pinned `trace_processor_shell` binary when available, and can download the pinned binary automatically on unsupported targets. Android capture itself never downloads tools at runtime: `adb` is resolved from `ADB_PATH`, an approved bundled slot, then `PATH`; pre-Android Q or `--sideload` tracebox capture requires an approved bundled `tracebox` or `--tracebox /path/to/tracebox`. If your network cannot reach Google's artifact bucket, set `TRACE_PROCESSOR_PATH=/path/to/trace_processor_shell` to use a local binary, or set `TRACE_PROCESSOR_DOWNLOAD_BASE` / `TRACE_PROCESSOR_DOWNLOAD_URL` to a trusted mirror; downloaded binaries are still checked against the pinned SHA256. `smartperfetto` remains available as the long command name; source checkout scripts are only for maintainers debugging the CLI. See [CLI Reference](docs/reference/cli.en.md) for all commands, capture presets, REPL slash commands, storage layout, and resume behavior. ## API Integration diff --git a/README.zh-CN.md b/README.zh-CN.md index 5d36aa299..877bc84f6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -304,6 +304,8 @@ smp report --open # 从已连接 Android 设备抓 trace,并可直接分析。 smp capture presets +smp capture suggest "分析 Camera 打开到首帧预览延迟" --app com.example.camera +smp capture config --preset camera --app com.example.camera --duration 20 smp capture android --preset startup --app com.example.app --duration 10 --out launch.perfetto-trace smp capture android --preset cpu --app '*' --duration 30 --categories dalvikviktime my_custom_tag --out cpu-custom.perfetto-trace smp capture android --config ~/tools/perfetto_shell/perfetto.config --out ~/tools/perfetto_shell/trace/dut-game-launch.ptrace --analyze --query "分析应用启动" @@ -312,6 +314,13 @@ smp capture android --config ~/tools/perfetto_shell/perfetto.config --out ~/tool smp repl ``` +`camera` 预设会采集设备可能提供的 Camera/HAL/厂商 atrace 候选、Binder 与 +scheduler 上下文、FrameTimeline,以及 DMA-BUF 或旧版 ION ftrace 事件。atrace +候选和这些内存 ftrace 事件均为可选证据,具体是否可用取决于 Android 版本、 +设备/厂商实现与内核支持。trace 仍可能缺少可移植的 Camera open、request/result、 +buffer 或预览 presentation 锚点;遇到这种证据缺口时,SmartPerfetto 会明确报告 +缺失,而不会编造“打开到首帧”耗时。 + npm CLI 包是正式独立终端产品,不启动也不包含 Web UI launcher;需要浏览器体验时使用 Docker 或 GitHub 免安装包。第一次分析时,CLI 会优先使用包内固定版本 `trace_processor_shell`;当前平台没有内置 binary 时会自动下载固定版本。Android 抓 trace 本身不会现场下载工具:`adb` 按 `ADB_PATH`、已批准的包内 slot、`PATH` 顺序解析;Android Q 之前或显式 `--sideload` 的 tracebox 抓取需要已批准的包内 `tracebox`,或通过 `--tracebox /path/to/tracebox` 指定。若网络无法访问 Google artifact bucket,可以设置 `TRACE_PROCESSOR_PATH=/path/to/trace_processor_shell` 使用本机已有 binary,或设置 `TRACE_PROCESSOR_DOWNLOAD_BASE` / `TRACE_PROCESSOR_DOWNLOAD_URL` 指向可信镜像;下载内容仍会按固定 SHA256 校验。`smartperfetto` 仍保留为长命令名;源码 checkout 里的脚本只用于维护者调试 CLI。完整命令、抓取预设、REPL slash 命令、存储布局和 resume 语义见 [CLI 参考](docs/reference/cli.md)。 ## API 接入 diff --git a/Trace/README.md b/Trace/README.md new file mode 100644 index 000000000..1abd588cc --- /dev/null +++ b/Trace/README.md @@ -0,0 +1,61 @@ + +# SmartPerfetto Trace Corpus + +This directory is the source-controlled trace test and reference corpus. + +- [Real trace cases](./real/README.md): 6 +- [Constructed trace cases](./constructed/README.md): 12 +- [Machine-readable catalog](./catalog.json) +- [Skill and Strategy coverage](./coverage.json) + +Skill execution quality: 131 semantic, 10 explicit graceful-empty, 3 explicit unavailable prerequisite, 87 definition-only. + +## Commands + +`npm run trace:validate` checks manifests, hashes, generated indexes, publication gates, legacy path coupling, and exact current Skill/Strategy inventory coverage. + +`npm run trace:build` deterministically materializes every base-plus-overlay case under ignored `Trace/.generated/` and reparses it with the pinned trace processor. + +`npm run trace:regression` validates, builds, and executes the complete deterministic corpus. Per-case evidence is written below `Trace/.generated/constructed//`. + +## Add a real case + +Stage a capture privately first; repeat `--result`, `--log`, and `--tag` as needed: + +`node Trace/tools/trace-corpus.cjs import-real --id typical-startup --title "Typical startup" --description "Representative cold start" --scene startup --trace /path/to/trace.pftrace --origin "maintainer capture" --result /path/to/result.json --log /path/to/session.log --android-release 16 --api-level 36 --device "Pixel"` + +The command copies evidence atomically into ignored `Trace/real/.private//`. Review the trace and manifest before publication. Promotion requires explicit approvals and rolls back if catalog validation fails: + +`node Trace/tools/trace-corpus.cjs promote-real typical-startup --license CC-BY-4.0 --consent owner-approved --privacy-review approved --sanitization-review approved` + +## Extend constructed cases + +Constructed cases keep source scenarios and overlay protobufs in Git; combined traces are generated. Copy [the scenario template](./constructed/_templates/scenario.example.json), declare Android compatibility in `case.json`, assign each target explicitly, then run `npm run trace:regression`. Adding a Skill or Strategy without a current expectation fails validation. Use separate case ids with non-overlapping API ranges when Android behavior differs. + +## Real cases + +| Case | Scene | Android | Publication | Analysis | +| --- | --- | --- | --- | --- | +| [Android 16 customer scroll](./real/android-scroll-customer/) | scrolling | Android 16 / API 36 | legacy-tracked | 1 file(s) | +| [Android 15 standard AOSP scroll](./real/android-scroll-standard/) | scrolling | Android 15 / API 35 | legacy-tracked | 1 file(s) | +| [Android 16 heavy startup](./real/android-startup-heavy/) | startup | Android 16 / API 36 | legacy-tracked | 1 file(s) | +| [Android 16 light startup](./real/android-startup-light/) | startup | Android 16 / API 36 | legacy-tracked | 1 file(s) | +| [Android 15 Flutter SurfaceView scroll](./real/flutter-scroll-surface-view/) | scrolling | Android 15 / API 35 | legacy-tracked | 1 file(s) | +| [Android 15 Flutter TextureView scroll](./real/flutter-scroll-texture-view/) | scrolling | Android 15 / API 35 | legacy-tracked | 1 file(s) | + +## Constructed cases + +| Case | Scene | Android | Base | Coverage | +| --- | --- | --- | --- | --- | --- | +| [Binder, lock, and I/O blocking](./constructed/binder-io-blocking/) | io | Android 16 / API 36 (tested API 35-36) | android-startup-heavy | 25 target(s) | +| [Framework rendering pipeline signatures](./constructed/framework-pipelines/) | pipeline | Android 16 / API 36 (tested API 35-36) | android-startup-heavy | 33 target(s) | +| [General analysis and runtime contracts](./constructed/general-runtime-contracts/) | general | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 14 target(s) | +| [GPU workload and frequency](./constructed/gpu-workload/) | gpu | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 10 target(s) | +| [Input and interaction latency](./constructed/input-interaction-latency/) | interaction | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 13 target(s) | +| [Linux and Android system state](./constructed/linux-system-state/) | linux | Android 16 / API 36 (tested API 35-36) | android-startup-light | 5 target(s) | +| [Media, network, and camera pipeline](./constructed/media-network-camera/) | media | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 8 target(s) | +| [Memory and GC pressure](./constructed/memory-gc-pressure/) | memory | Android 16 / API 36 (tested API 35-36) | android-startup-light | 19 target(s) | +| [Power and thermal throttling](./constructed/power-thermal/) | power | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 19 target(s) | +| [Rendering pipeline jank](./constructed/rendering-jank/) | scrolling | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 46 target(s) | +| [Scheduler and CPU contention](./constructed/scheduler-cpu-contention/) | cpu | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 36 target(s) | +| [Startup and process lifecycle](./constructed/startup-lifecycle/) | startup | Android 16 / API 36 (tested API 35-36) | android-startup-heavy | 24 target(s) | diff --git a/Trace/catalog.json b/Trace/catalog.json new file mode 100644 index 000000000..02c5233fe --- /dev/null +++ b/Trace/catalog.json @@ -0,0 +1,4437 @@ +{ + "schema_version": 1, + "cases": [ + { + "schema_version": 1, + "id": "android-scroll-customer", + "kind": "real", + "title": "Android 16 customer scroll", + "description": "Existing customer scrolling fixture with multiple gesture and jank events.", + "scene": "scrolling", + "tags": [ + "android", + "scrolling", + "customer", + "jank" + ], + "aliases": [ + "scroll-demo-customer-scroll.pftrace" + ], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "1015ba963c49cd57d08d8997c4c48027026f2d6e41f7eb72b4b1356e39f7af3a", + "materialization": "committed" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110", + "build_fingerprint": "OPPO/PKH110/OP5DC1L1:16/AP3A.240617.008/V.2a01376-7328d8-769621:user/release-keys", + "compatibility": { + "min_api": 36, + "max_api": 36 + } + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": [ + "analysis/scroll-demo-customer-scroll.pftrace.fps_report.txt" + ], + "logs": [] + }, + "coverage": { + "skills": [], + "strategies": [], + "expectations": [] + }, + "case_dir": "Trace/real/android-scroll-customer", + "manifest_path": "Trace/real/android-scroll-customer/case.json" + }, + { + "schema_version": 1, + "id": "android-scroll-standard", + "kind": "real", + "title": "Android 15 standard AOSP scroll", + "description": "Existing standard Android View scrolling fixture without pre-animation.", + "scene": "scrolling", + "tags": [ + "android", + "scrolling", + "aosp", + "view" + ], + "aliases": [ + "scroll_Standard-AOSP-App-Without-PreAnimation.pftrace" + ], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "df7eebc5c6ff19b48331f8f1bca346612d86a5ae26eae202d46842a83f87a653", + "materialization": "committed" + }, + "android": { + "release": "15", + "api_level": 35, + "device": "nubia pacific", + "build_fingerprint": "nubia/pacific/pacific:15/1.4.1.0/101:user/release-keys", + "compatibility": { + "min_api": 35, + "max_api": 35 + } + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": [ + "analysis/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace.fps_report.txt" + ], + "logs": [] + }, + "coverage": { + "skills": [], + "strategies": [], + "expectations": [] + }, + "case_dir": "Trace/real/android-scroll-standard", + "manifest_path": "Trace/real/android-scroll-standard/case.json" + }, + { + "schema_version": 1, + "id": "android-startup-heavy", + "kind": "real", + "title": "Android 16 heavy startup", + "description": "Existing SmartPerfetto heavy application startup fixture captured on a Xiaomi pandora device.", + "scene": "startup", + "tags": [ + "android", + "startup", + "heavy" + ], + "aliases": [ + "lacunh_heavy.pftrace" + ], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "2a0c7b85e7f4a14e43b7f8d6de21172e05eba1ed3ad8ef7df539bb5e59b6ba63", + "materialization": "committed" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Xiaomi pandora", + "build_fingerprint": "Xiaomi/pandora/pandora:16/BP2A.250605.031.A3/OS3.0.34.0.WBLCNXM:user/release-keys", + "compatibility": { + "min_api": 36, + "max_api": 36 + } + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": [ + "analysis/lacunh_heavy.pftrace.fps_report.txt" + ], + "logs": [] + }, + "coverage": { + "skills": [], + "strategies": [], + "expectations": [] + }, + "case_dir": "Trace/real/android-startup-heavy", + "manifest_path": "Trace/real/android-startup-heavy/case.json" + }, + { + "schema_version": 1, + "id": "android-startup-light", + "kind": "real", + "title": "Android 16 light startup", + "description": "Existing SmartPerfetto light application startup fixture captured on a Google raven device.", + "scene": "startup", + "tags": [ + "android", + "startup", + "light" + ], + "aliases": [ + "launch_light.pftrace" + ], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "6c5479fd1b765ee4d29692c43a8204b972bc0f97eb373aca98ea7e11e99fd8b4", + "materialization": "committed" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Google raven", + "build_fingerprint": "google/raven_beta/raven:16/BP31.250523.010/13667654:user/release-keys", + "compatibility": { + "min_api": 36, + "max_api": 36 + } + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": [ + "analysis/launch_light.pftrace.fps_report.txt" + ], + "logs": [] + }, + "coverage": { + "skills": [], + "strategies": [], + "expectations": [] + }, + "case_dir": "Trace/real/android-startup-light", + "manifest_path": "Trace/real/android-startup-light/case.json" + }, + { + "schema_version": 1, + "id": "binder-io-blocking", + "kind": "constructed", + "title": "Binder, lock, and I/O blocking", + "description": "Deterministic binder, lock, and i/o blocking signals over the android-startup-heavy real base trace.", + "scene": "io", + "tags": [ + "android", + "constructed", + "io" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "e3d9f69a6a1009f685c8abcb70951c4d42f1186db2f63dcba026b0058d848866", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Xiaomi pandora base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-heavy", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "binder-io-blocking-v1", + "output": "Trace/.generated/constructed/binder-io-blocking/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_kernel_wakelock_summary", + "anr_analysis", + "anr_context_in_range", + "anr_detail", + "anr_main_thread_blocking", + "binder_analysis", + "binder_blocking_in_range", + "binder_detail", + "binder_in_range", + "binder_module", + "binder_root_cause", + "binder_storm_detection", + "block_io_analysis", + "blocking_chain_analysis", + "filesystem_module", + "io_pressure", + "lock_binder_wait", + "lock_contention_analysis", + "lock_contention_in_range", + "lock_contention_module", + "main_thread_file_io_in_range", + "page_fault_in_range", + "wakelock_tracking" + ], + "strategies": [ + "anr", + "io" + ], + "expectations": [ + { + "id": "execute-android_kernel_wakelock_summary", + "type": "skill", + "target": "android_kernel_wakelock_summary", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/android_kernel_wakelock_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_summary" + ], + "semantic_step": "wakelock_summary", + "limitation_reason": "The current generator does not emit android_kernel_wakelock counter tracks.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_analysis", + "type": "skill", + "target": "anr_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/anr_analysis.skill.yaml", + "parameters": { + "multi_anr_threshold": 3, + "multi_anr_span_seconds": 10, + "io_wait_threshold_ms": 500, + "uninterruptible_wait_threshold_ms": 500, + "enable_lock_probe": true, + "lock_contention_futex_p95_critical_ms": 20, + "lock_contention_mutex_p95_warning_ms": 10, + "enable_detail_analysis": true + }, + "required_steps": [ + "init_cpu_topology", + "get_anr_context", + "anr_detection" + ], + "semantic_step": "anr_detection", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-anr_context_in_range", + "type": "skill", + "target": "anr_context_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/anr_context_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_detail", + "type": "skill", + "target": "anr_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/anr_detail.skill.yaml", + "parameters": { + "anr_ts": 1, + "timeout_ns": 1, + "process_name": "com.smartperfetto.fixture", + "pid": 700000, + "upid": "${fixture_upid}", + "anr_type": "" + }, + "required_steps": [ + "init_cpu_topology", + "anr_info" + ], + "semantic_step": "anr_info", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_main_thread_blocking", + "type": "skill", + "target": "anr_main_thread_blocking", + "mode": "semantic", + "source_file": "backend/skills/atomic/anr_main_thread_blocking.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "main_thread_state" + ], + "semantic_step": "main_thread_state", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_analysis", + "type": "skill", + "target": "binder_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/binder_analysis.skill.yaml", + "parameters": { + "slow_binder_critical_ms": 50, + "slow_binder_warning_ms": 16, + "main_thread_txn_warning": 50, + "server_response_warning_ms": 20 + }, + "required_steps": [ + "check_binder" + ], + "semantic_step": "check_binder", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-binder_blocking_in_range", + "type": "skill", + "target": "binder_blocking_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/binder_blocking_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_detail", + "type": "skill", + "target": "binder_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/binder_detail.skill.yaml", + "parameters": { + "binder_ts": 1, + "binder_end_ts": "${trace_end}", + "dur_ms": 1, + "server_process": "", + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "init_cpu_topology", + "binder_info" + ], + "semantic_step": "binder_info", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-binder_in_range", + "type": "skill", + "target": "binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_module", + "type": "skill", + "target": "binder_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/binder_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "binder_summary" + ], + "semantic_step": "binder_summary", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_root_cause", + "type": "skill", + "target": "binder_root_cause", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/binder_root_cause.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "slow_binder_breakdown" + ], + "semantic_step": "slow_binder_breakdown", + "limitation_reason": "The fixture has blocking slices but no kernel Binder transaction packet chain.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_storm_detection", + "type": "skill", + "target": "binder_storm_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/binder_storm_detection.skill.yaml", + "parameters": { + "threshold": 50 + }, + "required_steps": [ + "binder_density" + ], + "semantic_step": "binder_density", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-block_io_analysis", + "type": "skill", + "target": "block_io_analysis", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/block_io_analysis.skill.yaml", + "parameters": { + "min_queue_depth": 5 + }, + "required_steps": [ + "data_check", + "io_overview" + ], + "semantic_step": "io_overview", + "limitation_reason": "The fixture does not yet emit block_rq ftrace events.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-blocking_chain_analysis", + "type": "skill", + "target": "blocking_chain_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/blocking_chain_analysis.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "thread_state_distribution" + ], + "semantic_step": "thread_state_distribution", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-filesystem_module", + "type": "skill", + "target": "filesystem_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/filesystem_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "io_overview" + ], + "semantic_step": "io_overview", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-io_pressure", + "type": "skill", + "target": "io_pressure", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/io_pressure.skill.yaml", + "parameters": { + "min_duration_ms": 1, + "max_items": 20, + "long_io_threshold_ms": 10, + "critical_io_wait_ms": 5000, + "warning_io_wait_ms": 1000 + }, + "required_steps": [ + "data_check", + "io_wait_overview" + ], + "semantic_step": "io_wait_overview", + "limitation_reason": "The fixture does not yet emit PSI I/O pressure counters.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_binder_wait", + "type": "skill", + "target": "lock_binder_wait", + "mode": "semantic", + "source_file": "backend/skills/composite/lock_binder_wait.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "main_thread_blocking_chain" + ], + "semantic_step": "main_thread_blocking_chain", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_analysis", + "type": "skill", + "target": "lock_contention_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/lock_contention_analysis.skill.yaml", + "parameters": { + "min_duration_ms": 10 + }, + "required_steps": [ + "data_check", + "owner_contention_events" + ], + "semantic_step": "owner_contention_events", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_in_range", + "type": "skill", + "target": "lock_contention_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/lock_contention_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "monitor_contentions" + ], + "semantic_step": "monitor_contentions", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_module", + "type": "skill", + "target": "lock_contention_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/lock_contention_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "lock_contention_overview" + ], + "semantic_step": "lock_contention_overview", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-main_thread_file_io_in_range", + "type": "skill", + "target": "main_thread_file_io_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_file_io_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-page_fault_in_range", + "type": "skill", + "target": "page_fault_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/page_fault_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-wakelock_tracking", + "type": "skill", + "target": "wakelock_tracking", + "mode": "semantic", + "source_file": "backend/skills/atomic/wakelock_tracking.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_events" + ], + "semantic_step": "wakelock_events", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "strategy-anr", + "type": "strategy", + "target": "anr", + "query": "anr", + "expected_strategy": "anr", + "source_file": "backend/strategies/anr.strategy.md", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "strategy-io", + "type": "strategy", + "target": "io", + "query": "io", + "expected_strategy": "io", + "source_file": "backend/strategies/io.strategy.md", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + } + ] + }, + "case_dir": "Trace/constructed/binder-io-blocking", + "manifest_path": "Trace/constructed/binder-io-blocking/case.json" + }, + { + "schema_version": 1, + "id": "flutter-scroll-surface-view", + "kind": "real", + "title": "Android 15 Flutter SurfaceView scroll", + "description": "Existing WeChat Flutter scrolling fixture rendered through SurfaceView.", + "scene": "scrolling", + "tags": [ + "android", + "flutter", + "scrolling", + "surfaceview", + "wechat" + ], + "aliases": [ + "Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace" + ], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "e876c56b5d7290acda596e73929c1a127df4255e9ca6951605dad40833e67a53", + "materialization": "committed" + }, + "android": { + "release": "15", + "api_level": 35, + "device": "nubia pacific", + "build_fingerprint": "nubia/pacific/pacific:15/1.4.1.0/101:user/release-keys", + "compatibility": { + "min_api": 35, + "max_api": 35 + } + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": [ + "analysis/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace.fps_report.txt" + ], + "logs": [] + }, + "coverage": { + "skills": [], + "strategies": [], + "expectations": [] + }, + "case_dir": "Trace/real/flutter-scroll-surface-view", + "manifest_path": "Trace/real/flutter-scroll-surface-view/case.json" + }, + { + "schema_version": 1, + "id": "flutter-scroll-texture-view", + "kind": "real", + "title": "Android 15 Flutter TextureView scroll", + "description": "Existing Flutter scrolling fixture rendered through TextureView.", + "scene": "scrolling", + "tags": [ + "android", + "flutter", + "scrolling", + "textureview" + ], + "aliases": [ + "Scroll-Flutter-327-TextureView.pftrace" + ], + "known_issues": [ + "The original capture reports seven trace_sorter_negative_timestamp_dropped health events." + ], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "645deaa9fc564e0ea33072c83f5ecf1d0a6f53a97055cc98c640e8294deb315e", + "materialization": "committed" + }, + "android": { + "release": "15", + "api_level": 35, + "device": "nubia pacific", + "build_fingerprint": "nubia/pacific/pacific:15/1.4.1.0/101:user/release-keys", + "compatibility": { + "min_api": 35, + "max_api": 35 + } + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": [ + "analysis/Scroll-Flutter-327-TextureView.pftrace.fps_report.txt" + ], + "logs": [] + }, + "coverage": { + "skills": [], + "strategies": [], + "expectations": [] + }, + "case_dir": "Trace/real/flutter-scroll-texture-view", + "manifest_path": "Trace/real/flutter-scroll-texture-view/case.json" + }, + { + "schema_version": 1, + "id": "framework-pipelines", + "kind": "constructed", + "title": "Framework rendering pipeline signatures", + "description": "Deterministic framework rendering pipeline signatures signals over the android-startup-heavy real base trace.", + "scene": "pipeline", + "tags": [ + "android", + "constructed", + "pipeline" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "35c5cf1014ac0032188fb3da8d08899fa1ea3bb25ac6f34eb7f1c00645f3a9b3", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Xiaomi pandora base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-heavy", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "framework-pipelines-v1", + "output": "Trace/.generated/constructed/framework-pipelines/trace.pftrace" + }, + "coverage": { + "skills": [ + "ams_module", + "art_module", + "code_pinpoint", + "compose_recomposition_hotspot", + "pipeline_4feature_scoring", + "pipeline_android_pip_freeform", + "pipeline_android_view_mixed", + "pipeline_android_view_multi_window", + "pipeline_android_view_software", + "pipeline_android_view_standard_blast", + "pipeline_android_view_standard_legacy", + "pipeline_chrome_browser_viz", + "pipeline_compose_standard", + "pipeline_flutter_surfaceview_impeller", + "pipeline_flutter_surfaceview_skia", + "pipeline_game_engine", + "pipeline_imagereader_pipeline", + "pipeline_key_slices_overlay", + "pipeline_rn_new_arch", + "pipeline_rn_old_arch", + "pipeline_rn_skia", + "pipeline_software_compositing", + "pipeline_surface_control_api", + "pipeline_surfaceview_blast", + "pipeline_variable_refresh_rate", + "pipeline_video_overlay_hwc", + "pipeline_webview_gl_functor", + "pipeline_webview_surface_control", + "pipeline_webview_surfaceview_wrapper", + "systemui_module", + "third_party_module", + "wms_module" + ], + "strategies": [ + "pipeline" + ], + "expectations": [ + { + "id": "execute-ams_module", + "type": "skill", + "target": "ams_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/ams_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_timing" + ], + "semantic_step": "startup_timing", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-art_module", + "type": "skill", + "target": "art_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/art_module.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture" + }, + "required_steps": [ + "gc_overview" + ], + "semantic_step": "gc_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-code_pinpoint", + "type": "skill", + "target": "code_pinpoint", + "mode": "semantic", + "source_file": "backend/skills/composite/code_pinpoint.skill.yaml", + "parameters": {}, + "required_steps": [ + "hot_slices" + ], + "semantic_step": "hot_slices", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-compose_recomposition_hotspot", + "type": "skill", + "target": "compose_recomposition_hotspot", + "mode": "semantic", + "source_file": "backend/skills/atomic/compose_recomposition_hotspot.skill.yaml", + "parameters": {}, + "required_steps": [ + "frame_timeline_check", + "recomposition_overview" + ], + "semantic_step": "recomposition_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-pipeline_4feature_scoring", + "type": "skill", + "target": "pipeline_4feature_scoring", + "mode": "semantic", + "source_file": "backend/skills/atomic/pipeline_4feature_scoring.skill.yaml", + "parameters": {}, + "required_steps": [ + "producer_thread_signature" + ], + "semantic_step": "producer_thread_signature", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_pip_freeform", + "type": "skill", + "target": "pipeline_android_pip_freeform", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_pip_freeform.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_mixed", + "type": "skill", + "target": "pipeline_android_view_mixed", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_mixed.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_multi_window", + "type": "skill", + "target": "pipeline_android_view_multi_window", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_multi_window.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_software", + "type": "skill", + "target": "pipeline_android_view_software", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_software.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_standard_blast", + "type": "skill", + "target": "pipeline_android_view_standard_blast", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_standard_blast.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_standard_legacy", + "type": "skill", + "target": "pipeline_android_view_standard_legacy", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_standard_legacy.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_chrome_browser_viz", + "type": "skill", + "target": "pipeline_chrome_browser_viz", + "mode": "definition", + "source_file": "backend/skills/pipelines/chrome_browser_viz.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_compose_standard", + "type": "skill", + "target": "pipeline_compose_standard", + "mode": "definition", + "source_file": "backend/skills/pipelines/compose_standard.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_flutter_surfaceview_impeller", + "type": "skill", + "target": "pipeline_flutter_surfaceview_impeller", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_surfaceview_impeller.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_flutter_surfaceview_skia", + "type": "skill", + "target": "pipeline_flutter_surfaceview_skia", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_surfaceview_skia.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_game_engine", + "type": "skill", + "target": "pipeline_game_engine", + "mode": "definition", + "source_file": "backend/skills/pipelines/game_engine.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_imagereader_pipeline", + "type": "skill", + "target": "pipeline_imagereader_pipeline", + "mode": "definition", + "source_file": "backend/skills/pipelines/imagereader_pipeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-pipeline_key_slices_overlay", + "type": "skill", + "target": "pipeline_key_slices_overlay", + "mode": "semantic", + "source_file": "backend/skills/atomic/pipeline_key_slices_overlay.skill.yaml", + "parameters": { + "slice_names": "'Choreographer#doFrame','DrawFrame','queueBuffer','latchBuffer'" + }, + "required_steps": [ + "pipeline_key_slices_overlay" + ], + "semantic_step": "pipeline_key_slices_overlay", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_new_arch", + "type": "skill", + "target": "pipeline_rn_new_arch", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_new_arch.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_old_arch", + "type": "skill", + "target": "pipeline_rn_old_arch", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_old_arch.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_skia", + "type": "skill", + "target": "pipeline_rn_skia", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_skia.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_software_compositing", + "type": "skill", + "target": "pipeline_software_compositing", + "mode": "definition", + "source_file": "backend/skills/pipelines/software_compositing.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_surface_control_api", + "type": "skill", + "target": "pipeline_surface_control_api", + "mode": "definition", + "source_file": "backend/skills/pipelines/surface_control_api.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_surfaceview_blast", + "type": "skill", + "target": "pipeline_surfaceview_blast", + "mode": "definition", + "source_file": "backend/skills/pipelines/surfaceview_blast.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_variable_refresh_rate", + "type": "skill", + "target": "pipeline_variable_refresh_rate", + "mode": "definition", + "source_file": "backend/skills/pipelines/variable_refresh_rate.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_video_overlay_hwc", + "type": "skill", + "target": "pipeline_video_overlay_hwc", + "mode": "definition", + "source_file": "backend/skills/pipelines/video_overlay_hwc.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_gl_functor", + "type": "skill", + "target": "pipeline_webview_gl_functor", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_gl_functor.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_surface_control", + "type": "skill", + "target": "pipeline_webview_surface_control", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_surface_control.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_surfaceview_wrapper", + "type": "skill", + "target": "pipeline_webview_surfaceview_wrapper", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_surfaceview_wrapper.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-systemui_module", + "type": "skill", + "target": "systemui_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/systemui_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "systemui_process" + ], + "semantic_step": "systemui_process", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-third_party_module", + "type": "skill", + "target": "third_party_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/third_party_module.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture" + }, + "required_steps": [ + "thread_overview" + ], + "semantic_step": "thread_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-wms_module", + "type": "skill", + "target": "wms_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/wms_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "window_animation_overview" + ], + "semantic_step": "window_animation_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "strategy-pipeline", + "type": "strategy", + "target": "pipeline", + "query": "管线识别", + "expected_strategy": "pipeline", + "source_file": "backend/strategies/pipeline.strategy.md", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + } + ] + }, + "case_dir": "Trace/constructed/framework-pipelines", + "manifest_path": "Trace/constructed/framework-pipelines/case.json" + }, + { + "schema_version": 1, + "id": "general-runtime-contracts", + "kind": "constructed", + "title": "General analysis and runtime contracts", + "description": "Deterministic general analysis and runtime contracts signals over the android-scroll-customer real base trace.", + "scene": "general", + "tags": [ + "android", + "constructed", + "general" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "2627a1c665d57bd90a52a8c01feb98e188125c99bd4268e2282318445b0c743a", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "general-runtime-contracts-v1", + "output": "Trace/.generated/constructed/general-runtime-contracts/trace.pftrace" + }, + "coverage": { + "skills": [ + "callstack_analysis", + "fence_wait_decomposition", + "global_trace_sanity_check", + "main_thread_slices_in_range", + "main_thread_states_in_range", + "multi_trace_result_comparison", + "process_identity_resolver" + ], + "strategies": [ + "general", + "multi_trace_result_comparison", + "overview", + "runtime_correctness", + "smart", + "teaching", + "verifier_misdiagnosis" + ], + "expectations": [ + { + "id": "execute-callstack_analysis", + "type": "skill", + "target": "callstack_analysis", + "mode": "graceful_empty", + "source_file": "backend/skills/deep/callstack_analysis.skill.yaml", + "parameters": { + "min_samples": 10 + }, + "required_steps": [ + "check_samples" + ], + "semantic_step": "check_samples", + "limitation_reason": "The fixture does not contain perf samples or interned callstacks.", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "execute-fence_wait_decomposition", + "type": "skill", + "target": "fence_wait_decomposition", + "mode": "semantic", + "source_file": "backend/skills/atomic/fence_wait_decomposition.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "acquire_fence_wait" + ], + "semantic_step": "acquire_fence_wait", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "execute-global_trace_sanity_check", + "type": "skill", + "target": "global_trace_sanity_check", + "mode": "semantic", + "source_file": "backend/skills/composite/global_trace_sanity_check.skill.yaml", + "parameters": { + "max_rows": 20 + }, + "required_steps": [ + "trace_window" + ], + "semantic_step": "trace_window", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-main_thread_slices_in_range", + "type": "skill", + "target": "main_thread_slices_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_slices_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-main_thread_states_in_range", + "type": "skill", + "target": "main_thread_states_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_states_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-multi_trace_result_comparison", + "type": "skill", + "target": "multi_trace_result_comparison", + "mode": "definition", + "source_file": "backend/skills/comparison/multi_trace_result_comparison.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-process_identity_resolver", + "type": "skill", + "target": "process_identity_resolver", + "mode": "definition", + "source_file": "backend/skills/atomic/process_identity_resolver.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-general", + "type": "strategy", + "target": "general", + "query": "分析这份 trace", + "expected_strategy": "general", + "source_file": "backend/strategies/general.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-multi_trace_result_comparison", + "type": "strategy", + "target": "multi_trace_result_comparison", + "query": "分析结果对比", + "expected_strategy": "multi_trace_result_comparison", + "source_file": "backend/strategies/multi-trace-result-comparison.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-overview", + "type": "strategy", + "target": "overview", + "query": "发生了什么", + "expected_strategy": "overview", + "source_file": "backend/strategies/overview.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-runtime_correctness", + "type": "strategy", + "target": "runtime_correctness", + "query": "runtime", + "expected_strategy": "runtime_correctness", + "source_file": "backend/strategies/runtime-correctness.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-smart", + "type": "strategy", + "target": "smart", + "query": "smart", + "expected_strategy": "smart", + "source_file": "backend/strategies/smart.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-teaching", + "type": "strategy", + "target": "teaching", + "query": "教学", + "expected_strategy": "teaching", + "source_file": "backend/strategies/teaching.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-verifier_misdiagnosis", + "type": "strategy", + "target": "verifier_misdiagnosis", + "query": "contract-only:verifier_misdiagnosis", + "expected_strategy": "verifier_misdiagnosis", + "source_file": "backend/strategies/verifier-misdiagnosis.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + } + ] + }, + "case_dir": "Trace/constructed/general-runtime-contracts", + "manifest_path": "Trace/constructed/general-runtime-contracts/case.json" + }, + { + "schema_version": 1, + "id": "gpu-workload", + "kind": "constructed", + "title": "GPU workload and frequency", + "description": "Deterministic gpu workload and frequency signals over the android-scroll-customer real base trace.", + "scene": "gpu", + "tags": [ + "android", + "constructed", + "gpu" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "2e6a951520693480b5316c262b57557a564ec9277b24e97fac49b4bb8e747e73", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "gpu-workload-v1", + "output": "Trace/.generated/constructed/gpu-workload/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_gpu_work_period_track", + "gpu_analysis", + "gpu_freq_in_range", + "gpu_frequency_analysis", + "gpu_metrics", + "gpu_module", + "gpu_v57_ai_diagnostics", + "pipeline_angle_gles_vulkan", + "pipeline_opengl_es", + "pipeline_vulkan_native" + ], + "strategies": [], + "expectations": [ + { + "id": "execute-android_gpu_work_period_track", + "type": "skill", + "target": "android_gpu_work_period_track", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_gpu_work_period_track.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_work" + ], + "semantic_step": "gpu_work", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_analysis", + "type": "skill", + "target": "gpu_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/gpu_analysis.skill.yaml", + "parameters": { + "high_freq_threshold_pct": 70, + "mid_freq_threshold_pct": 40, + "low_freq_threshold_pct": 10, + "freq_drop_ratio": 0.7, + "freq_drop_count_threshold": 20, + "high_load_min_dur_ns": 500000000 + }, + "required_steps": [ + "data_check", + "gpu_freq_overview" + ], + "semantic_step": "gpu_freq_overview", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-gpu_freq_in_range", + "type": "skill", + "target": "gpu_freq_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_freq_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-gpu_frequency_analysis", + "type": "skill", + "target": "gpu_frequency_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_frequency_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_metrics", + "type": "skill", + "target": "gpu_metrics", + "mode": "semantic", + "source_file": "backend/skills/atomic/gpu_metrics.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_frequency" + ], + "semantic_step": "gpu_frequency", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_module", + "type": "skill", + "target": "gpu_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/gpu_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_frequency_overview" + ], + "semantic_step": "gpu_frequency_overview", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_v57_ai_diagnostics", + "type": "skill", + "target": "gpu_v57_ai_diagnostics", + "mode": "semantic", + "source_file": "backend/skills/composite/gpu_v57_ai_diagnostics.skill.yaml", + "parameters": { + "target_freq_ratio": 0.9, + "min_throttle_ns": 1000, + "max_rows": 20 + }, + "required_steps": [ + "data_check", + "gpu_inventory" + ], + "semantic_step": "gpu_inventory", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_angle_gles_vulkan", + "type": "skill", + "target": "pipeline_angle_gles_vulkan", + "mode": "definition", + "source_file": "backend/skills/pipelines/angle_gles_vulkan.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_opengl_es", + "type": "skill", + "target": "pipeline_opengl_es", + "mode": "definition", + "source_file": "backend/skills/pipelines/opengl_es.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_vulkan_native", + "type": "skill", + "target": "pipeline_vulkan_native", + "mode": "definition", + "source_file": "backend/skills/pipelines/vulkan_native.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + } + ] + }, + "case_dir": "Trace/constructed/gpu-workload", + "manifest_path": "Trace/constructed/gpu-workload/case.json" + }, + { + "schema_version": 1, + "id": "input-interaction-latency", + "kind": "constructed", + "title": "Input and interaction latency", + "description": "Deterministic input and interaction latency signals over the android-scroll-customer real base trace.", + "scene": "interaction", + "tags": [ + "android", + "constructed", + "interaction" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "7195386093ce0d29a5110861a928aa96d6af9fb15580bba9bc41248c29a39ab2", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "input-interaction-latency-v1", + "output": "Trace/.generated/constructed/input-interaction-latency/trace.pftrace" + }, + "coverage": { + "skills": [ + "app_lifecycle_in_range", + "battery_doze_state_timeline", + "click_response_analysis", + "click_response_detail", + "device_state_timeline", + "input_events_in_range", + "input_module", + "navigation_analysis", + "scene_reconstruction", + "state_timeline", + "touch_to_display_latency" + ], + "strategies": [ + "interaction", + "touch_tracking" + ], + "expectations": [ + { + "id": "definition-app_lifecycle_in_range", + "type": "skill", + "target": "app_lifecycle_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/app_lifecycle_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-battery_doze_state_timeline", + "type": "skill", + "target": "battery_doze_state_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_doze_state_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "deep_idle_intervals" + ], + "semantic_step": "deep_idle_intervals", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-click_response_analysis", + "type": "skill", + "target": "click_response_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/click_response_analysis.skill.yaml", + "parameters": { + "slow_event_threshold_ms": 100, + "avg_latency_critical_ms": 100, + "avg_handling_warning_ms": 50, + "avg_dispatch_warning_ms": 20, + "binder_blocking_threshold_ms": 30, + "critical_event_threshold_ms": 200, + "thread_state_min_dur_ms": 50, + "enable_per_event_detail": true + }, + "required_steps": [ + "check_input_data" + ], + "semantic_step": "check_input_data", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-click_response_detail", + "type": "skill", + "target": "click_response_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/click_response_detail.skill.yaml", + "parameters": { + "event_ts": "${trace_start}", + "event_end_ts": "${trace_end}", + "total_ms": 1, + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "init_cpu_topology", + "event_info" + ], + "semantic_step": "event_info", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-device_state_timeline", + "type": "skill", + "target": "device_state_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/device_state_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "cpu_freq_transitions" + ], + "semantic_step": "cpu_freq_transitions", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "definition-input_events_in_range", + "type": "skill", + "target": "input_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/input_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-input_module", + "type": "skill", + "target": "input_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/input_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "click_response_overview" + ], + "semantic_step": "click_response_overview", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-navigation_analysis", + "type": "skill", + "target": "navigation_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/navigation_analysis.skill.yaml", + "parameters": { + "slow_navigation_ms": 400, + "nav_rating_good_ms": 200, + "nav_rating_severe_ms": 700, + "slow_lifecycle_ms": 100, + "blocking_op_min_dur_ms": 5, + "inflate_critical_ms": 100, + "binder_blocking_ms": 16, + "oncreate_slow_ms": 200 + }, + "required_steps": [ + "check_lifecycle_data", + "get_process", + "navigation_overview" + ], + "semantic_step": "navigation_overview", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-scene_reconstruction", + "type": "skill", + "target": "scene_reconstruction", + "mode": "semantic", + "source_file": "backend/skills/composite/scene_reconstruction.skill.yaml", + "parameters": { + "trace_id": 1 + }, + "required_steps": [ + "trace_time_range" + ], + "semantic_step": "trace_time_range", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-state_timeline", + "type": "skill", + "target": "state_timeline", + "mode": "semantic", + "source_file": "backend/skills/composite/state_timeline.skill.yaml", + "parameters": { + "trace_id": 1 + }, + "required_steps": [ + "timeline_bounds", + "device_state_lane" + ], + "semantic_step": "device_state_lane", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "definition-touch_to_display_latency", + "type": "skill", + "target": "touch_to_display_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/touch_to_display_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "strategy-interaction", + "type": "strategy", + "target": "interaction", + "query": "点击", + "expected_strategy": "interaction", + "source_file": "backend/strategies/interaction.strategy.md", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "strategy-touch_tracking", + "type": "strategy", + "target": "touch_tracking", + "query": "跟手度", + "expected_strategy": "touch_tracking", + "source_file": "backend/strategies/touch-tracking.strategy.md", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + } + ] + }, + "case_dir": "Trace/constructed/input-interaction-latency", + "manifest_path": "Trace/constructed/input-interaction-latency/case.json" + }, + { + "schema_version": 1, + "id": "linux-system-state", + "kind": "constructed", + "title": "Linux and Android system state", + "description": "Deterministic linux and android system state signals over the android-startup-light real base trace.", + "scene": "linux", + "tags": [ + "android", + "constructed", + "linux" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "f00d779b7a75887416a4e564508903e4db87ee3caa8a7066ff1975c62c88031a", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Google raven base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-light", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "linux-system-state-v1", + "output": "Trace/.generated/constructed/linux-system-state/trace.pftrace" + }, + "coverage": { + "skills": [ + "device_state_snapshot", + "linux_perf_counter_hotspots", + "linux_systemd_journald_analysis", + "trace_state_track_summary" + ], + "strategies": [ + "linux" + ], + "expectations": [ + { + "id": "execute-device_state_snapshot", + "type": "skill", + "target": "device_state_snapshot", + "mode": "semantic", + "source_file": "backend/skills/composite/device_state_snapshot.skill.yaml", + "parameters": {}, + "required_steps": [ + "environment_snapshot" + ], + "semantic_step": "environment_snapshot", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "execute-linux_perf_counter_hotspots", + "type": "skill", + "target": "linux_perf_counter_hotspots", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/linux_perf_counter_hotspots.skill.yaml", + "parameters": {}, + "required_steps": [ + "perf_counter_hotspots" + ], + "semantic_step": "perf_counter_hotspots", + "limitation_reason": "The fixture does not contain PMU perf sample/counter packets.", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "definition-linux_systemd_journald_analysis", + "type": "skill", + "target": "linux_systemd_journald_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/linux_systemd_journald_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "definition-trace_state_track_summary", + "type": "skill", + "target": "trace_state_track_summary", + "mode": "definition", + "source_file": "backend/skills/atomic/trace_state_track_summary.skill.yaml", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "strategy-linux", + "type": "strategy", + "target": "linux", + "query": "linux", + "expected_strategy": "linux", + "source_file": "backend/strategies/linux.strategy.md", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + } + ] + }, + "case_dir": "Trace/constructed/linux-system-state", + "manifest_path": "Trace/constructed/linux-system-state/case.json" + }, + { + "schema_version": 1, + "id": "media-network-camera", + "kind": "constructed", + "title": "Media, network, and camera pipeline", + "description": "Deterministic media, network, and camera pipeline signals over the android-scroll-customer real base trace.", + "scene": "media", + "tags": [ + "android", + "constructed", + "media" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "4abf868de9627fd097ba4a473eca1f137a0d17351b12e43817dc0c0e16448ce4", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "media-network-camera-v1", + "output": "Trace/.generated/constructed/media-network-camera/trace.pftrace" + }, + "coverage": { + "skills": [ + "camera_trace_evidence", + "media_codec_activity", + "modem_network_correlation_summary", + "network_analysis", + "pipeline_camera_pipeline", + "webview_v8_analysis" + ], + "strategies": [ + "media", + "network" + ], + "expectations": [ + { + "id": "execute-camera_trace_evidence", + "type": "skill", + "target": "camera_trace_evidence", + "mode": "semantic", + "source_file": "backend/skills/composite/camera_trace_evidence.skill.yaml", + "parameters": { + "max_rows": 20 + }, + "required_steps": [ + "evidence_coverage" + ], + "semantic_step": "evidence_coverage", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-media_codec_activity", + "type": "skill", + "target": "media_codec_activity", + "mode": "semantic", + "source_file": "backend/skills/atomic/media_codec_activity.skill.yaml", + "parameters": {}, + "required_steps": [ + "codec_activity_summary" + ], + "semantic_step": "codec_activity_summary", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-modem_network_correlation_summary", + "type": "skill", + "target": "modem_network_correlation_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/modem_network_correlation_summary.skill.yaml", + "parameters": { + "top_n": 20 + }, + "required_steps": [ + "modem_network_correlation" + ], + "semantic_step": "modem_network_correlation", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-network_analysis", + "type": "skill", + "target": "network_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/network_analysis.skill.yaml", + "parameters": { + "heavy_traffic_critical_mb": 100, + "heavy_traffic_warning_mb": 10, + "frequent_active_periods": 20 + }, + "required_steps": [ + "check_network_data", + "check_network_slices", + "evidence_scope", + "network_overview", + "traffic_by_interface", + "app_traffic_ranking", + "transport_distribution", + "port_analysis", + "traffic_timeline", + "large_transfers", + "network_power_cost", + "network_slice_overview" + ], + "semantic_step": "network_slice_overview", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "definition-pipeline_camera_pipeline", + "type": "skill", + "target": "pipeline_camera_pipeline", + "mode": "definition", + "source_file": "backend/skills/pipelines/camera_pipeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-webview_v8_analysis", + "type": "skill", + "target": "webview_v8_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/webview_v8_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "v8_gc_events" + ], + "semantic_step": "v8_gc_events", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "strategy-media", + "type": "strategy", + "target": "media", + "query": "视频", + "expected_strategy": "media", + "source_file": "backend/strategies/media.strategy.md", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "strategy-network", + "type": "strategy", + "target": "network", + "query": "网络", + "expected_strategy": "network", + "source_file": "backend/strategies/network.strategy.md", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + } + ] + }, + "case_dir": "Trace/constructed/media-network-camera", + "manifest_path": "Trace/constructed/media-network-camera/case.json" + }, + { + "schema_version": 1, + "id": "memory-gc-pressure", + "kind": "constructed", + "title": "Memory and GC pressure", + "description": "Deterministic memory and gc pressure signals over the android-startup-light real base trace.", + "scene": "memory", + "tags": [ + "android", + "constructed", + "memory" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "4587bab434e00b01b06c43fc676a0bac58d3ecd5eed40be4f6b3cbe0ed967c67", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Google raven base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-light", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "memory-gc-pressure-v1", + "output": "Trace/.generated/constructed/memory-gc-pressure/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_bitmap_memory_per_process", + "android_heap_graph_leak_candidates", + "android_heap_graph_summary", + "android_memory_v57_ai_diagnostics", + "dmabuf_analysis", + "gc_analysis", + "gc_events_in_range", + "linux_process_rss_swap_timeline", + "lmk_analysis", + "lmk_kill_attribution", + "logcat_analysis", + "memory_analysis", + "memory_growth_detector", + "memory_module", + "memory_pressure_in_range", + "memory_rss_high_watermark", + "native_heap_breakdown", + "oom_adjuster_score_timeline" + ], + "strategies": [ + "memory" + ], + "expectations": [ + { + "id": "execute-android_bitmap_memory_per_process", + "type": "skill", + "target": "android_bitmap_memory_per_process", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_bitmap_memory_per_process.skill.yaml", + "parameters": {}, + "required_steps": [ + "bitmap_memory" + ], + "semantic_step": "bitmap_memory", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_heap_graph_leak_candidates", + "type": "skill", + "target": "android_heap_graph_leak_candidates", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/android_heap_graph_leak_candidates.skill.yaml", + "parameters": { + "lifecycle_slice_prefix": "SI$", + "max_candidates": 50, + "max_reference_edges": 100 + }, + "required_steps": [ + "leak_candidates" + ], + "semantic_step": "leak_candidates", + "limitation_reason": "The fixture does not contain a managed heap graph dump.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_heap_graph_summary", + "type": "skill", + "target": "android_heap_graph_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_heap_graph_summary.skill.yaml", + "parameters": { + "max_rows": 30 + }, + "required_steps": [ + "heap_graph_availability" + ], + "semantic_step": "heap_graph_availability", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_memory_v57_ai_diagnostics", + "type": "skill", + "target": "android_memory_v57_ai_diagnostics", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/android_memory_v57_ai_diagnostics.skill.yaml", + "parameters": { + "min_size_mb": 1, + "max_rows": 40 + }, + "required_steps": [ + "data_check", + "heap_graph_repeated_objects" + ], + "semantic_step": "heap_graph_repeated_objects", + "limitation_reason": "Heap-graph diagnostics remain empty without a managed heap graph dump.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-dmabuf_analysis", + "type": "skill", + "target": "dmabuf_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/dmabuf_analysis.skill.yaml", + "parameters": { + "min_size_mb": 1 + }, + "required_steps": [ + "data_check", + "dmabuf_overview" + ], + "semantic_step": "dmabuf_overview", + "limitation_reason": "The fixture does not contain DMA-BUF allocation/residency events.", + "expected_error": "Condition not met", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-gc_analysis", + "type": "skill", + "target": "gc_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/gc_analysis.skill.yaml", + "parameters": { + "min_gc_dur_ms": 5 + }, + "required_steps": [ + "data_check", + "gc_overview" + ], + "semantic_step": "gc_overview", + "limitation_reason": "The fixture has GC marker slices but not ART garbage-collection packets.", + "expected_error": "Condition not met", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "definition-gc_events_in_range", + "type": "skill", + "target": "gc_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gc_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-linux_process_rss_swap_timeline", + "type": "skill", + "target": "linux_process_rss_swap_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_process_rss_swap_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "rss_swap_peaks" + ], + "semantic_step": "rss_swap_peaks", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-lmk_analysis", + "type": "skill", + "target": "lmk_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/lmk_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "data_check", + "lmk_overview" + ], + "semantic_step": "lmk_overview", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-lmk_kill_attribution", + "type": "skill", + "target": "lmk_kill_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/lmk_kill_attribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "lmk_events" + ], + "semantic_step": "lmk_events", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "definition-logcat_analysis", + "type": "skill", + "target": "logcat_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/logcat_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_analysis", + "type": "skill", + "target": "memory_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/memory_analysis.skill.yaml", + "parameters": { + "gc_count_critical": 100, + "gc_count_warning": 50, + "gc_total_time_critical_ms": 2000, + "main_thread_gc_critical": 10, + "single_gc_warning_ms": 50 + }, + "required_steps": [ + "get_process" + ], + "semantic_step": "get_process", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_growth_detector", + "type": "skill", + "target": "memory_growth_detector", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_growth_detector.skill.yaml", + "parameters": { + "growth_warning_mb": 50, + "growth_pct_min_mb": 5, + "growth_warning_pct": 20, + "growth_critical_pct": 50, + "jump_warning_mb": 10, + "peak_avg_warning_ratio": 2 + }, + "required_steps": [ + "memory_growth" + ], + "semantic_step": "memory_growth", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_module", + "type": "skill", + "target": "memory_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/memory_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "memory_overview" + ], + "semantic_step": "memory_overview", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_pressure_in_range", + "type": "skill", + "target": "memory_pressure_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_pressure_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "memory_pressure_analysis" + ], + "semantic_step": "memory_pressure_analysis", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_rss_high_watermark", + "type": "skill", + "target": "memory_rss_high_watermark", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_rss_high_watermark.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "rss_peaks" + ], + "semantic_step": "rss_peaks", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-native_heap_breakdown", + "type": "skill", + "target": "native_heap_breakdown", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/native_heap_breakdown.skill.yaml", + "parameters": {}, + "required_steps": [ + "native_heap_hotspots" + ], + "semantic_step": "native_heap_hotspots", + "limitation_reason": "The fixture does not contain heapprofd allocation packets.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-oom_adjuster_score_timeline", + "type": "skill", + "target": "oom_adjuster_score_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/oom_adjuster_score_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "oom_intervals" + ], + "semantic_step": "oom_intervals", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "strategy-memory", + "type": "strategy", + "target": "memory", + "query": "内存", + "expected_strategy": "memory", + "source_file": "backend/strategies/memory.strategy.md", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + } + ] + }, + "case_dir": "Trace/constructed/memory-gc-pressure", + "manifest_path": "Trace/constructed/memory-gc-pressure/case.json" + }, + { + "schema_version": 1, + "id": "power-thermal", + "kind": "constructed", + "title": "Power and thermal throttling", + "description": "Deterministic power and thermal throttling signals over the android-scroll-customer real base trace.", + "scene": "power", + "tags": [ + "android", + "constructed", + "power" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "d821be29b55da321f6c2578846bd8dc7acadee49d6be15757d2709c783b24246", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "power-thermal-v1", + "output": "Trace/.generated/constructed/power-thermal/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_dvfs_counter_stats", + "battery_charge_timeline", + "battery_drain_attribution", + "battery_drain_rate_summary", + "gpu_power_state_analysis", + "mali_gpu_power_state", + "power_consumption_overview", + "power_module", + "power_rails_energy_breakdown", + "suspend_wakeup_analysis", + "thermal_module", + "thermal_predictor", + "thermal_throttling", + "thermal_throttling_chain", + "wakeup_frequency_summary", + "wattson_app_startup_power", + "wattson_rails_power_breakdown", + "wattson_thread_power_attribution" + ], + "strategies": [ + "power" + ], + "expectations": [ + { + "id": "execute-android_dvfs_counter_stats", + "type": "skill", + "target": "android_dvfs_counter_stats", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_dvfs_counter_stats.skill.yaml", + "parameters": {}, + "required_steps": [ + "dvfs_stats" + ], + "semantic_step": "dvfs_stats", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_charge_timeline", + "type": "skill", + "target": "battery_charge_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_charge_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "battery_samples" + ], + "semantic_step": "battery_samples", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_drain_attribution", + "type": "skill", + "target": "battery_drain_attribution", + "mode": "semantic", + "source_file": "backend/skills/composite/battery_drain_attribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "battery_drain_rate" + ], + "semantic_step": "battery_drain_rate", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_drain_rate_summary", + "type": "skill", + "target": "battery_drain_rate_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_drain_rate_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "drain_rate" + ], + "semantic_step": "drain_rate", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "definition-gpu_power_state_analysis", + "type": "skill", + "target": "gpu_power_state_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_power_state_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-mali_gpu_power_state", + "type": "skill", + "target": "mali_gpu_power_state", + "mode": "semantic", + "source_file": "backend/skills/atomic/mali_gpu_power_state.skill.yaml", + "parameters": {}, + "required_steps": [ + "mali_state" + ], + "semantic_step": "mali_state", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_consumption_overview", + "type": "skill", + "target": "power_consumption_overview", + "mode": "semantic", + "source_file": "backend/skills/composite/power_consumption_overview.skill.yaml", + "parameters": {}, + "required_steps": [ + "hardware_rails" + ], + "semantic_step": "hardware_rails", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_module", + "type": "skill", + "target": "power_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/power_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_overview" + ], + "semantic_step": "wakelock_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_rails_energy_breakdown", + "type": "skill", + "target": "power_rails_energy_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/power_rails_energy_breakdown.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "rail_energy" + ], + "semantic_step": "rail_energy", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-suspend_wakeup_analysis", + "type": "skill", + "target": "suspend_wakeup_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/suspend_wakeup_analysis.skill.yaml", + "parameters": { + "frequent_wakeup_critical": 50, + "frequent_wakeup_warning": 20, + "abort_pct_critical": 30, + "abort_pct_warning": 10, + "low_suspend_pct": 30 + }, + "required_steps": [ + "check_suspend_data", + "check_wakeup_data", + "check_suspend_slices", + "suspend_overview" + ], + "semantic_step": "suspend_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_module", + "type": "skill", + "target": "thermal_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/thermal_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "temperature_overview" + ], + "semantic_step": "temperature_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "definition-thermal_predictor", + "type": "skill", + "target": "thermal_predictor", + "mode": "definition", + "source_file": "backend/skills/atomic/thermal_predictor.skill.yaml", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_throttling", + "type": "skill", + "target": "thermal_throttling", + "mode": "semantic", + "source_file": "backend/skills/composite/thermal_throttling.skill.yaml", + "parameters": { + "enable_expert_probes": true, + "thermal_predictor_high_drop_pct": 30, + "thermal_predictor_medium_drop_pct": 15, + "thermal_predictor_high_core_ratio_pct": 50, + "thermal_predictor_medium_core_ratio_pct": 25, + "thermal_predictor_core_drop_threshold_pct": 30, + "gpu_transition_threshold_pct": 12, + "gpu_downshift_warning_pct": 25 + }, + "required_steps": [ + "data_check", + "expert_analysis_window", + "thermal_predictor_probe", + "thermal_overview" + ], + "semantic_step": "thermal_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_throttling_chain", + "type": "skill", + "target": "thermal_throttling_chain", + "mode": "semantic", + "source_file": "backend/skills/composite/thermal_throttling_chain.skill.yaml", + "parameters": {}, + "required_steps": [ + "thermal" + ], + "semantic_step": "thermal", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wakeup_frequency_summary", + "type": "skill", + "target": "wakeup_frequency_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/wakeup_frequency_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakeup_frequency" + ], + "semantic_step": "wakeup_frequency", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_app_startup_power", + "type": "skill", + "target": "wattson_app_startup_power", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/wattson_app_startup_power.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_power" + ], + "semantic_step": "startup_power", + "limitation_reason": "Wattson startup attribution is device-model gated and unsupported by this base device.", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_rails_power_breakdown", + "type": "skill", + "target": "wattson_rails_power_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/wattson_rails_power_breakdown.skill.yaml", + "parameters": { + "top_n": 20 + }, + "required_steps": [ + "rails_aggregation" + ], + "semantic_step": "rails_aggregation", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_thread_power_attribution", + "type": "skill", + "target": "wattson_thread_power_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/wattson_thread_power_attribution.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "thread_attribution" + ], + "semantic_step": "thread_attribution", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "strategy-power", + "type": "strategy", + "target": "power", + "query": "功耗", + "expected_strategy": "power", + "source_file": "backend/strategies/power.strategy.md", + "required_marker": "SmartPerfetto::CASE::power-thermal" + } + ] + }, + "case_dir": "Trace/constructed/power-thermal", + "manifest_path": "Trace/constructed/power-thermal/case.json" + }, + { + "schema_version": 1, + "id": "rendering-jank", + "kind": "constructed", + "title": "Rendering pipeline jank", + "description": "Deterministic rendering pipeline jank signals over the android-scroll-customer real base trace.", + "scene": "scrolling", + "tags": [ + "android", + "constructed", + "scrolling" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "3545492fc56cacbc73022aae8db4528c9c8f3fe54df733ff6273d95a416b2f55", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "rendering-jank-v1", + "output": "Trace/.generated/constructed/rendering-jank/trace.pftrace" + }, + "coverage": { + "skills": [ + "app_frame_production", + "buffer_transaction_lifecycle", + "choreographer_module", + "chrome_scroll_jank_frame_timeline", + "consumer_jank_detection", + "flutter_scrolling_analysis", + "fpsgo_analysis", + "frame_blocking_calls", + "frame_overrun_summary", + "frame_pipeline_variance", + "frame_production_gap", + "frame_ui_time_breakdown", + "game_fps_analysis", + "game_main_loop_jank", + "gl_standalone_swap_jank", + "gpu_render_in_range", + "input_to_frame_latency", + "jank_frame_detail", + "pipeline_flutter_textureview", + "pipeline_hardware_buffer_renderer", + "pipeline_textureview_standard", + "pipeline_webview_textureview_custom", + "present_fence_timing", + "render_pipeline_latency", + "render_thread_slices", + "rendering_pipeline_detection", + "rn_bridge_to_frame_jank", + "rn_fabric_render_jank", + "scroll_response_latency", + "scroll_session_analysis", + "scrolling_analysis", + "sf_composition_in_range", + "sf_frame_consumption", + "sf_layer_count_in_range", + "surfaceflinger_analysis", + "surfaceflinger_module", + "textureview_producer_frame_timing", + "vrr_detection", + "vsync_alignment_in_range", + "vsync_config", + "vsync_period_detection", + "vsync_phase_alignment", + "webview_drawfunctor_jank_chain" + ], + "strategies": [ + "game", + "scroll_response", + "scrolling" + ], + "expectations": [ + { + "id": "execute-app_frame_production", + "type": "skill", + "target": "app_frame_production", + "mode": "semantic", + "source_file": "backend/skills/atomic/app_frame_production.skill.yaml", + "parameters": {}, + "required_steps": [ + "app_production_stats" + ], + "semantic_step": "app_production_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-buffer_transaction_lifecycle", + "type": "skill", + "target": "buffer_transaction_lifecycle", + "mode": "semantic", + "source_file": "backend/skills/atomic/buffer_transaction_lifecycle.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "apply_transaction_summary" + ], + "semantic_step": "apply_transaction_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-choreographer_module", + "type": "skill", + "target": "choreographer_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/choreographer_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "doframe_overview" + ], + "semantic_step": "doframe_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-chrome_scroll_jank_frame_timeline", + "type": "skill", + "target": "chrome_scroll_jank_frame_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/chrome_scroll_jank_frame_timeline.skill.yaml", + "parameters": { + "min_janky_frames": 1 + }, + "required_steps": [ + "chrome_trace_availability" + ], + "semantic_step": "chrome_trace_availability", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-consumer_jank_detection", + "type": "skill", + "target": "consumer_jank_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/consumer_jank_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_config", + "consumer_jank_frames" + ], + "semantic_step": "consumer_jank_frames", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-flutter_scrolling_analysis", + "type": "skill", + "target": "flutter_scrolling_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/flutter_scrolling_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "flutter_frame_overview" + ], + "semantic_step": "flutter_frame_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-fpsgo_analysis", + "type": "skill", + "target": "fpsgo_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/fpsgo_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-frame_blocking_calls", + "type": "skill", + "target": "frame_blocking_calls", + "mode": "definition", + "source_file": "backend/skills/atomic/frame_blocking_calls.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_overrun_summary", + "type": "skill", + "target": "frame_overrun_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_overrun_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "overrun_frames" + ], + "semantic_step": "overrun_frames", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-frame_pipeline_variance", + "type": "skill", + "target": "frame_pipeline_variance", + "mode": "definition", + "source_file": "backend/skills/atomic/frame_pipeline_variance.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_production_gap", + "type": "skill", + "target": "frame_production_gap", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_production_gap.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "gap_summary" + ], + "semantic_step": "gap_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_ui_time_breakdown", + "type": "skill", + "target": "frame_ui_time_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_ui_time_breakdown.skill.yaml", + "parameters": {}, + "required_steps": [ + "ui_time" + ], + "semantic_step": "ui_time", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-game_fps_analysis", + "type": "skill", + "target": "game_fps_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/game_fps_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_target_fps" + ], + "semantic_step": "detect_target_fps", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-game_main_loop_jank", + "type": "skill", + "target": "game_main_loop_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/game_main_loop_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "engine_loop_summary" + ], + "semantic_step": "engine_loop_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-gl_standalone_swap_jank", + "type": "skill", + "target": "gl_standalone_swap_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/gl_standalone_swap_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "swap_interval_summary" + ], + "semantic_step": "swap_interval_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-gpu_render_in_range", + "type": "skill", + "target": "gpu_render_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_render_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-input_to_frame_latency", + "type": "skill", + "target": "input_to_frame_latency", + "mode": "semantic", + "source_file": "backend/skills/atomic/input_to_frame_latency.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_period" + ], + "semantic_step": "vsync_period", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-jank_frame_detail", + "type": "skill", + "target": "jank_frame_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/jank_frame_detail.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture", + "pid": 700000, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "monitor_contention_check", + "gc_table_check", + "gpu_table_check", + "binder_table_check", + "quadrant_analysis" + ], + "semantic_step": "quadrant_analysis", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_flutter_textureview", + "type": "skill", + "target": "pipeline_flutter_textureview", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_textureview.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_hardware_buffer_renderer", + "type": "skill", + "target": "pipeline_hardware_buffer_renderer", + "mode": "definition", + "source_file": "backend/skills/pipelines/hardware_buffer_renderer.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_textureview_standard", + "type": "skill", + "target": "pipeline_textureview_standard", + "mode": "definition", + "source_file": "backend/skills/pipelines/textureview_standard.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_webview_textureview_custom", + "type": "skill", + "target": "pipeline_webview_textureview_custom", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_textureview_custom.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-present_fence_timing", + "type": "skill", + "target": "present_fence_timing", + "mode": "semantic", + "source_file": "backend/skills/atomic/present_fence_timing.skill.yaml", + "parameters": {}, + "required_steps": [ + "present_fence_stats" + ], + "semantic_step": "present_fence_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-render_pipeline_latency", + "type": "skill", + "target": "render_pipeline_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/render_pipeline_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-render_thread_slices", + "type": "skill", + "target": "render_thread_slices", + "mode": "semantic", + "source_file": "backend/skills/atomic/render_thread_slices.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "render_slices" + ], + "semantic_step": "render_slices", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rendering_pipeline_detection", + "type": "skill", + "target": "rendering_pipeline_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/rendering_pipeline_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "thread_signals" + ], + "semantic_step": "thread_signals", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rn_bridge_to_frame_jank", + "type": "skill", + "target": "rn_bridge_to_frame_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/rn_bridge_to_frame_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "rn_bridge_frame_overlap" + ], + "semantic_step": "rn_bridge_frame_overlap", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rn_fabric_render_jank", + "type": "skill", + "target": "rn_fabric_render_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/rn_fabric_render_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "fabric_frame_overlap" + ], + "semantic_step": "fabric_frame_overlap", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-scroll_response_latency", + "type": "skill", + "target": "scroll_response_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/scroll_response_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-scroll_session_analysis", + "type": "skill", + "target": "scroll_session_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/scroll_session_analysis.skill.yaml", + "parameters": { + "session_id": 1, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "full_session_stats" + ], + "semantic_step": "full_session_stats", + "limitation_reason": "The generator does not yet emit Winscope android_input_event packets.", + "expected_error": "Trace is missing required tables: android_input_event", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-scrolling_analysis", + "type": "skill", + "target": "scrolling_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/scrolling_analysis.skill.yaml", + "parameters": { + "enable_expert_probes": true, + "frame_variance_probe_min_janky_frames": 1, + "frame_variance_transition_threshold_ms": 1, + "input_handling_budget_ratio": 0.5, + "input_event_backlog_threshold": 3 + }, + "required_steps": [ + "init_cpu_topology", + "frame_timeline_check", + "vsync_config" + ], + "semantic_step": "vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-sf_composition_in_range", + "type": "skill", + "target": "sf_composition_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/sf_composition_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-sf_frame_consumption", + "type": "skill", + "target": "sf_frame_consumption", + "mode": "semantic", + "source_file": "backend/skills/atomic/sf_frame_consumption.skill.yaml", + "parameters": {}, + "required_steps": [ + "sf_consumption_stats" + ], + "semantic_step": "sf_consumption_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-sf_layer_count_in_range", + "type": "skill", + "target": "sf_layer_count_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/sf_layer_count_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "layer_summary" + ], + "semantic_step": "layer_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-surfaceflinger_analysis", + "type": "skill", + "target": "surfaceflinger_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/surfaceflinger_analysis.skill.yaml", + "parameters": { + "slow_composition_multiplier": 1.5, + "composition_rating_poor_ms": 12, + "composition_rating_fair_ms": 8, + "composition_rating_good_ms": 4, + "long_fence_threshold_ms": 10, + "fence_critical_ms": 16, + "fence_warning_ms": 8, + "slow_pct_threshold": 10, + "gpu_comp_ratio_threshold": 0.5 + }, + "required_steps": [ + "data_check", + "vsync_config" + ], + "semantic_step": "vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-surfaceflinger_module", + "type": "skill", + "target": "surfaceflinger_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/surfaceflinger_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "frame_overview" + ], + "semantic_step": "frame_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-textureview_producer_frame_timing", + "type": "skill", + "target": "textureview_producer_frame_timing", + "mode": "semantic", + "source_file": "backend/skills/atomic/textureview_producer_frame_timing.skill.yaml", + "parameters": {}, + "required_steps": [ + "textureview_signal_summary" + ], + "semantic_step": "textureview_signal_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vrr_detection", + "type": "skill", + "target": "vrr_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/vrr_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_interval_distribution" + ], + "semantic_step": "vsync_interval_distribution", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-vsync_alignment_in_range", + "type": "skill", + "target": "vsync_alignment_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/vsync_alignment_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_config", + "type": "skill", + "target": "vsync_config", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_config.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_vsync_config" + ], + "semantic_step": "detect_vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_period_detection", + "type": "skill", + "target": "vsync_period_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_period_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_vsync_period" + ], + "semantic_step": "detect_vsync_period", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_phase_alignment", + "type": "skill", + "target": "vsync_phase_alignment", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_phase_alignment.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_timeline" + ], + "semantic_step": "vsync_timeline", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-webview_drawfunctor_jank_chain", + "type": "skill", + "target": "webview_drawfunctor_jank_chain", + "mode": "semantic", + "source_file": "backend/skills/composite/webview_drawfunctor_jank_chain.skill.yaml", + "parameters": {}, + "required_steps": [ + "drawfunctor_signal_summary" + ], + "semantic_step": "drawfunctor_signal_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-game", + "type": "strategy", + "target": "game", + "query": "游戏", + "expected_strategy": "game", + "source_file": "backend/strategies/game.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-scroll_response", + "type": "strategy", + "target": "scroll_response", + "query": "滑动响应", + "expected_strategy": "scroll_response", + "source_file": "backend/strategies/scroll-response.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-scrolling", + "type": "strategy", + "target": "scrolling", + "query": "滑动", + "expected_strategy": "scrolling", + "source_file": "backend/strategies/scrolling.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + } + ] + }, + "case_dir": "Trace/constructed/rendering-jank", + "manifest_path": "Trace/constructed/rendering-jank/case.json" + }, + { + "schema_version": 1, + "id": "scheduler-cpu-contention", + "kind": "constructed", + "title": "Scheduler and CPU contention", + "description": "Deterministic scheduler and cpu contention signals over the android-scroll-customer real base trace.", + "scene": "cpu", + "tags": [ + "android", + "constructed", + "cpu" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "f28f29a672ea9aa39c16533feeb06e003b1ae29084374364250fd7736ac33c51", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "scheduler-cpu-contention-v1", + "output": "Trace/.generated/constructed/scheduler-cpu-contention/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_job_scheduler_events", + "cache_miss_impact", + "cpu_analysis", + "cpu_cluster_load_in_range", + "cpu_cluster_mapping_view", + "cpu_freq_residency_summary", + "cpu_freq_timeline", + "cpu_idle_analysis", + "cpu_idle_state_residency", + "cpu_load_in_range", + "cpu_module", + "cpu_process_utilization_period", + "cpu_profiling", + "cpu_slice_analysis", + "cpu_thread_utilization_period", + "cpu_throttling_in_range", + "cpu_time_per_frame", + "cpu_topology_detection", + "cpu_topology_view", + "cpu_utilization_per_period", + "futex_wait_distribution", + "irq_analysis", + "linux_irq_summary", + "linux_runqueue_depth_timeline", + "linux_sched_latency_distribution", + "main_thread_sched_latency_in_range", + "process_slice_cpu_hotspots", + "sched_latency_in_range", + "scheduler_module", + "scheduling_analysis", + "screen_off_background_cpu_attribution", + "selection_range_cpu_sched_summary", + "system_load_in_range", + "task_migration_in_range", + "thread_affinity_violation", + "util_tracking_analysis" + ], + "strategies": [], + "expectations": [ + { + "id": "execute-android_job_scheduler_events", + "type": "skill", + "target": "android_job_scheduler_events", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_job_scheduler_events.skill.yaml", + "parameters": {}, + "required_steps": [ + "job_events" + ], + "semantic_step": "job_events", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-cache_miss_impact", + "type": "skill", + "target": "cache_miss_impact", + "mode": "definition", + "source_file": "backend/skills/atomic/cache_miss_impact.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_analysis", + "type": "skill", + "target": "cpu_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/cpu_analysis.skill.yaml", + "parameters": { + "big_core_threshold_pct": 30, + "running_threshold_pct": 50, + "sched_delay_critical_ms": 16, + "blocked_function_threshold_ms": 50, + "enable_expert_probes": true, + "affinity_probe_migration_ratio_threshold_pct": 25, + "affinity_violation_warning_threshold_pct": 35, + "cache_miss_high_avg_delta_threshold": 500000, + "cache_miss_medium_avg_delta_threshold": 100000 + }, + "required_steps": [ + "init_cpu_topology", + "get_process" + ], + "semantic_step": "get_process", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_cluster_load_in_range", + "type": "skill", + "target": "cpu_cluster_load_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_cluster_load_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cluster_load" + ], + "semantic_step": "cluster_load", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_cluster_mapping_view", + "type": "skill", + "target": "cpu_cluster_mapping_view", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_cluster_mapping_view.skill.yaml", + "parameters": {}, + "required_steps": [ + "cluster_mapping" + ], + "semantic_step": "cluster_mapping", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_freq_residency_summary", + "type": "skill", + "target": "cpu_freq_residency_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_freq_residency_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "freq_residency" + ], + "semantic_step": "freq_residency", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_freq_timeline", + "type": "skill", + "target": "cpu_freq_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_freq_timeline.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cpu_freq_changes", + "cpu_freq_summary" + ], + "semantic_step": "cpu_freq_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-cpu_idle_analysis", + "type": "skill", + "target": "cpu_idle_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/cpu_idle_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_idle_state_residency", + "type": "skill", + "target": "cpu_idle_state_residency", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_idle_state_residency.skill.yaml", + "parameters": {}, + "required_steps": [ + "idle_residency" + ], + "semantic_step": "idle_residency", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_load_in_range", + "type": "skill", + "target": "cpu_load_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_load_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cpu_utilization" + ], + "semantic_step": "cpu_utilization", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_module", + "type": "skill", + "target": "cpu_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/cpu_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "frequency_overview" + ], + "semantic_step": "frequency_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_process_utilization_period", + "type": "skill", + "target": "cpu_process_utilization_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_process_utilization_period.skill.yaml", + "parameters": {}, + "required_steps": [ + "process_util" + ], + "semantic_step": "process_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_profiling", + "type": "skill", + "target": "cpu_profiling", + "mode": "semantic", + "source_file": "backend/skills/deep/cpu_profiling.skill.yaml", + "parameters": { + "min_runtime_ms": 1 + }, + "required_steps": [ + "init_cpu_topology", + "cpu_usage_overview" + ], + "semantic_step": "cpu_usage_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_slice_analysis", + "type": "skill", + "target": "cpu_slice_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_slice_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "cpu_time_by_core" + ], + "semantic_step": "cpu_time_by_core", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_thread_utilization_period", + "type": "skill", + "target": "cpu_thread_utilization_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_thread_utilization_period.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "thread_util" + ], + "semantic_step": "thread_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_throttling_in_range", + "type": "skill", + "target": "cpu_throttling_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_throttling_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "throttle_detection" + ], + "semantic_step": "throttle_detection", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_time_per_frame", + "type": "skill", + "target": "cpu_time_per_frame", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_time_per_frame.skill.yaml", + "parameters": {}, + "required_steps": [ + "cpu_time" + ], + "semantic_step": "cpu_time", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_topology_detection", + "type": "skill", + "target": "cpu_topology_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_topology_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "detect_cpu_clusters" + ], + "semantic_step": "detect_cpu_clusters", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_topology_view", + "type": "skill", + "target": "cpu_topology_view", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_topology_view.skill.yaml", + "parameters": {}, + "required_steps": [ + "inspect_existing_topology_object", + "drop_existing_topology_view", + "drop_existing_topology_table", + "create_topology_view", + "read_topology" + ], + "semantic_step": "read_topology", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_utilization_per_period", + "type": "skill", + "target": "cpu_utilization_per_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_utilization_per_period.skill.yaml", + "parameters": {}, + "required_steps": [ + "system_util" + ], + "semantic_step": "system_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-futex_wait_distribution", + "type": "skill", + "target": "futex_wait_distribution", + "mode": "definition", + "source_file": "backend/skills/atomic/futex_wait_distribution.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-irq_analysis", + "type": "skill", + "target": "irq_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/irq_analysis.skill.yaml", + "parameters": { + "hard_irq_long_threshold_us": 1000, + "soft_irq_long_threshold_us": 10000, + "irq_rate_heavy_threshold": 10000, + "irq_dur_heavy_threshold_ms": 50 + }, + "required_steps": [ + "data_check", + "irq_overview" + ], + "semantic_step": "irq_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_irq_summary", + "type": "skill", + "target": "linux_irq_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_irq_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "irq_summary" + ], + "semantic_step": "irq_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_runqueue_depth_timeline", + "type": "skill", + "target": "linux_runqueue_depth_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_runqueue_depth_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "runqueue_pressure_summary" + ], + "semantic_step": "runqueue_pressure_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_sched_latency_distribution", + "type": "skill", + "target": "linux_sched_latency_distribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_sched_latency_distribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "sched_latency_distribution" + ], + "semantic_step": "sched_latency_distribution", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-main_thread_sched_latency_in_range", + "type": "skill", + "target": "main_thread_sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-process_slice_cpu_hotspots", + "type": "skill", + "target": "process_slice_cpu_hotspots", + "mode": "definition", + "source_file": "backend/skills/atomic/process_slice_cpu_hotspots.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-sched_latency_in_range", + "type": "skill", + "target": "sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-scheduler_module", + "type": "skill", + "target": "scheduler_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/scheduler_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "runnable_analysis" + ], + "semantic_step": "runnable_analysis", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-scheduling_analysis", + "type": "skill", + "target": "scheduling_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/scheduling_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-screen_off_background_cpu_attribution", + "type": "skill", + "target": "screen_off_background_cpu_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/screen_off_background_cpu_attribution.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "screen_off_cpu" + ], + "semantic_step": "screen_off_cpu", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-selection_range_cpu_sched_summary", + "type": "skill", + "target": "selection_range_cpu_sched_summary", + "mode": "semantic", + "source_file": "backend/skills/composite/selection_range_cpu_sched_summary.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "running_thread_quadrants" + ], + "semantic_step": "running_thread_quadrants", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-system_load_in_range", + "type": "skill", + "target": "system_load_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/system_load_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-task_migration_in_range", + "type": "skill", + "target": "task_migration_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/task_migration_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "migration_analysis" + ], + "semantic_step": "migration_analysis", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-thread_affinity_violation", + "type": "skill", + "target": "thread_affinity_violation", + "mode": "definition", + "source_file": "backend/skills/atomic/thread_affinity_violation.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-util_tracking_analysis", + "type": "skill", + "target": "util_tracking_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/util_tracking_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + } + ] + }, + "case_dir": "Trace/constructed/scheduler-cpu-contention", + "manifest_path": "Trace/constructed/scheduler-cpu-contention/case.json" + }, + { + "schema_version": 1, + "id": "startup-lifecycle", + "kind": "constructed", + "title": "Startup and process lifecycle", + "description": "Deterministic startup and process lifecycle signals over the android-startup-heavy real base trace.", + "scene": "startup", + "tags": [ + "android", + "constructed", + "startup" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "884d969ba9a0e477e862c1178b25afca25c4a0c845cc77cc01db0371c4e40de7", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Xiaomi pandora base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-heavy", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "startup-lifecycle-v1", + "output": "Trace/.generated/constructed/startup-lifecycle/trace.pftrace" + }, + "coverage": { + "skills": [ + "app_process_starts_summary", + "launcher_module", + "startup_analysis", + "startup_binder_in_range", + "startup_binder_pool_analysis", + "startup_breakdown_in_range", + "startup_class_loading_in_range", + "startup_cpu_placement_timeline", + "startup_critical_tasks", + "startup_detail", + "startup_events_in_range", + "startup_freq_rampup", + "startup_gc_in_range", + "startup_hot_slice_states", + "startup_jit_analysis", + "startup_main_thread_binder_blocking_in_range", + "startup_main_thread_file_io_in_range", + "startup_main_thread_slices_in_range", + "startup_main_thread_states_in_range", + "startup_main_thread_sync_binder_in_range", + "startup_sched_latency_in_range", + "startup_slow_reasons", + "startup_thread_blocking_graph" + ], + "strategies": [ + "startup" + ], + "expectations": [ + { + "id": "execute-app_process_starts_summary", + "type": "skill", + "target": "app_process_starts_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/app_process_starts_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "process_starts" + ], + "semantic_step": "process_starts", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-launcher_module", + "type": "skill", + "target": "launcher_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/launcher_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "launcher_process" + ], + "semantic_step": "launcher_process", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_analysis", + "type": "skill", + "target": "startup_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/startup_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "get_startups", + "startup_quality" + ], + "semantic_step": "startup_quality", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_binder_in_range", + "type": "skill", + "target": "startup_binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_binder_pool_analysis", + "type": "skill", + "target": "startup_binder_pool_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_binder_pool_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_breakdown_in_range", + "type": "skill", + "target": "startup_breakdown_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_breakdown_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_class_loading_in_range", + "type": "skill", + "target": "startup_class_loading_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_class_loading_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_cpu_placement_timeline", + "type": "skill", + "target": "startup_cpu_placement_timeline", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_cpu_placement_timeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_critical_tasks", + "type": "skill", + "target": "startup_critical_tasks", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_critical_tasks.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_detail", + "type": "skill", + "target": "startup_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/startup_detail.skill.yaml", + "parameters": { + "startup_id": 1, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}", + "dur_ms": 1, + "package": "com.smartperfetto.fixture", + "startup_type": "cold" + }, + "required_steps": [ + "init_cpu_topology", + "startup_info" + ], + "semantic_step": "startup_info", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_events_in_range", + "type": "skill", + "target": "startup_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_freq_rampup", + "type": "skill", + "target": "startup_freq_rampup", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_freq_rampup.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_gc_in_range", + "type": "skill", + "target": "startup_gc_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_gc_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_hot_slice_states", + "type": "skill", + "target": "startup_hot_slice_states", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_hot_slice_states.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_jit_analysis", + "type": "skill", + "target": "startup_jit_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_jit_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_binder_blocking_in_range", + "type": "skill", + "target": "startup_main_thread_binder_blocking_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_binder_blocking_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_file_io_in_range", + "type": "skill", + "target": "startup_main_thread_file_io_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_file_io_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_slices_in_range", + "type": "skill", + "target": "startup_main_thread_slices_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_slices_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_states_in_range", + "type": "skill", + "target": "startup_main_thread_states_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_states_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_sync_binder_in_range", + "type": "skill", + "target": "startup_main_thread_sync_binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_sync_binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_sched_latency_in_range", + "type": "skill", + "target": "startup_sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_slow_reasons", + "type": "skill", + "target": "startup_slow_reasons", + "mode": "semantic", + "source_file": "backend/skills/atomic/startup_slow_reasons.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_overview" + ], + "semantic_step": "startup_overview", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_thread_blocking_graph", + "type": "skill", + "target": "startup_thread_blocking_graph", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_thread_blocking_graph.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "strategy-startup", + "type": "strategy", + "target": "startup", + "query": "启动", + "expected_strategy": "startup", + "source_file": "backend/strategies/startup.strategy.md", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + } + ] + }, + "case_dir": "Trace/constructed/startup-lifecycle", + "manifest_path": "Trace/constructed/startup-lifecycle/case.json" + } + ] +} diff --git a/Trace/constructed/README.md b/Trace/constructed/README.md new file mode 100644 index 000000000..e9f8777b3 --- /dev/null +++ b/Trace/constructed/README.md @@ -0,0 +1,19 @@ + +# Constructed Trace Cases + +Run `npm run trace:build` to materialize base-plus-overlay traces under `Trace/.generated/`. + +| Case | Scene | Android | Base | Coverage | +| --- | --- | --- | --- | --- | --- | +| [Binder, lock, and I/O blocking](./binder-io-blocking/) | io | Android 16 / API 36 (tested API 35-36) | android-startup-heavy | 25 target(s) | +| [Framework rendering pipeline signatures](./framework-pipelines/) | pipeline | Android 16 / API 36 (tested API 35-36) | android-startup-heavy | 33 target(s) | +| [General analysis and runtime contracts](./general-runtime-contracts/) | general | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 14 target(s) | +| [GPU workload and frequency](./gpu-workload/) | gpu | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 10 target(s) | +| [Input and interaction latency](./input-interaction-latency/) | interaction | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 13 target(s) | +| [Linux and Android system state](./linux-system-state/) | linux | Android 16 / API 36 (tested API 35-36) | android-startup-light | 5 target(s) | +| [Media, network, and camera pipeline](./media-network-camera/) | media | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 8 target(s) | +| [Memory and GC pressure](./memory-gc-pressure/) | memory | Android 16 / API 36 (tested API 35-36) | android-startup-light | 19 target(s) | +| [Power and thermal throttling](./power-thermal/) | power | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 19 target(s) | +| [Rendering pipeline jank](./rendering-jank/) | scrolling | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 46 target(s) | +| [Scheduler and CPU contention](./scheduler-cpu-contention/) | cpu | Android 16 / API 36 (tested API 35-36) | android-scroll-customer | 36 target(s) | +| [Startup and process lifecycle](./startup-lifecycle/) | startup | Android 16 / API 36 (tested API 35-36) | android-startup-heavy | 24 target(s) | diff --git a/Trace/constructed/_templates/scenario.example.json b/Trace/constructed/_templates/scenario.example.json new file mode 100644 index 000000000..64eddfede --- /dev/null +++ b/Trace/constructed/_templates/scenario.example.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + {"id": "app", "name": "com.example.fixture", "uid": 10999} + ], + "threads": [ + {"id": "main", "process": "app", "name": "main", "is_main": true} + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::replace-me" + } + ] +} diff --git a/Trace/constructed/binder-io-blocking/analysis/expected.json b/Trace/constructed/binder-io-blocking/analysis/expected.json new file mode 100644 index 000000000..aea811784 --- /dev/null +++ b/Trace/constructed/binder-io-blocking/analysis/expected.json @@ -0,0 +1,366 @@ +{ + "schema_version": 1, + "case_id": "binder-io-blocking", + "marker": "SmartPerfetto::CASE::binder-io-blocking", + "expectations": [ + { + "id": "execute-android_kernel_wakelock_summary", + "type": "skill", + "target": "android_kernel_wakelock_summary", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/android_kernel_wakelock_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_summary" + ], + "semantic_step": "wakelock_summary", + "limitation_reason": "The current generator does not emit android_kernel_wakelock counter tracks.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_analysis", + "type": "skill", + "target": "anr_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/anr_analysis.skill.yaml", + "parameters": { + "multi_anr_threshold": 3, + "multi_anr_span_seconds": 10, + "io_wait_threshold_ms": 500, + "uninterruptible_wait_threshold_ms": 500, + "enable_lock_probe": true, + "lock_contention_futex_p95_critical_ms": 20, + "lock_contention_mutex_p95_warning_ms": 10, + "enable_detail_analysis": true + }, + "required_steps": [ + "init_cpu_topology", + "get_anr_context", + "anr_detection" + ], + "semantic_step": "anr_detection", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-anr_context_in_range", + "type": "skill", + "target": "anr_context_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/anr_context_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_detail", + "type": "skill", + "target": "anr_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/anr_detail.skill.yaml", + "parameters": { + "anr_ts": 1, + "timeout_ns": 1, + "process_name": "com.smartperfetto.fixture", + "pid": 700000, + "upid": "${fixture_upid}", + "anr_type": "" + }, + "required_steps": [ + "init_cpu_topology", + "anr_info" + ], + "semantic_step": "anr_info", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_main_thread_blocking", + "type": "skill", + "target": "anr_main_thread_blocking", + "mode": "semantic", + "source_file": "backend/skills/atomic/anr_main_thread_blocking.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "main_thread_state" + ], + "semantic_step": "main_thread_state", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_analysis", + "type": "skill", + "target": "binder_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/binder_analysis.skill.yaml", + "parameters": { + "slow_binder_critical_ms": 50, + "slow_binder_warning_ms": 16, + "main_thread_txn_warning": 50, + "server_response_warning_ms": 20 + }, + "required_steps": [ + "check_binder" + ], + "semantic_step": "check_binder", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-binder_blocking_in_range", + "type": "skill", + "target": "binder_blocking_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/binder_blocking_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_detail", + "type": "skill", + "target": "binder_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/binder_detail.skill.yaml", + "parameters": { + "binder_ts": 1, + "binder_end_ts": "${trace_end}", + "dur_ms": 1, + "server_process": "", + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "init_cpu_topology", + "binder_info" + ], + "semantic_step": "binder_info", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-binder_in_range", + "type": "skill", + "target": "binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_module", + "type": "skill", + "target": "binder_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/binder_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "binder_summary" + ], + "semantic_step": "binder_summary", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_root_cause", + "type": "skill", + "target": "binder_root_cause", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/binder_root_cause.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "slow_binder_breakdown" + ], + "semantic_step": "slow_binder_breakdown", + "limitation_reason": "The fixture has blocking slices but no kernel Binder transaction packet chain.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_storm_detection", + "type": "skill", + "target": "binder_storm_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/binder_storm_detection.skill.yaml", + "parameters": { + "threshold": 50 + }, + "required_steps": [ + "binder_density" + ], + "semantic_step": "binder_density", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-block_io_analysis", + "type": "skill", + "target": "block_io_analysis", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/block_io_analysis.skill.yaml", + "parameters": { + "min_queue_depth": 5 + }, + "required_steps": [ + "data_check", + "io_overview" + ], + "semantic_step": "io_overview", + "limitation_reason": "The fixture does not yet emit block_rq ftrace events.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-blocking_chain_analysis", + "type": "skill", + "target": "blocking_chain_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/blocking_chain_analysis.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "thread_state_distribution" + ], + "semantic_step": "thread_state_distribution", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-filesystem_module", + "type": "skill", + "target": "filesystem_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/filesystem_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "io_overview" + ], + "semantic_step": "io_overview", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-io_pressure", + "type": "skill", + "target": "io_pressure", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/io_pressure.skill.yaml", + "parameters": { + "min_duration_ms": 1, + "max_items": 20, + "long_io_threshold_ms": 10, + "critical_io_wait_ms": 5000, + "warning_io_wait_ms": 1000 + }, + "required_steps": [ + "data_check", + "io_wait_overview" + ], + "semantic_step": "io_wait_overview", + "limitation_reason": "The fixture does not yet emit PSI I/O pressure counters.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_binder_wait", + "type": "skill", + "target": "lock_binder_wait", + "mode": "semantic", + "source_file": "backend/skills/composite/lock_binder_wait.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "main_thread_blocking_chain" + ], + "semantic_step": "main_thread_blocking_chain", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_analysis", + "type": "skill", + "target": "lock_contention_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/lock_contention_analysis.skill.yaml", + "parameters": { + "min_duration_ms": 10 + }, + "required_steps": [ + "data_check", + "owner_contention_events" + ], + "semantic_step": "owner_contention_events", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_in_range", + "type": "skill", + "target": "lock_contention_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/lock_contention_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "monitor_contentions" + ], + "semantic_step": "monitor_contentions", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_module", + "type": "skill", + "target": "lock_contention_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/lock_contention_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "lock_contention_overview" + ], + "semantic_step": "lock_contention_overview", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-main_thread_file_io_in_range", + "type": "skill", + "target": "main_thread_file_io_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_file_io_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-page_fault_in_range", + "type": "skill", + "target": "page_fault_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/page_fault_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-wakelock_tracking", + "type": "skill", + "target": "wakelock_tracking", + "mode": "semantic", + "source_file": "backend/skills/atomic/wakelock_tracking.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_events" + ], + "semantic_step": "wakelock_events", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "strategy-anr", + "type": "strategy", + "target": "anr", + "query": "anr", + "expected_strategy": "anr", + "source_file": "backend/strategies/anr.strategy.md", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "strategy-io", + "type": "strategy", + "target": "io", + "query": "io", + "expected_strategy": "io", + "source_file": "backend/strategies/io.strategy.md", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + } + ] +} diff --git a/Trace/constructed/binder-io-blocking/case.json b/Trace/constructed/binder-io-blocking/case.json new file mode 100644 index 000000000..9cb49c42c --- /dev/null +++ b/Trace/constructed/binder-io-blocking/case.json @@ -0,0 +1,445 @@ +{ + "schema_version": 1, + "id": "binder-io-blocking", + "kind": "constructed", + "title": "Binder, lock, and I/O blocking", + "description": "Deterministic binder, lock, and i/o blocking signals over the android-startup-heavy real base trace.", + "scene": "io", + "tags": [ + "android", + "constructed", + "io" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "e3d9f69a6a1009f685c8abcb70951c4d42f1186db2f63dcba026b0058d848866", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Xiaomi pandora base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-heavy", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "binder-io-blocking-v1", + "output": "Trace/.generated/constructed/binder-io-blocking/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_kernel_wakelock_summary", + "anr_analysis", + "anr_context_in_range", + "anr_detail", + "anr_main_thread_blocking", + "binder_analysis", + "binder_blocking_in_range", + "binder_detail", + "binder_in_range", + "binder_module", + "binder_root_cause", + "binder_storm_detection", + "block_io_analysis", + "blocking_chain_analysis", + "filesystem_module", + "io_pressure", + "lock_binder_wait", + "lock_contention_analysis", + "lock_contention_in_range", + "lock_contention_module", + "main_thread_file_io_in_range", + "page_fault_in_range", + "wakelock_tracking" + ], + "strategies": [ + "anr", + "io" + ], + "expectations": [ + { + "id": "execute-android_kernel_wakelock_summary", + "type": "skill", + "target": "android_kernel_wakelock_summary", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/android_kernel_wakelock_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_summary" + ], + "semantic_step": "wakelock_summary", + "limitation_reason": "The current generator does not emit android_kernel_wakelock counter tracks.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_analysis", + "type": "skill", + "target": "anr_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/anr_analysis.skill.yaml", + "parameters": { + "multi_anr_threshold": 3, + "multi_anr_span_seconds": 10, + "io_wait_threshold_ms": 500, + "uninterruptible_wait_threshold_ms": 500, + "enable_lock_probe": true, + "lock_contention_futex_p95_critical_ms": 20, + "lock_contention_mutex_p95_warning_ms": 10, + "enable_detail_analysis": true + }, + "required_steps": [ + "init_cpu_topology", + "get_anr_context", + "anr_detection" + ], + "semantic_step": "anr_detection", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-anr_context_in_range", + "type": "skill", + "target": "anr_context_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/anr_context_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_detail", + "type": "skill", + "target": "anr_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/anr_detail.skill.yaml", + "parameters": { + "anr_ts": 1, + "timeout_ns": 1, + "process_name": "com.smartperfetto.fixture", + "pid": 700000, + "upid": "${fixture_upid}", + "anr_type": "" + }, + "required_steps": [ + "init_cpu_topology", + "anr_info" + ], + "semantic_step": "anr_info", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-anr_main_thread_blocking", + "type": "skill", + "target": "anr_main_thread_blocking", + "mode": "semantic", + "source_file": "backend/skills/atomic/anr_main_thread_blocking.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "main_thread_state" + ], + "semantic_step": "main_thread_state", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_analysis", + "type": "skill", + "target": "binder_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/binder_analysis.skill.yaml", + "parameters": { + "slow_binder_critical_ms": 50, + "slow_binder_warning_ms": 16, + "main_thread_txn_warning": 50, + "server_response_warning_ms": 20 + }, + "required_steps": [ + "check_binder" + ], + "semantic_step": "check_binder", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-binder_blocking_in_range", + "type": "skill", + "target": "binder_blocking_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/binder_blocking_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_detail", + "type": "skill", + "target": "binder_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/binder_detail.skill.yaml", + "parameters": { + "binder_ts": 1, + "binder_end_ts": "${trace_end}", + "dur_ms": 1, + "server_process": "", + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "init_cpu_topology", + "binder_info" + ], + "semantic_step": "binder_info", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-binder_in_range", + "type": "skill", + "target": "binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_module", + "type": "skill", + "target": "binder_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/binder_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "binder_summary" + ], + "semantic_step": "binder_summary", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_root_cause", + "type": "skill", + "target": "binder_root_cause", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/binder_root_cause.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "slow_binder_breakdown" + ], + "semantic_step": "slow_binder_breakdown", + "limitation_reason": "The fixture has blocking slices but no kernel Binder transaction packet chain.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-binder_storm_detection", + "type": "skill", + "target": "binder_storm_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/binder_storm_detection.skill.yaml", + "parameters": { + "threshold": 50 + }, + "required_steps": [ + "binder_density" + ], + "semantic_step": "binder_density", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-block_io_analysis", + "type": "skill", + "target": "block_io_analysis", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/block_io_analysis.skill.yaml", + "parameters": { + "min_queue_depth": 5 + }, + "required_steps": [ + "data_check", + "io_overview" + ], + "semantic_step": "io_overview", + "limitation_reason": "The fixture does not yet emit block_rq ftrace events.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-blocking_chain_analysis", + "type": "skill", + "target": "blocking_chain_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/blocking_chain_analysis.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "thread_state_distribution" + ], + "semantic_step": "thread_state_distribution", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-filesystem_module", + "type": "skill", + "target": "filesystem_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/filesystem_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "io_overview" + ], + "semantic_step": "io_overview", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-io_pressure", + "type": "skill", + "target": "io_pressure", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/io_pressure.skill.yaml", + "parameters": { + "min_duration_ms": 1, + "max_items": 20, + "long_io_threshold_ms": 10, + "critical_io_wait_ms": 5000, + "warning_io_wait_ms": 1000 + }, + "required_steps": [ + "data_check", + "io_wait_overview" + ], + "semantic_step": "io_wait_overview", + "limitation_reason": "The fixture does not yet emit PSI I/O pressure counters.", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_binder_wait", + "type": "skill", + "target": "lock_binder_wait", + "mode": "semantic", + "source_file": "backend/skills/composite/lock_binder_wait.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture", + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "main_thread_blocking_chain" + ], + "semantic_step": "main_thread_blocking_chain", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_analysis", + "type": "skill", + "target": "lock_contention_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/lock_contention_analysis.skill.yaml", + "parameters": { + "min_duration_ms": 10 + }, + "required_steps": [ + "data_check", + "owner_contention_events" + ], + "semantic_step": "owner_contention_events", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_in_range", + "type": "skill", + "target": "lock_contention_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/lock_contention_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "monitor_contentions" + ], + "semantic_step": "monitor_contentions", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-lock_contention_module", + "type": "skill", + "target": "lock_contention_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/lock_contention_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "lock_contention_overview" + ], + "semantic_step": "lock_contention_overview", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-main_thread_file_io_in_range", + "type": "skill", + "target": "main_thread_file_io_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_file_io_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "definition-page_fault_in_range", + "type": "skill", + "target": "page_fault_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/page_fault_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "execute-wakelock_tracking", + "type": "skill", + "target": "wakelock_tracking", + "mode": "semantic", + "source_file": "backend/skills/atomic/wakelock_tracking.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_events" + ], + "semantic_step": "wakelock_events", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "strategy-anr", + "type": "strategy", + "target": "anr", + "query": "anr", + "expected_strategy": "anr", + "source_file": "backend/strategies/anr.strategy.md", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "id": "strategy-io", + "type": "strategy", + "target": "io", + "query": "io", + "expected_strategy": "io", + "source_file": "backend/strategies/io.strategy.md", + "required_marker": "SmartPerfetto::CASE::binder-io-blocking" + } + ] + } +} diff --git a/Trace/constructed/binder-io-blocking/scenario.json b/Trace/constructed/binder-io-blocking/scenario.json new file mode 100644 index 000000000..8c2db50b5 --- /dev/null +++ b/Trace/constructed/binder-io-blocking/scenario.json @@ -0,0 +1,153 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::binder-io-blocking" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "binder transaction" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "monitor contention" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "FileIO" + }, + { + "type": "atrace-slice", + "at_ns": "200000000", + "duration_ns": "22000000", + "process": "system", + "thread": "system-main", + "name": "binder transaction" + }, + { + "type": "atrace-slice", + "at_ns": "225000000", + "duration_ns": "23000000", + "process": "app", + "thread": "main", + "name": "monitor contention" + }, + { + "type": "atrace-slice", + "at_ns": "250000000", + "duration_ns": "24000000", + "process": "app", + "thread": "main", + "name": "FileIO::fsync" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.binder-io-blocking.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "D" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "D" + } + ] +} diff --git a/Trace/constructed/binder-io-blocking/trace.overlay.pftrace b/Trace/constructed/binder-io-blocking/trace.overlay.pftrace new file mode 100644 index 000000000..04ce3ca34 Binary files /dev/null and b/Trace/constructed/binder-io-blocking/trace.overlay.pftrace differ diff --git a/Trace/constructed/framework-pipelines/analysis/expected.json b/Trace/constructed/framework-pipelines/analysis/expected.json new file mode 100644 index 000000000..8f025a2f0 --- /dev/null +++ b/Trace/constructed/framework-pipelines/analysis/expected.json @@ -0,0 +1,324 @@ +{ + "schema_version": 1, + "case_id": "framework-pipelines", + "marker": "SmartPerfetto::CASE::framework-pipelines", + "expectations": [ + { + "id": "execute-ams_module", + "type": "skill", + "target": "ams_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/ams_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_timing" + ], + "semantic_step": "startup_timing", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-art_module", + "type": "skill", + "target": "art_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/art_module.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture" + }, + "required_steps": [ + "gc_overview" + ], + "semantic_step": "gc_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-code_pinpoint", + "type": "skill", + "target": "code_pinpoint", + "mode": "semantic", + "source_file": "backend/skills/composite/code_pinpoint.skill.yaml", + "parameters": {}, + "required_steps": [ + "hot_slices" + ], + "semantic_step": "hot_slices", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-compose_recomposition_hotspot", + "type": "skill", + "target": "compose_recomposition_hotspot", + "mode": "semantic", + "source_file": "backend/skills/atomic/compose_recomposition_hotspot.skill.yaml", + "parameters": {}, + "required_steps": [ + "frame_timeline_check", + "recomposition_overview" + ], + "semantic_step": "recomposition_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-pipeline_4feature_scoring", + "type": "skill", + "target": "pipeline_4feature_scoring", + "mode": "semantic", + "source_file": "backend/skills/atomic/pipeline_4feature_scoring.skill.yaml", + "parameters": {}, + "required_steps": [ + "producer_thread_signature" + ], + "semantic_step": "producer_thread_signature", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_pip_freeform", + "type": "skill", + "target": "pipeline_android_pip_freeform", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_pip_freeform.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_mixed", + "type": "skill", + "target": "pipeline_android_view_mixed", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_mixed.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_multi_window", + "type": "skill", + "target": "pipeline_android_view_multi_window", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_multi_window.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_software", + "type": "skill", + "target": "pipeline_android_view_software", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_software.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_standard_blast", + "type": "skill", + "target": "pipeline_android_view_standard_blast", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_standard_blast.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_standard_legacy", + "type": "skill", + "target": "pipeline_android_view_standard_legacy", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_standard_legacy.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_chrome_browser_viz", + "type": "skill", + "target": "pipeline_chrome_browser_viz", + "mode": "definition", + "source_file": "backend/skills/pipelines/chrome_browser_viz.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_compose_standard", + "type": "skill", + "target": "pipeline_compose_standard", + "mode": "definition", + "source_file": "backend/skills/pipelines/compose_standard.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_flutter_surfaceview_impeller", + "type": "skill", + "target": "pipeline_flutter_surfaceview_impeller", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_surfaceview_impeller.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_flutter_surfaceview_skia", + "type": "skill", + "target": "pipeline_flutter_surfaceview_skia", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_surfaceview_skia.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_game_engine", + "type": "skill", + "target": "pipeline_game_engine", + "mode": "definition", + "source_file": "backend/skills/pipelines/game_engine.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_imagereader_pipeline", + "type": "skill", + "target": "pipeline_imagereader_pipeline", + "mode": "definition", + "source_file": "backend/skills/pipelines/imagereader_pipeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-pipeline_key_slices_overlay", + "type": "skill", + "target": "pipeline_key_slices_overlay", + "mode": "semantic", + "source_file": "backend/skills/atomic/pipeline_key_slices_overlay.skill.yaml", + "parameters": { + "slice_names": "'Choreographer#doFrame','DrawFrame','queueBuffer','latchBuffer'" + }, + "required_steps": [ + "pipeline_key_slices_overlay" + ], + "semantic_step": "pipeline_key_slices_overlay", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_new_arch", + "type": "skill", + "target": "pipeline_rn_new_arch", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_new_arch.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_old_arch", + "type": "skill", + "target": "pipeline_rn_old_arch", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_old_arch.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_skia", + "type": "skill", + "target": "pipeline_rn_skia", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_skia.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_software_compositing", + "type": "skill", + "target": "pipeline_software_compositing", + "mode": "definition", + "source_file": "backend/skills/pipelines/software_compositing.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_surface_control_api", + "type": "skill", + "target": "pipeline_surface_control_api", + "mode": "definition", + "source_file": "backend/skills/pipelines/surface_control_api.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_surfaceview_blast", + "type": "skill", + "target": "pipeline_surfaceview_blast", + "mode": "definition", + "source_file": "backend/skills/pipelines/surfaceview_blast.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_variable_refresh_rate", + "type": "skill", + "target": "pipeline_variable_refresh_rate", + "mode": "definition", + "source_file": "backend/skills/pipelines/variable_refresh_rate.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_video_overlay_hwc", + "type": "skill", + "target": "pipeline_video_overlay_hwc", + "mode": "definition", + "source_file": "backend/skills/pipelines/video_overlay_hwc.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_gl_functor", + "type": "skill", + "target": "pipeline_webview_gl_functor", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_gl_functor.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_surface_control", + "type": "skill", + "target": "pipeline_webview_surface_control", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_surface_control.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_surfaceview_wrapper", + "type": "skill", + "target": "pipeline_webview_surfaceview_wrapper", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_surfaceview_wrapper.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-systemui_module", + "type": "skill", + "target": "systemui_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/systemui_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "systemui_process" + ], + "semantic_step": "systemui_process", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-third_party_module", + "type": "skill", + "target": "third_party_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/third_party_module.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture" + }, + "required_steps": [ + "thread_overview" + ], + "semantic_step": "thread_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-wms_module", + "type": "skill", + "target": "wms_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/wms_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "window_animation_overview" + ], + "semantic_step": "window_animation_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "strategy-pipeline", + "type": "strategy", + "target": "pipeline", + "query": "管线识别", + "expected_strategy": "pipeline", + "source_file": "backend/strategies/pipeline.strategy.md", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + } + ] +} diff --git a/Trace/constructed/framework-pipelines/case.json b/Trace/constructed/framework-pipelines/case.json new file mode 100644 index 000000000..b79025c55 --- /dev/null +++ b/Trace/constructed/framework-pipelines/case.json @@ -0,0 +1,411 @@ +{ + "schema_version": 1, + "id": "framework-pipelines", + "kind": "constructed", + "title": "Framework rendering pipeline signatures", + "description": "Deterministic framework rendering pipeline signatures signals over the android-startup-heavy real base trace.", + "scene": "pipeline", + "tags": [ + "android", + "constructed", + "pipeline" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "35c5cf1014ac0032188fb3da8d08899fa1ea3bb25ac6f34eb7f1c00645f3a9b3", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Xiaomi pandora base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-heavy", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "framework-pipelines-v1", + "output": "Trace/.generated/constructed/framework-pipelines/trace.pftrace" + }, + "coverage": { + "skills": [ + "ams_module", + "art_module", + "code_pinpoint", + "compose_recomposition_hotspot", + "pipeline_4feature_scoring", + "pipeline_android_pip_freeform", + "pipeline_android_view_mixed", + "pipeline_android_view_multi_window", + "pipeline_android_view_software", + "pipeline_android_view_standard_blast", + "pipeline_android_view_standard_legacy", + "pipeline_chrome_browser_viz", + "pipeline_compose_standard", + "pipeline_flutter_surfaceview_impeller", + "pipeline_flutter_surfaceview_skia", + "pipeline_game_engine", + "pipeline_imagereader_pipeline", + "pipeline_key_slices_overlay", + "pipeline_rn_new_arch", + "pipeline_rn_old_arch", + "pipeline_rn_skia", + "pipeline_software_compositing", + "pipeline_surface_control_api", + "pipeline_surfaceview_blast", + "pipeline_variable_refresh_rate", + "pipeline_video_overlay_hwc", + "pipeline_webview_gl_functor", + "pipeline_webview_surface_control", + "pipeline_webview_surfaceview_wrapper", + "systemui_module", + "third_party_module", + "wms_module" + ], + "strategies": [ + "pipeline" + ], + "expectations": [ + { + "id": "execute-ams_module", + "type": "skill", + "target": "ams_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/ams_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_timing" + ], + "semantic_step": "startup_timing", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-art_module", + "type": "skill", + "target": "art_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/art_module.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture" + }, + "required_steps": [ + "gc_overview" + ], + "semantic_step": "gc_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-code_pinpoint", + "type": "skill", + "target": "code_pinpoint", + "mode": "semantic", + "source_file": "backend/skills/composite/code_pinpoint.skill.yaml", + "parameters": {}, + "required_steps": [ + "hot_slices" + ], + "semantic_step": "hot_slices", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-compose_recomposition_hotspot", + "type": "skill", + "target": "compose_recomposition_hotspot", + "mode": "semantic", + "source_file": "backend/skills/atomic/compose_recomposition_hotspot.skill.yaml", + "parameters": {}, + "required_steps": [ + "frame_timeline_check", + "recomposition_overview" + ], + "semantic_step": "recomposition_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-pipeline_4feature_scoring", + "type": "skill", + "target": "pipeline_4feature_scoring", + "mode": "semantic", + "source_file": "backend/skills/atomic/pipeline_4feature_scoring.skill.yaml", + "parameters": {}, + "required_steps": [ + "producer_thread_signature" + ], + "semantic_step": "producer_thread_signature", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_pip_freeform", + "type": "skill", + "target": "pipeline_android_pip_freeform", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_pip_freeform.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_mixed", + "type": "skill", + "target": "pipeline_android_view_mixed", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_mixed.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_multi_window", + "type": "skill", + "target": "pipeline_android_view_multi_window", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_multi_window.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_software", + "type": "skill", + "target": "pipeline_android_view_software", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_software.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_standard_blast", + "type": "skill", + "target": "pipeline_android_view_standard_blast", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_standard_blast.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_android_view_standard_legacy", + "type": "skill", + "target": "pipeline_android_view_standard_legacy", + "mode": "definition", + "source_file": "backend/skills/pipelines/android_view_standard_legacy.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_chrome_browser_viz", + "type": "skill", + "target": "pipeline_chrome_browser_viz", + "mode": "definition", + "source_file": "backend/skills/pipelines/chrome_browser_viz.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_compose_standard", + "type": "skill", + "target": "pipeline_compose_standard", + "mode": "definition", + "source_file": "backend/skills/pipelines/compose_standard.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_flutter_surfaceview_impeller", + "type": "skill", + "target": "pipeline_flutter_surfaceview_impeller", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_surfaceview_impeller.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_flutter_surfaceview_skia", + "type": "skill", + "target": "pipeline_flutter_surfaceview_skia", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_surfaceview_skia.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_game_engine", + "type": "skill", + "target": "pipeline_game_engine", + "mode": "definition", + "source_file": "backend/skills/pipelines/game_engine.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_imagereader_pipeline", + "type": "skill", + "target": "pipeline_imagereader_pipeline", + "mode": "definition", + "source_file": "backend/skills/pipelines/imagereader_pipeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-pipeline_key_slices_overlay", + "type": "skill", + "target": "pipeline_key_slices_overlay", + "mode": "semantic", + "source_file": "backend/skills/atomic/pipeline_key_slices_overlay.skill.yaml", + "parameters": { + "slice_names": "'Choreographer#doFrame','DrawFrame','queueBuffer','latchBuffer'" + }, + "required_steps": [ + "pipeline_key_slices_overlay" + ], + "semantic_step": "pipeline_key_slices_overlay", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_new_arch", + "type": "skill", + "target": "pipeline_rn_new_arch", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_new_arch.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_old_arch", + "type": "skill", + "target": "pipeline_rn_old_arch", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_old_arch.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_rn_skia", + "type": "skill", + "target": "pipeline_rn_skia", + "mode": "definition", + "source_file": "backend/skills/pipelines/rn_skia.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_software_compositing", + "type": "skill", + "target": "pipeline_software_compositing", + "mode": "definition", + "source_file": "backend/skills/pipelines/software_compositing.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_surface_control_api", + "type": "skill", + "target": "pipeline_surface_control_api", + "mode": "definition", + "source_file": "backend/skills/pipelines/surface_control_api.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_surfaceview_blast", + "type": "skill", + "target": "pipeline_surfaceview_blast", + "mode": "definition", + "source_file": "backend/skills/pipelines/surfaceview_blast.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_variable_refresh_rate", + "type": "skill", + "target": "pipeline_variable_refresh_rate", + "mode": "definition", + "source_file": "backend/skills/pipelines/variable_refresh_rate.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_video_overlay_hwc", + "type": "skill", + "target": "pipeline_video_overlay_hwc", + "mode": "definition", + "source_file": "backend/skills/pipelines/video_overlay_hwc.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_gl_functor", + "type": "skill", + "target": "pipeline_webview_gl_functor", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_gl_functor.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_surface_control", + "type": "skill", + "target": "pipeline_webview_surface_control", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_surface_control.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "definition-pipeline_webview_surfaceview_wrapper", + "type": "skill", + "target": "pipeline_webview_surfaceview_wrapper", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_surfaceview_wrapper.skill.yaml", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-systemui_module", + "type": "skill", + "target": "systemui_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/systemui_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "systemui_process" + ], + "semantic_step": "systemui_process", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-third_party_module", + "type": "skill", + "target": "third_party_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/third_party_module.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture" + }, + "required_steps": [ + "thread_overview" + ], + "semantic_step": "thread_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "execute-wms_module", + "type": "skill", + "target": "wms_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/wms_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "window_animation_overview" + ], + "semantic_step": "window_animation_overview", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "id": "strategy-pipeline", + "type": "strategy", + "target": "pipeline", + "query": "管线识别", + "expected_strategy": "pipeline", + "source_file": "backend/strategies/pipeline.strategy.md", + "required_marker": "SmartPerfetto::CASE::framework-pipelines" + } + ] + } +} diff --git a/Trace/constructed/framework-pipelines/scenario.json b/Trace/constructed/framework-pipelines/scenario.json new file mode 100644 index 000000000..c705787b0 --- /dev/null +++ b/Trace/constructed/framework-pipelines/scenario.json @@ -0,0 +1,201 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::framework-pipelines" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "Recomposer:recompose" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "Flutter::BeginFrame" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "RN::FabricCommit" + }, + { + "type": "atrace-slice", + "at_ns": "130000000", + "duration_ns": "22000000", + "process": "app", + "thread": "main", + "name": "WebView::DrawFun" + }, + { + "type": "atrace-slice", + "at_ns": "200000000", + "duration_ns": "22000000", + "process": "app", + "thread": "main", + "name": "Choreographer#doFrame" + }, + { + "type": "atrace-slice", + "at_ns": "225000000", + "duration_ns": "23000000", + "process": "app", + "thread": "render", + "name": "DrawFrame" + }, + { + "type": "atrace-slice", + "at_ns": "250000000", + "duration_ns": "24000000", + "process": "app", + "thread": "render", + "name": "queueBuffer" + }, + { + "type": "atrace-slice", + "at_ns": "275000000", + "duration_ns": "25000000", + "process": "sf", + "thread": "sf-main", + "name": "latchBuffer" + }, + { + "type": "atrace-slice", + "at_ns": "300000000", + "duration_ns": "26000000", + "process": "system", + "thread": "system-main", + "name": "WindowAnimation" + }, + { + "type": "atrace-slice", + "at_ns": "325000000", + "duration_ns": "27000000", + "process": "system", + "thread": "system-main", + "name": "AppTransition" + }, + { + "type": "atrace-slice", + "at_ns": "350000000", + "duration_ns": "28000000", + "process": "app", + "thread": "rn-native", + "name": "FabricMount::executeMount" + }, + { + "type": "atrace-slice", + "at_ns": "375000000", + "duration_ns": "29000000", + "process": "app", + "thread": "webview", + "name": "DrawGL::DrawFunctor" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.framework-pipelines.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + } + ] +} diff --git a/Trace/constructed/framework-pipelines/trace.overlay.pftrace b/Trace/constructed/framework-pipelines/trace.overlay.pftrace new file mode 100644 index 000000000..b7925dea2 Binary files /dev/null and b/Trace/constructed/framework-pipelines/trace.overlay.pftrace differ diff --git a/Trace/constructed/general-runtime-contracts/analysis/expected.json b/Trace/constructed/general-runtime-contracts/analysis/expected.json new file mode 100644 index 000000000..7a35058f5 --- /dev/null +++ b/Trace/constructed/general-runtime-contracts/analysis/expected.json @@ -0,0 +1,149 @@ +{ + "schema_version": 1, + "case_id": "general-runtime-contracts", + "marker": "SmartPerfetto::CASE::general-runtime-contracts", + "expectations": [ + { + "id": "execute-callstack_analysis", + "type": "skill", + "target": "callstack_analysis", + "mode": "graceful_empty", + "source_file": "backend/skills/deep/callstack_analysis.skill.yaml", + "parameters": { + "min_samples": 10 + }, + "required_steps": [ + "check_samples" + ], + "semantic_step": "check_samples", + "limitation_reason": "The fixture does not contain perf samples or interned callstacks.", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "execute-fence_wait_decomposition", + "type": "skill", + "target": "fence_wait_decomposition", + "mode": "semantic", + "source_file": "backend/skills/atomic/fence_wait_decomposition.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "acquire_fence_wait" + ], + "semantic_step": "acquire_fence_wait", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "execute-global_trace_sanity_check", + "type": "skill", + "target": "global_trace_sanity_check", + "mode": "semantic", + "source_file": "backend/skills/composite/global_trace_sanity_check.skill.yaml", + "parameters": { + "max_rows": 20 + }, + "required_steps": [ + "trace_window" + ], + "semantic_step": "trace_window", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-main_thread_slices_in_range", + "type": "skill", + "target": "main_thread_slices_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_slices_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-main_thread_states_in_range", + "type": "skill", + "target": "main_thread_states_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_states_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-multi_trace_result_comparison", + "type": "skill", + "target": "multi_trace_result_comparison", + "mode": "definition", + "source_file": "backend/skills/comparison/multi_trace_result_comparison.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-process_identity_resolver", + "type": "skill", + "target": "process_identity_resolver", + "mode": "definition", + "source_file": "backend/skills/atomic/process_identity_resolver.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-general", + "type": "strategy", + "target": "general", + "query": "分析这份 trace", + "expected_strategy": "general", + "source_file": "backend/strategies/general.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-multi_trace_result_comparison", + "type": "strategy", + "target": "multi_trace_result_comparison", + "query": "分析结果对比", + "expected_strategy": "multi_trace_result_comparison", + "source_file": "backend/strategies/multi-trace-result-comparison.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-overview", + "type": "strategy", + "target": "overview", + "query": "发生了什么", + "expected_strategy": "overview", + "source_file": "backend/strategies/overview.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-runtime_correctness", + "type": "strategy", + "target": "runtime_correctness", + "query": "runtime", + "expected_strategy": "runtime_correctness", + "source_file": "backend/strategies/runtime-correctness.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-smart", + "type": "strategy", + "target": "smart", + "query": "smart", + "expected_strategy": "smart", + "source_file": "backend/strategies/smart.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-teaching", + "type": "strategy", + "target": "teaching", + "query": "教学", + "expected_strategy": "teaching", + "source_file": "backend/strategies/teaching.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-verifier_misdiagnosis", + "type": "strategy", + "target": "verifier_misdiagnosis", + "query": "contract-only:verifier_misdiagnosis", + "expected_strategy": "verifier_misdiagnosis", + "source_file": "backend/strategies/verifier-misdiagnosis.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + } + ] +} diff --git a/Trace/constructed/general-runtime-contracts/case.json b/Trace/constructed/general-runtime-contracts/case.json new file mode 100644 index 000000000..08bb8e326 --- /dev/null +++ b/Trace/constructed/general-runtime-contracts/case.json @@ -0,0 +1,217 @@ +{ + "schema_version": 1, + "id": "general-runtime-contracts", + "kind": "constructed", + "title": "General analysis and runtime contracts", + "description": "Deterministic general analysis and runtime contracts signals over the android-scroll-customer real base trace.", + "scene": "general", + "tags": [ + "android", + "constructed", + "general" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "2627a1c665d57bd90a52a8c01feb98e188125c99bd4268e2282318445b0c743a", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "general-runtime-contracts-v1", + "output": "Trace/.generated/constructed/general-runtime-contracts/trace.pftrace" + }, + "coverage": { + "skills": [ + "callstack_analysis", + "fence_wait_decomposition", + "global_trace_sanity_check", + "main_thread_slices_in_range", + "main_thread_states_in_range", + "multi_trace_result_comparison", + "process_identity_resolver" + ], + "strategies": [ + "general", + "multi_trace_result_comparison", + "overview", + "runtime_correctness", + "smart", + "teaching", + "verifier_misdiagnosis" + ], + "expectations": [ + { + "id": "execute-callstack_analysis", + "type": "skill", + "target": "callstack_analysis", + "mode": "graceful_empty", + "source_file": "backend/skills/deep/callstack_analysis.skill.yaml", + "parameters": { + "min_samples": 10 + }, + "required_steps": [ + "check_samples" + ], + "semantic_step": "check_samples", + "limitation_reason": "The fixture does not contain perf samples or interned callstacks.", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "execute-fence_wait_decomposition", + "type": "skill", + "target": "fence_wait_decomposition", + "mode": "semantic", + "source_file": "backend/skills/atomic/fence_wait_decomposition.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "acquire_fence_wait" + ], + "semantic_step": "acquire_fence_wait", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "execute-global_trace_sanity_check", + "type": "skill", + "target": "global_trace_sanity_check", + "mode": "semantic", + "source_file": "backend/skills/composite/global_trace_sanity_check.skill.yaml", + "parameters": { + "max_rows": 20 + }, + "required_steps": [ + "trace_window" + ], + "semantic_step": "trace_window", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-main_thread_slices_in_range", + "type": "skill", + "target": "main_thread_slices_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_slices_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-main_thread_states_in_range", + "type": "skill", + "target": "main_thread_states_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_states_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-multi_trace_result_comparison", + "type": "skill", + "target": "multi_trace_result_comparison", + "mode": "definition", + "source_file": "backend/skills/comparison/multi_trace_result_comparison.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "definition-process_identity_resolver", + "type": "skill", + "target": "process_identity_resolver", + "mode": "definition", + "source_file": "backend/skills/atomic/process_identity_resolver.skill.yaml", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-general", + "type": "strategy", + "target": "general", + "query": "分析这份 trace", + "expected_strategy": "general", + "source_file": "backend/strategies/general.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-multi_trace_result_comparison", + "type": "strategy", + "target": "multi_trace_result_comparison", + "query": "分析结果对比", + "expected_strategy": "multi_trace_result_comparison", + "source_file": "backend/strategies/multi-trace-result-comparison.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-overview", + "type": "strategy", + "target": "overview", + "query": "发生了什么", + "expected_strategy": "overview", + "source_file": "backend/strategies/overview.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-runtime_correctness", + "type": "strategy", + "target": "runtime_correctness", + "query": "runtime", + "expected_strategy": "runtime_correctness", + "source_file": "backend/strategies/runtime-correctness.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-smart", + "type": "strategy", + "target": "smart", + "query": "smart", + "expected_strategy": "smart", + "source_file": "backend/strategies/smart.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-teaching", + "type": "strategy", + "target": "teaching", + "query": "教学", + "expected_strategy": "teaching", + "source_file": "backend/strategies/teaching.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "id": "strategy-verifier_misdiagnosis", + "type": "strategy", + "target": "verifier_misdiagnosis", + "query": "contract-only:verifier_misdiagnosis", + "expected_strategy": "verifier_misdiagnosis", + "source_file": "backend/strategies/verifier-misdiagnosis.strategy.md", + "required_marker": "SmartPerfetto::CASE::general-runtime-contracts" + } + ] + } +} diff --git a/Trace/constructed/general-runtime-contracts/scenario.json b/Trace/constructed/general-runtime-contracts/scenario.json new file mode 100644 index 000000000..ead29efe2 --- /dev/null +++ b/Trace/constructed/general-runtime-contracts/scenario.json @@ -0,0 +1,129 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::general-runtime-contracts" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "SmartPerfetto::general-analysis" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "trace overview" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "comparison baseline" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.general-runtime-contracts.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + } + ] +} diff --git a/Trace/constructed/general-runtime-contracts/trace.overlay.pftrace b/Trace/constructed/general-runtime-contracts/trace.overlay.pftrace new file mode 100644 index 000000000..8977fe773 Binary files /dev/null and b/Trace/constructed/general-runtime-contracts/trace.overlay.pftrace differ diff --git a/Trace/constructed/gpu-workload/analysis/expected.json b/Trace/constructed/gpu-workload/analysis/expected.json new file mode 100644 index 000000000..ca61fa8b7 --- /dev/null +++ b/Trace/constructed/gpu-workload/analysis/expected.json @@ -0,0 +1,125 @@ +{ + "schema_version": 1, + "case_id": "gpu-workload", + "marker": "SmartPerfetto::CASE::gpu-workload", + "expectations": [ + { + "id": "execute-android_gpu_work_period_track", + "type": "skill", + "target": "android_gpu_work_period_track", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_gpu_work_period_track.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_work" + ], + "semantic_step": "gpu_work", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_analysis", + "type": "skill", + "target": "gpu_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/gpu_analysis.skill.yaml", + "parameters": { + "high_freq_threshold_pct": 70, + "mid_freq_threshold_pct": 40, + "low_freq_threshold_pct": 10, + "freq_drop_ratio": 0.7, + "freq_drop_count_threshold": 20, + "high_load_min_dur_ns": 500000000 + }, + "required_steps": [ + "data_check", + "gpu_freq_overview" + ], + "semantic_step": "gpu_freq_overview", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-gpu_freq_in_range", + "type": "skill", + "target": "gpu_freq_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_freq_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-gpu_frequency_analysis", + "type": "skill", + "target": "gpu_frequency_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_frequency_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_metrics", + "type": "skill", + "target": "gpu_metrics", + "mode": "semantic", + "source_file": "backend/skills/atomic/gpu_metrics.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_frequency" + ], + "semantic_step": "gpu_frequency", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_module", + "type": "skill", + "target": "gpu_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/gpu_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_frequency_overview" + ], + "semantic_step": "gpu_frequency_overview", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_v57_ai_diagnostics", + "type": "skill", + "target": "gpu_v57_ai_diagnostics", + "mode": "semantic", + "source_file": "backend/skills/composite/gpu_v57_ai_diagnostics.skill.yaml", + "parameters": { + "target_freq_ratio": 0.9, + "min_throttle_ns": 1000, + "max_rows": 20 + }, + "required_steps": [ + "data_check", + "gpu_inventory" + ], + "semantic_step": "gpu_inventory", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_angle_gles_vulkan", + "type": "skill", + "target": "pipeline_angle_gles_vulkan", + "mode": "definition", + "source_file": "backend/skills/pipelines/angle_gles_vulkan.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_opengl_es", + "type": "skill", + "target": "pipeline_opengl_es", + "mode": "definition", + "source_file": "backend/skills/pipelines/opengl_es.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_vulkan_native", + "type": "skill", + "target": "pipeline_vulkan_native", + "mode": "definition", + "source_file": "backend/skills/pipelines/vulkan_native.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + } + ] +} diff --git a/Trace/constructed/gpu-workload/case.json b/Trace/constructed/gpu-workload/case.json new file mode 100644 index 000000000..bbdd5458c --- /dev/null +++ b/Trace/constructed/gpu-workload/case.json @@ -0,0 +1,188 @@ +{ + "schema_version": 1, + "id": "gpu-workload", + "kind": "constructed", + "title": "GPU workload and frequency", + "description": "Deterministic gpu workload and frequency signals over the android-scroll-customer real base trace.", + "scene": "gpu", + "tags": [ + "android", + "constructed", + "gpu" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "2e6a951520693480b5316c262b57557a564ec9277b24e97fac49b4bb8e747e73", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "gpu-workload-v1", + "output": "Trace/.generated/constructed/gpu-workload/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_gpu_work_period_track", + "gpu_analysis", + "gpu_freq_in_range", + "gpu_frequency_analysis", + "gpu_metrics", + "gpu_module", + "gpu_v57_ai_diagnostics", + "pipeline_angle_gles_vulkan", + "pipeline_opengl_es", + "pipeline_vulkan_native" + ], + "strategies": [], + "expectations": [ + { + "id": "execute-android_gpu_work_period_track", + "type": "skill", + "target": "android_gpu_work_period_track", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_gpu_work_period_track.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_work" + ], + "semantic_step": "gpu_work", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_analysis", + "type": "skill", + "target": "gpu_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/gpu_analysis.skill.yaml", + "parameters": { + "high_freq_threshold_pct": 70, + "mid_freq_threshold_pct": 40, + "low_freq_threshold_pct": 10, + "freq_drop_ratio": 0.7, + "freq_drop_count_threshold": 20, + "high_load_min_dur_ns": 500000000 + }, + "required_steps": [ + "data_check", + "gpu_freq_overview" + ], + "semantic_step": "gpu_freq_overview", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-gpu_freq_in_range", + "type": "skill", + "target": "gpu_freq_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_freq_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-gpu_frequency_analysis", + "type": "skill", + "target": "gpu_frequency_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_frequency_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_metrics", + "type": "skill", + "target": "gpu_metrics", + "mode": "semantic", + "source_file": "backend/skills/atomic/gpu_metrics.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_frequency" + ], + "semantic_step": "gpu_frequency", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_module", + "type": "skill", + "target": "gpu_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/gpu_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "gpu_frequency_overview" + ], + "semantic_step": "gpu_frequency_overview", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "execute-gpu_v57_ai_diagnostics", + "type": "skill", + "target": "gpu_v57_ai_diagnostics", + "mode": "semantic", + "source_file": "backend/skills/composite/gpu_v57_ai_diagnostics.skill.yaml", + "parameters": { + "target_freq_ratio": 0.9, + "min_throttle_ns": 1000, + "max_rows": 20 + }, + "required_steps": [ + "data_check", + "gpu_inventory" + ], + "semantic_step": "gpu_inventory", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_angle_gles_vulkan", + "type": "skill", + "target": "pipeline_angle_gles_vulkan", + "mode": "definition", + "source_file": "backend/skills/pipelines/angle_gles_vulkan.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_opengl_es", + "type": "skill", + "target": "pipeline_opengl_es", + "mode": "definition", + "source_file": "backend/skills/pipelines/opengl_es.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + }, + { + "id": "definition-pipeline_vulkan_native", + "type": "skill", + "target": "pipeline_vulkan_native", + "mode": "definition", + "source_file": "backend/skills/pipelines/vulkan_native.skill.yaml", + "required_marker": "SmartPerfetto::CASE::gpu-workload" + } + ] + } +} diff --git a/Trace/constructed/gpu-workload/scenario.json b/Trace/constructed/gpu-workload/scenario.json new file mode 100644 index 000000000..a7b05eeb9 --- /dev/null +++ b/Trace/constructed/gpu-workload/scenario.json @@ -0,0 +1,168 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::gpu-workload" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "GPU completion" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "vkQueueSubmit" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "gpu_work_period" + }, + { + "type": "atrace-slice", + "at_ns": "200000000", + "duration_ns": "22000000", + "process": "app", + "thread": "render", + "name": "vkQueueSubmit" + }, + { + "type": "atrace-slice", + "at_ns": "225000000", + "duration_ns": "23000000", + "process": "app", + "thread": "render", + "name": "eglSwapBuffers" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.gpu-workload.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "gpu-work-period", + "at_ns": "450000000", + "duration_ns": "30000000", + "gpu_id": 0, + "uid": 10999, + "active_duration_ns": "24000000", + "cpu": 0 + }, + { + "type": "gpu-frequency", + "at_ns": "450000000", + "gpu_id": 0, + "value": 700000, + "cpu": 0 + }, + { + "type": "gpu-power-state", + "at_ns": "460000000", + "old_state": 0, + "new_state": 2, + "cpu": 0 + } + ] +} diff --git a/Trace/constructed/gpu-workload/trace.overlay.pftrace b/Trace/constructed/gpu-workload/trace.overlay.pftrace new file mode 100644 index 000000000..b51b53102 Binary files /dev/null and b/Trace/constructed/gpu-workload/trace.overlay.pftrace differ diff --git a/Trace/constructed/input-interaction-latency/analysis/expected.json b/Trace/constructed/input-interaction-latency/analysis/expected.json new file mode 100644 index 000000000..5e41f2446 --- /dev/null +++ b/Trace/constructed/input-interaction-latency/analysis/expected.json @@ -0,0 +1,184 @@ +{ + "schema_version": 1, + "case_id": "input-interaction-latency", + "marker": "SmartPerfetto::CASE::input-interaction-latency", + "expectations": [ + { + "id": "definition-app_lifecycle_in_range", + "type": "skill", + "target": "app_lifecycle_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/app_lifecycle_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-battery_doze_state_timeline", + "type": "skill", + "target": "battery_doze_state_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_doze_state_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "deep_idle_intervals" + ], + "semantic_step": "deep_idle_intervals", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-click_response_analysis", + "type": "skill", + "target": "click_response_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/click_response_analysis.skill.yaml", + "parameters": { + "slow_event_threshold_ms": 100, + "avg_latency_critical_ms": 100, + "avg_handling_warning_ms": 50, + "avg_dispatch_warning_ms": 20, + "binder_blocking_threshold_ms": 30, + "critical_event_threshold_ms": 200, + "thread_state_min_dur_ms": 50, + "enable_per_event_detail": true + }, + "required_steps": [ + "check_input_data" + ], + "semantic_step": "check_input_data", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-click_response_detail", + "type": "skill", + "target": "click_response_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/click_response_detail.skill.yaml", + "parameters": { + "event_ts": "${trace_start}", + "event_end_ts": "${trace_end}", + "total_ms": 1, + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "init_cpu_topology", + "event_info" + ], + "semantic_step": "event_info", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-device_state_timeline", + "type": "skill", + "target": "device_state_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/device_state_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "cpu_freq_transitions" + ], + "semantic_step": "cpu_freq_transitions", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "definition-input_events_in_range", + "type": "skill", + "target": "input_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/input_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-input_module", + "type": "skill", + "target": "input_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/input_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "click_response_overview" + ], + "semantic_step": "click_response_overview", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-navigation_analysis", + "type": "skill", + "target": "navigation_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/navigation_analysis.skill.yaml", + "parameters": { + "slow_navigation_ms": 400, + "nav_rating_good_ms": 200, + "nav_rating_severe_ms": 700, + "slow_lifecycle_ms": 100, + "blocking_op_min_dur_ms": 5, + "inflate_critical_ms": 100, + "binder_blocking_ms": 16, + "oncreate_slow_ms": 200 + }, + "required_steps": [ + "check_lifecycle_data", + "get_process", + "navigation_overview" + ], + "semantic_step": "navigation_overview", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-scene_reconstruction", + "type": "skill", + "target": "scene_reconstruction", + "mode": "semantic", + "source_file": "backend/skills/composite/scene_reconstruction.skill.yaml", + "parameters": { + "trace_id": 1 + }, + "required_steps": [ + "trace_time_range" + ], + "semantic_step": "trace_time_range", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-state_timeline", + "type": "skill", + "target": "state_timeline", + "mode": "semantic", + "source_file": "backend/skills/composite/state_timeline.skill.yaml", + "parameters": { + "trace_id": 1 + }, + "required_steps": [ + "timeline_bounds", + "device_state_lane" + ], + "semantic_step": "device_state_lane", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "definition-touch_to_display_latency", + "type": "skill", + "target": "touch_to_display_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/touch_to_display_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "strategy-interaction", + "type": "strategy", + "target": "interaction", + "query": "点击", + "expected_strategy": "interaction", + "source_file": "backend/strategies/interaction.strategy.md", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "strategy-touch_tracking", + "type": "strategy", + "target": "touch_tracking", + "query": "跟手度", + "expected_strategy": "touch_tracking", + "source_file": "backend/strategies/touch-tracking.strategy.md", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + } + ] +} diff --git a/Trace/constructed/input-interaction-latency/case.json b/Trace/constructed/input-interaction-latency/case.json new file mode 100644 index 000000000..1d8ec1784 --- /dev/null +++ b/Trace/constructed/input-interaction-latency/case.json @@ -0,0 +1,251 @@ +{ + "schema_version": 1, + "id": "input-interaction-latency", + "kind": "constructed", + "title": "Input and interaction latency", + "description": "Deterministic input and interaction latency signals over the android-scroll-customer real base trace.", + "scene": "interaction", + "tags": [ + "android", + "constructed", + "interaction" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "7195386093ce0d29a5110861a928aa96d6af9fb15580bba9bc41248c29a39ab2", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "input-interaction-latency-v1", + "output": "Trace/.generated/constructed/input-interaction-latency/trace.pftrace" + }, + "coverage": { + "skills": [ + "app_lifecycle_in_range", + "battery_doze_state_timeline", + "click_response_analysis", + "click_response_detail", + "device_state_timeline", + "input_events_in_range", + "input_module", + "navigation_analysis", + "scene_reconstruction", + "state_timeline", + "touch_to_display_latency" + ], + "strategies": [ + "interaction", + "touch_tracking" + ], + "expectations": [ + { + "id": "definition-app_lifecycle_in_range", + "type": "skill", + "target": "app_lifecycle_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/app_lifecycle_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-battery_doze_state_timeline", + "type": "skill", + "target": "battery_doze_state_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_doze_state_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "deep_idle_intervals" + ], + "semantic_step": "deep_idle_intervals", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-click_response_analysis", + "type": "skill", + "target": "click_response_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/click_response_analysis.skill.yaml", + "parameters": { + "slow_event_threshold_ms": 100, + "avg_latency_critical_ms": 100, + "avg_handling_warning_ms": 50, + "avg_dispatch_warning_ms": 20, + "binder_blocking_threshold_ms": 30, + "critical_event_threshold_ms": 200, + "thread_state_min_dur_ms": 50, + "enable_per_event_detail": true + }, + "required_steps": [ + "check_input_data" + ], + "semantic_step": "check_input_data", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-click_response_detail", + "type": "skill", + "target": "click_response_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/click_response_detail.skill.yaml", + "parameters": { + "event_ts": "${trace_start}", + "event_end_ts": "${trace_end}", + "total_ms": 1, + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "init_cpu_topology", + "event_info" + ], + "semantic_step": "event_info", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-device_state_timeline", + "type": "skill", + "target": "device_state_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/device_state_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "cpu_freq_transitions" + ], + "semantic_step": "cpu_freq_transitions", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "definition-input_events_in_range", + "type": "skill", + "target": "input_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/input_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-input_module", + "type": "skill", + "target": "input_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/input_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "click_response_overview" + ], + "semantic_step": "click_response_overview", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-navigation_analysis", + "type": "skill", + "target": "navigation_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/navigation_analysis.skill.yaml", + "parameters": { + "slow_navigation_ms": 400, + "nav_rating_good_ms": 200, + "nav_rating_severe_ms": 700, + "slow_lifecycle_ms": 100, + "blocking_op_min_dur_ms": 5, + "inflate_critical_ms": 100, + "binder_blocking_ms": 16, + "oncreate_slow_ms": 200 + }, + "required_steps": [ + "check_lifecycle_data", + "get_process", + "navigation_overview" + ], + "semantic_step": "navigation_overview", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-scene_reconstruction", + "type": "skill", + "target": "scene_reconstruction", + "mode": "semantic", + "source_file": "backend/skills/composite/scene_reconstruction.skill.yaml", + "parameters": { + "trace_id": 1 + }, + "required_steps": [ + "trace_time_range" + ], + "semantic_step": "trace_time_range", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "execute-state_timeline", + "type": "skill", + "target": "state_timeline", + "mode": "semantic", + "source_file": "backend/skills/composite/state_timeline.skill.yaml", + "parameters": { + "trace_id": 1 + }, + "required_steps": [ + "timeline_bounds", + "device_state_lane" + ], + "semantic_step": "device_state_lane", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "definition-touch_to_display_latency", + "type": "skill", + "target": "touch_to_display_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/touch_to_display_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "strategy-interaction", + "type": "strategy", + "target": "interaction", + "query": "点击", + "expected_strategy": "interaction", + "source_file": "backend/strategies/interaction.strategy.md", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "id": "strategy-touch_tracking", + "type": "strategy", + "target": "touch_tracking", + "query": "跟手度", + "expected_strategy": "touch_tracking", + "source_file": "backend/strategies/touch-tracking.strategy.md", + "required_marker": "SmartPerfetto::CASE::input-interaction-latency" + } + ] + } +} diff --git a/Trace/constructed/input-interaction-latency/scenario.json b/Trace/constructed/input-interaction-latency/scenario.json new file mode 100644 index 000000000..c0a8d845f --- /dev/null +++ b/Trace/constructed/input-interaction-latency/scenario.json @@ -0,0 +1,201 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::input-interaction-latency" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "InputDispatcher::dispatch" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "deliverInputEvent" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "performClick" + }, + { + "type": "atrace-slice", + "at_ns": "200000000", + "duration_ns": "22000000", + "process": "system", + "thread": "system-main", + "name": "InputDispatcher::dispatchMotion" + }, + { + "type": "atrace-slice", + "at_ns": "225000000", + "duration_ns": "23000000", + "process": "app", + "thread": "main", + "name": "deliverInputEvent src=0x1002" + }, + { + "type": "atrace-slice", + "at_ns": "250000000", + "duration_ns": "24000000", + "process": "app", + "thread": "main", + "name": "performClick" + }, + { + "type": "atrace-slice", + "at_ns": "275000000", + "duration_ns": "25000000", + "process": "app", + "thread": "main", + "name": "ActivityThread::performCreate" + }, + { + "type": "atrace-slice", + "at_ns": "300000000", + "duration_ns": "26000000", + "process": "app", + "thread": "main", + "name": "ActivityThread::performResume" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.input-interaction-latency.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "atrace-counter", + "at_ns": "520000000", + "process": "system", + "thread": "system-main", + "name": "ScreenState", + "value": 1 + }, + { + "type": "battery-counters", + "at_ns": "540000000", + "capacity_percent": 80, + "charge_counter_uah": "4000000", + "current_ua": "-500000", + "voltage_uv": "4000000" + }, + { + "type": "atrace-counter", + "at_ns": "560000000", + "process": "system", + "thread": "system-main", + "name": "DozeDeepState", + "value": 5 + }, + { + "type": "atrace-counter", + "at_ns": "660000000", + "process": "system", + "thread": "system-main", + "name": "DozeDeepState", + "value": 0 + } + ] +} diff --git a/Trace/constructed/input-interaction-latency/trace.overlay.pftrace b/Trace/constructed/input-interaction-latency/trace.overlay.pftrace new file mode 100644 index 000000000..70bdaa9c4 Binary files /dev/null and b/Trace/constructed/input-interaction-latency/trace.overlay.pftrace differ diff --git a/Trace/constructed/linux-system-state/analysis/expected.json b/Trace/constructed/linux-system-state/analysis/expected.json new file mode 100644 index 000000000..5276e935d --- /dev/null +++ b/Trace/constructed/linux-system-state/analysis/expected.json @@ -0,0 +1,59 @@ +{ + "schema_version": 1, + "case_id": "linux-system-state", + "marker": "SmartPerfetto::CASE::linux-system-state", + "expectations": [ + { + "id": "execute-device_state_snapshot", + "type": "skill", + "target": "device_state_snapshot", + "mode": "semantic", + "source_file": "backend/skills/composite/device_state_snapshot.skill.yaml", + "parameters": {}, + "required_steps": [ + "environment_snapshot" + ], + "semantic_step": "environment_snapshot", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "execute-linux_perf_counter_hotspots", + "type": "skill", + "target": "linux_perf_counter_hotspots", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/linux_perf_counter_hotspots.skill.yaml", + "parameters": {}, + "required_steps": [ + "perf_counter_hotspots" + ], + "semantic_step": "perf_counter_hotspots", + "limitation_reason": "The fixture does not contain PMU perf sample/counter packets.", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "definition-linux_systemd_journald_analysis", + "type": "skill", + "target": "linux_systemd_journald_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/linux_systemd_journald_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "definition-trace_state_track_summary", + "type": "skill", + "target": "trace_state_track_summary", + "mode": "definition", + "source_file": "backend/skills/atomic/trace_state_track_summary.skill.yaml", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "strategy-linux", + "type": "strategy", + "target": "linux", + "query": "linux", + "expected_strategy": "linux", + "source_file": "backend/strategies/linux.strategy.md", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + } + ] +} diff --git a/Trace/constructed/linux-system-state/case.json b/Trace/constructed/linux-system-state/case.json new file mode 100644 index 000000000..e1a732dac --- /dev/null +++ b/Trace/constructed/linux-system-state/case.json @@ -0,0 +1,118 @@ +{ + "schema_version": 1, + "id": "linux-system-state", + "kind": "constructed", + "title": "Linux and Android system state", + "description": "Deterministic linux and android system state signals over the android-startup-light real base trace.", + "scene": "linux", + "tags": [ + "android", + "constructed", + "linux" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "f00d779b7a75887416a4e564508903e4db87ee3caa8a7066ff1975c62c88031a", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Google raven base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-light", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "linux-system-state-v1", + "output": "Trace/.generated/constructed/linux-system-state/trace.pftrace" + }, + "coverage": { + "skills": [ + "device_state_snapshot", + "linux_perf_counter_hotspots", + "linux_systemd_journald_analysis", + "trace_state_track_summary" + ], + "strategies": [ + "linux" + ], + "expectations": [ + { + "id": "execute-device_state_snapshot", + "type": "skill", + "target": "device_state_snapshot", + "mode": "semantic", + "source_file": "backend/skills/composite/device_state_snapshot.skill.yaml", + "parameters": {}, + "required_steps": [ + "environment_snapshot" + ], + "semantic_step": "environment_snapshot", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "execute-linux_perf_counter_hotspots", + "type": "skill", + "target": "linux_perf_counter_hotspots", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/linux_perf_counter_hotspots.skill.yaml", + "parameters": {}, + "required_steps": [ + "perf_counter_hotspots" + ], + "semantic_step": "perf_counter_hotspots", + "limitation_reason": "The fixture does not contain PMU perf sample/counter packets.", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "definition-linux_systemd_journald_analysis", + "type": "skill", + "target": "linux_systemd_journald_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/linux_systemd_journald_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "definition-trace_state_track_summary", + "type": "skill", + "target": "trace_state_track_summary", + "mode": "definition", + "source_file": "backend/skills/atomic/trace_state_track_summary.skill.yaml", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + }, + { + "id": "strategy-linux", + "type": "strategy", + "target": "linux", + "query": "linux", + "expected_strategy": "linux", + "source_file": "backend/strategies/linux.strategy.md", + "required_marker": "SmartPerfetto::CASE::linux-system-state" + } + ] + } +} diff --git a/Trace/constructed/linux-system-state/scenario.json b/Trace/constructed/linux-system-state/scenario.json new file mode 100644 index 000000000..c736b49e5 --- /dev/null +++ b/Trace/constructed/linux-system-state/scenario.json @@ -0,0 +1,153 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::linux-system-state" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "system_server state" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "journald" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "JobScheduler" + }, + { + "type": "atrace-slice", + "at_ns": "200000000", + "duration_ns": "22000000", + "process": "system", + "thread": "system-main", + "name": "device_idle" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.linux-system-state.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "atrace-counter", + "at_ns": "520000000", + "process": "system", + "thread": "system-main", + "name": "ScreenState", + "value": 1 + }, + { + "type": "battery-counters", + "at_ns": "540000000", + "capacity_percent": 80, + "charge_counter_uah": "4000000", + "current_ua": "-500000", + "voltage_uv": "4000000" + } + ] +} diff --git a/Trace/constructed/linux-system-state/trace.overlay.pftrace b/Trace/constructed/linux-system-state/trace.overlay.pftrace new file mode 100644 index 000000000..4a58672f5 Binary files /dev/null and b/Trace/constructed/linux-system-state/trace.overlay.pftrace differ diff --git a/Trace/constructed/media-network-camera/analysis/expected.json b/Trace/constructed/media-network-camera/analysis/expected.json new file mode 100644 index 000000000..8f50d9522 --- /dev/null +++ b/Trace/constructed/media-network-camera/analysis/expected.json @@ -0,0 +1,117 @@ +{ + "schema_version": 1, + "case_id": "media-network-camera", + "marker": "SmartPerfetto::CASE::media-network-camera", + "expectations": [ + { + "id": "execute-camera_trace_evidence", + "type": "skill", + "target": "camera_trace_evidence", + "mode": "semantic", + "source_file": "backend/skills/composite/camera_trace_evidence.skill.yaml", + "parameters": { + "max_rows": 20 + }, + "required_steps": [ + "evidence_coverage" + ], + "semantic_step": "evidence_coverage", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-media_codec_activity", + "type": "skill", + "target": "media_codec_activity", + "mode": "semantic", + "source_file": "backend/skills/atomic/media_codec_activity.skill.yaml", + "parameters": {}, + "required_steps": [ + "codec_activity_summary" + ], + "semantic_step": "codec_activity_summary", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-modem_network_correlation_summary", + "type": "skill", + "target": "modem_network_correlation_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/modem_network_correlation_summary.skill.yaml", + "parameters": { + "top_n": 20 + }, + "required_steps": [ + "modem_network_correlation" + ], + "semantic_step": "modem_network_correlation", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-network_analysis", + "type": "skill", + "target": "network_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/network_analysis.skill.yaml", + "parameters": { + "heavy_traffic_critical_mb": 100, + "heavy_traffic_warning_mb": 10, + "frequent_active_periods": 20 + }, + "required_steps": [ + "check_network_data", + "check_network_slices", + "evidence_scope", + "network_overview", + "traffic_by_interface", + "app_traffic_ranking", + "transport_distribution", + "port_analysis", + "traffic_timeline", + "large_transfers", + "network_power_cost", + "network_slice_overview" + ], + "semantic_step": "network_slice_overview", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "definition-pipeline_camera_pipeline", + "type": "skill", + "target": "pipeline_camera_pipeline", + "mode": "definition", + "source_file": "backend/skills/pipelines/camera_pipeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-webview_v8_analysis", + "type": "skill", + "target": "webview_v8_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/webview_v8_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "v8_gc_events" + ], + "semantic_step": "v8_gc_events", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "strategy-media", + "type": "strategy", + "target": "media", + "query": "视频", + "expected_strategy": "media", + "source_file": "backend/strategies/media.strategy.md", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "strategy-network", + "type": "strategy", + "target": "network", + "query": "网络", + "expected_strategy": "network", + "source_file": "backend/strategies/network.strategy.md", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + } + ] +} diff --git a/Trace/constructed/media-network-camera/case.json b/Trace/constructed/media-network-camera/case.json new file mode 100644 index 000000000..918999e04 --- /dev/null +++ b/Trace/constructed/media-network-camera/case.json @@ -0,0 +1,179 @@ +{ + "schema_version": 1, + "id": "media-network-camera", + "kind": "constructed", + "title": "Media, network, and camera pipeline", + "description": "Deterministic media, network, and camera pipeline signals over the android-scroll-customer real base trace.", + "scene": "media", + "tags": [ + "android", + "constructed", + "media" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "4abf868de9627fd097ba4a473eca1f137a0d17351b12e43817dc0c0e16448ce4", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "media-network-camera-v1", + "output": "Trace/.generated/constructed/media-network-camera/trace.pftrace" + }, + "coverage": { + "skills": [ + "camera_trace_evidence", + "media_codec_activity", + "modem_network_correlation_summary", + "network_analysis", + "pipeline_camera_pipeline", + "webview_v8_analysis" + ], + "strategies": [ + "media", + "network" + ], + "expectations": [ + { + "id": "execute-camera_trace_evidence", + "type": "skill", + "target": "camera_trace_evidence", + "mode": "semantic", + "source_file": "backend/skills/composite/camera_trace_evidence.skill.yaml", + "parameters": { + "max_rows": 20 + }, + "required_steps": [ + "evidence_coverage" + ], + "semantic_step": "evidence_coverage", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-media_codec_activity", + "type": "skill", + "target": "media_codec_activity", + "mode": "semantic", + "source_file": "backend/skills/atomic/media_codec_activity.skill.yaml", + "parameters": {}, + "required_steps": [ + "codec_activity_summary" + ], + "semantic_step": "codec_activity_summary", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-modem_network_correlation_summary", + "type": "skill", + "target": "modem_network_correlation_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/modem_network_correlation_summary.skill.yaml", + "parameters": { + "top_n": 20 + }, + "required_steps": [ + "modem_network_correlation" + ], + "semantic_step": "modem_network_correlation", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-network_analysis", + "type": "skill", + "target": "network_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/network_analysis.skill.yaml", + "parameters": { + "heavy_traffic_critical_mb": 100, + "heavy_traffic_warning_mb": 10, + "frequent_active_periods": 20 + }, + "required_steps": [ + "check_network_data", + "check_network_slices", + "evidence_scope", + "network_overview", + "traffic_by_interface", + "app_traffic_ranking", + "transport_distribution", + "port_analysis", + "traffic_timeline", + "large_transfers", + "network_power_cost", + "network_slice_overview" + ], + "semantic_step": "network_slice_overview", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "definition-pipeline_camera_pipeline", + "type": "skill", + "target": "pipeline_camera_pipeline", + "mode": "definition", + "source_file": "backend/skills/pipelines/camera_pipeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "execute-webview_v8_analysis", + "type": "skill", + "target": "webview_v8_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/webview_v8_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "v8_gc_events" + ], + "semantic_step": "v8_gc_events", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "strategy-media", + "type": "strategy", + "target": "media", + "query": "视频", + "expected_strategy": "media", + "source_file": "backend/strategies/media.strategy.md", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + }, + { + "id": "strategy-network", + "type": "strategy", + "target": "network", + "query": "网络", + "expected_strategy": "network", + "source_file": "backend/strategies/network.strategy.md", + "required_marker": "SmartPerfetto::CASE::media-network-camera" + } + ] + } +} diff --git a/Trace/constructed/media-network-camera/scenario.json b/Trace/constructed/media-network-camera/scenario.json new file mode 100644 index 000000000..a5be59561 --- /dev/null +++ b/Trace/constructed/media-network-camera/scenario.json @@ -0,0 +1,169 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::media-network-camera" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "Camera3-Device" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "MediaCodec::queueInputBuffer" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "NetworkRequest" + }, + { + "type": "atrace-slice", + "at_ns": "200000000", + "duration_ns": "22000000", + "process": "app", + "thread": "webview", + "name": "V8.GCCompactor" + }, + { + "type": "atrace-slice", + "at_ns": "225000000", + "duration_ns": "23000000", + "process": "app", + "thread": "webview", + "name": "v8.run::LongTask" + }, + { + "type": "atrace-slice", + "at_ns": "250000000", + "duration_ns": "24000000", + "process": "app", + "thread": "render", + "name": "MediaCodec::queueInputBuffer" + }, + { + "type": "atrace-slice", + "at_ns": "275000000", + "duration_ns": "25000000", + "process": "app", + "thread": "render", + "name": "Camera3-Device::processCaptureRequest" + }, + { + "type": "atrace-slice", + "at_ns": "300000000", + "duration_ns": "26000000", + "process": "app", + "thread": "main", + "name": "NetworkRequest::TTFB" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.media-network-camera.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + } + ] +} diff --git a/Trace/constructed/media-network-camera/trace.overlay.pftrace b/Trace/constructed/media-network-camera/trace.overlay.pftrace new file mode 100644 index 000000000..a1389c36e Binary files /dev/null and b/Trace/constructed/media-network-camera/trace.overlay.pftrace differ diff --git a/Trace/constructed/memory-gc-pressure/analysis/expected.json b/Trace/constructed/memory-gc-pressure/analysis/expected.json new file mode 100644 index 000000000..9cca1e26e --- /dev/null +++ b/Trace/constructed/memory-gc-pressure/analysis/expected.json @@ -0,0 +1,282 @@ +{ + "schema_version": 1, + "case_id": "memory-gc-pressure", + "marker": "SmartPerfetto::CASE::memory-gc-pressure", + "expectations": [ + { + "id": "execute-android_bitmap_memory_per_process", + "type": "skill", + "target": "android_bitmap_memory_per_process", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_bitmap_memory_per_process.skill.yaml", + "parameters": {}, + "required_steps": [ + "bitmap_memory" + ], + "semantic_step": "bitmap_memory", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_heap_graph_leak_candidates", + "type": "skill", + "target": "android_heap_graph_leak_candidates", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/android_heap_graph_leak_candidates.skill.yaml", + "parameters": { + "lifecycle_slice_prefix": "SI$", + "max_candidates": 50, + "max_reference_edges": 100 + }, + "required_steps": [ + "leak_candidates" + ], + "semantic_step": "leak_candidates", + "limitation_reason": "The fixture does not contain a managed heap graph dump.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_heap_graph_summary", + "type": "skill", + "target": "android_heap_graph_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_heap_graph_summary.skill.yaml", + "parameters": { + "max_rows": 30 + }, + "required_steps": [ + "heap_graph_availability" + ], + "semantic_step": "heap_graph_availability", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_memory_v57_ai_diagnostics", + "type": "skill", + "target": "android_memory_v57_ai_diagnostics", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/android_memory_v57_ai_diagnostics.skill.yaml", + "parameters": { + "min_size_mb": 1, + "max_rows": 40 + }, + "required_steps": [ + "data_check", + "heap_graph_repeated_objects" + ], + "semantic_step": "heap_graph_repeated_objects", + "limitation_reason": "Heap-graph diagnostics remain empty without a managed heap graph dump.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-dmabuf_analysis", + "type": "skill", + "target": "dmabuf_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/dmabuf_analysis.skill.yaml", + "parameters": { + "min_size_mb": 1 + }, + "required_steps": [ + "data_check", + "dmabuf_overview" + ], + "semantic_step": "dmabuf_overview", + "limitation_reason": "The fixture does not contain DMA-BUF allocation/residency events.", + "expected_error": "Condition not met", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-gc_analysis", + "type": "skill", + "target": "gc_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/gc_analysis.skill.yaml", + "parameters": { + "min_gc_dur_ms": 5 + }, + "required_steps": [ + "data_check", + "gc_overview" + ], + "semantic_step": "gc_overview", + "limitation_reason": "The fixture has GC marker slices but not ART garbage-collection packets.", + "expected_error": "Condition not met", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "definition-gc_events_in_range", + "type": "skill", + "target": "gc_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gc_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-linux_process_rss_swap_timeline", + "type": "skill", + "target": "linux_process_rss_swap_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_process_rss_swap_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "rss_swap_peaks" + ], + "semantic_step": "rss_swap_peaks", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-lmk_analysis", + "type": "skill", + "target": "lmk_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/lmk_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "data_check", + "lmk_overview" + ], + "semantic_step": "lmk_overview", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-lmk_kill_attribution", + "type": "skill", + "target": "lmk_kill_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/lmk_kill_attribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "lmk_events" + ], + "semantic_step": "lmk_events", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "definition-logcat_analysis", + "type": "skill", + "target": "logcat_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/logcat_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_analysis", + "type": "skill", + "target": "memory_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/memory_analysis.skill.yaml", + "parameters": { + "gc_count_critical": 100, + "gc_count_warning": 50, + "gc_total_time_critical_ms": 2000, + "main_thread_gc_critical": 10, + "single_gc_warning_ms": 50 + }, + "required_steps": [ + "get_process" + ], + "semantic_step": "get_process", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_growth_detector", + "type": "skill", + "target": "memory_growth_detector", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_growth_detector.skill.yaml", + "parameters": { + "growth_warning_mb": 50, + "growth_pct_min_mb": 5, + "growth_warning_pct": 20, + "growth_critical_pct": 50, + "jump_warning_mb": 10, + "peak_avg_warning_ratio": 2 + }, + "required_steps": [ + "memory_growth" + ], + "semantic_step": "memory_growth", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_module", + "type": "skill", + "target": "memory_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/memory_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "memory_overview" + ], + "semantic_step": "memory_overview", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_pressure_in_range", + "type": "skill", + "target": "memory_pressure_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_pressure_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "memory_pressure_analysis" + ], + "semantic_step": "memory_pressure_analysis", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_rss_high_watermark", + "type": "skill", + "target": "memory_rss_high_watermark", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_rss_high_watermark.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "rss_peaks" + ], + "semantic_step": "rss_peaks", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-native_heap_breakdown", + "type": "skill", + "target": "native_heap_breakdown", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/native_heap_breakdown.skill.yaml", + "parameters": {}, + "required_steps": [ + "native_heap_hotspots" + ], + "semantic_step": "native_heap_hotspots", + "limitation_reason": "The fixture does not contain heapprofd allocation packets.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-oom_adjuster_score_timeline", + "type": "skill", + "target": "oom_adjuster_score_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/oom_adjuster_score_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "oom_intervals" + ], + "semantic_step": "oom_intervals", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "strategy-memory", + "type": "strategy", + "target": "memory", + "query": "内存", + "expected_strategy": "memory", + "source_file": "backend/strategies/memory.strategy.md", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + } + ] +} diff --git a/Trace/constructed/memory-gc-pressure/case.json b/Trace/constructed/memory-gc-pressure/case.json new file mode 100644 index 000000000..c11e99dae --- /dev/null +++ b/Trace/constructed/memory-gc-pressure/case.json @@ -0,0 +1,355 @@ +{ + "schema_version": 1, + "id": "memory-gc-pressure", + "kind": "constructed", + "title": "Memory and GC pressure", + "description": "Deterministic memory and gc pressure signals over the android-startup-light real base trace.", + "scene": "memory", + "tags": [ + "android", + "constructed", + "memory" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "4587bab434e00b01b06c43fc676a0bac58d3ecd5eed40be4f6b3cbe0ed967c67", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Google raven base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-light", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "memory-gc-pressure-v1", + "output": "Trace/.generated/constructed/memory-gc-pressure/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_bitmap_memory_per_process", + "android_heap_graph_leak_candidates", + "android_heap_graph_summary", + "android_memory_v57_ai_diagnostics", + "dmabuf_analysis", + "gc_analysis", + "gc_events_in_range", + "linux_process_rss_swap_timeline", + "lmk_analysis", + "lmk_kill_attribution", + "logcat_analysis", + "memory_analysis", + "memory_growth_detector", + "memory_module", + "memory_pressure_in_range", + "memory_rss_high_watermark", + "native_heap_breakdown", + "oom_adjuster_score_timeline" + ], + "strategies": [ + "memory" + ], + "expectations": [ + { + "id": "execute-android_bitmap_memory_per_process", + "type": "skill", + "target": "android_bitmap_memory_per_process", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_bitmap_memory_per_process.skill.yaml", + "parameters": {}, + "required_steps": [ + "bitmap_memory" + ], + "semantic_step": "bitmap_memory", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_heap_graph_leak_candidates", + "type": "skill", + "target": "android_heap_graph_leak_candidates", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/android_heap_graph_leak_candidates.skill.yaml", + "parameters": { + "lifecycle_slice_prefix": "SI$", + "max_candidates": 50, + "max_reference_edges": 100 + }, + "required_steps": [ + "leak_candidates" + ], + "semantic_step": "leak_candidates", + "limitation_reason": "The fixture does not contain a managed heap graph dump.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_heap_graph_summary", + "type": "skill", + "target": "android_heap_graph_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_heap_graph_summary.skill.yaml", + "parameters": { + "max_rows": 30 + }, + "required_steps": [ + "heap_graph_availability" + ], + "semantic_step": "heap_graph_availability", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-android_memory_v57_ai_diagnostics", + "type": "skill", + "target": "android_memory_v57_ai_diagnostics", + "mode": "graceful_empty", + "source_file": "backend/skills/composite/android_memory_v57_ai_diagnostics.skill.yaml", + "parameters": { + "min_size_mb": 1, + "max_rows": 40 + }, + "required_steps": [ + "data_check", + "heap_graph_repeated_objects" + ], + "semantic_step": "heap_graph_repeated_objects", + "limitation_reason": "Heap-graph diagnostics remain empty without a managed heap graph dump.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-dmabuf_analysis", + "type": "skill", + "target": "dmabuf_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/dmabuf_analysis.skill.yaml", + "parameters": { + "min_size_mb": 1 + }, + "required_steps": [ + "data_check", + "dmabuf_overview" + ], + "semantic_step": "dmabuf_overview", + "limitation_reason": "The fixture does not contain DMA-BUF allocation/residency events.", + "expected_error": "Condition not met", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-gc_analysis", + "type": "skill", + "target": "gc_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/gc_analysis.skill.yaml", + "parameters": { + "min_gc_dur_ms": 5 + }, + "required_steps": [ + "data_check", + "gc_overview" + ], + "semantic_step": "gc_overview", + "limitation_reason": "The fixture has GC marker slices but not ART garbage-collection packets.", + "expected_error": "Condition not met", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "definition-gc_events_in_range", + "type": "skill", + "target": "gc_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gc_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-linux_process_rss_swap_timeline", + "type": "skill", + "target": "linux_process_rss_swap_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_process_rss_swap_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "rss_swap_peaks" + ], + "semantic_step": "rss_swap_peaks", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-lmk_analysis", + "type": "skill", + "target": "lmk_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/lmk_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "data_check", + "lmk_overview" + ], + "semantic_step": "lmk_overview", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-lmk_kill_attribution", + "type": "skill", + "target": "lmk_kill_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/lmk_kill_attribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "lmk_events" + ], + "semantic_step": "lmk_events", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "definition-logcat_analysis", + "type": "skill", + "target": "logcat_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/logcat_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_analysis", + "type": "skill", + "target": "memory_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/memory_analysis.skill.yaml", + "parameters": { + "gc_count_critical": 100, + "gc_count_warning": 50, + "gc_total_time_critical_ms": 2000, + "main_thread_gc_critical": 10, + "single_gc_warning_ms": 50 + }, + "required_steps": [ + "get_process" + ], + "semantic_step": "get_process", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_growth_detector", + "type": "skill", + "target": "memory_growth_detector", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_growth_detector.skill.yaml", + "parameters": { + "growth_warning_mb": 50, + "growth_pct_min_mb": 5, + "growth_warning_pct": 20, + "growth_critical_pct": 50, + "jump_warning_mb": 10, + "peak_avg_warning_ratio": 2 + }, + "required_steps": [ + "memory_growth" + ], + "semantic_step": "memory_growth", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_module", + "type": "skill", + "target": "memory_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/memory_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "memory_overview" + ], + "semantic_step": "memory_overview", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_pressure_in_range", + "type": "skill", + "target": "memory_pressure_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_pressure_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "memory_pressure_analysis" + ], + "semantic_step": "memory_pressure_analysis", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-memory_rss_high_watermark", + "type": "skill", + "target": "memory_rss_high_watermark", + "mode": "semantic", + "source_file": "backend/skills/atomic/memory_rss_high_watermark.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "rss_peaks" + ], + "semantic_step": "rss_peaks", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-native_heap_breakdown", + "type": "skill", + "target": "native_heap_breakdown", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/native_heap_breakdown.skill.yaml", + "parameters": {}, + "required_steps": [ + "native_heap_hotspots" + ], + "semantic_step": "native_heap_hotspots", + "limitation_reason": "The fixture does not contain heapprofd allocation packets.", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "execute-oom_adjuster_score_timeline", + "type": "skill", + "target": "oom_adjuster_score_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/oom_adjuster_score_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "oom_intervals" + ], + "semantic_step": "oom_intervals", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "id": "strategy-memory", + "type": "strategy", + "target": "memory", + "query": "内存", + "expected_strategy": "memory", + "source_file": "backend/strategies/memory.strategy.md", + "required_marker": "SmartPerfetto::CASE::memory-gc-pressure" + } + ] + } +} diff --git a/Trace/constructed/memory-gc-pressure/scenario.json b/Trace/constructed/memory-gc-pressure/scenario.json new file mode 100644 index 000000000..a919950fe --- /dev/null +++ b/Trace/constructed/memory-gc-pressure/scenario.json @@ -0,0 +1,162 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::memory-gc-pressure" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "GC Young Concurrent" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "HeapTrim" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "memory pressure" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.memory-gc-pressure.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "process-stats", + "at_ns": "400000000", + "process": "app", + "vm_rss_kb": 102400, + "rss_anon_kb": 81920, + "rss_file_kb": 20480, + "rss_shmem_kb": 1024, + "vm_swap_kb": 1024, + "vm_hwm_kb": 122880, + "oom_score_adj": 200 + }, + { + "type": "process-stats", + "at_ns": "600000000", + "process": "app", + "vm_rss_kb": 174080, + "rss_anon_kb": 133120, + "rss_file_kb": 40960, + "rss_shmem_kb": 2048, + "vm_swap_kb": 4096, + "vm_hwm_kb": 184320, + "oom_score_adj": 900 + }, + { + "type": "lmk-kill", + "at_ns": "700000000", + "duration_ns": "1000000", + "process": "app", + "thread": "main", + "kill_reason": 3, + "oom_score_adj": 900 + } + ] +} diff --git a/Trace/constructed/memory-gc-pressure/trace.overlay.pftrace b/Trace/constructed/memory-gc-pressure/trace.overlay.pftrace new file mode 100644 index 000000000..d85d2e11e Binary files /dev/null and b/Trace/constructed/memory-gc-pressure/trace.overlay.pftrace differ diff --git a/Trace/constructed/power-thermal/analysis/expected.json b/Trace/constructed/power-thermal/analysis/expected.json new file mode 100644 index 000000000..747754a57 --- /dev/null +++ b/Trace/constructed/power-thermal/analysis/expected.json @@ -0,0 +1,268 @@ +{ + "schema_version": 1, + "case_id": "power-thermal", + "marker": "SmartPerfetto::CASE::power-thermal", + "expectations": [ + { + "id": "execute-android_dvfs_counter_stats", + "type": "skill", + "target": "android_dvfs_counter_stats", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_dvfs_counter_stats.skill.yaml", + "parameters": {}, + "required_steps": [ + "dvfs_stats" + ], + "semantic_step": "dvfs_stats", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_charge_timeline", + "type": "skill", + "target": "battery_charge_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_charge_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "battery_samples" + ], + "semantic_step": "battery_samples", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_drain_attribution", + "type": "skill", + "target": "battery_drain_attribution", + "mode": "semantic", + "source_file": "backend/skills/composite/battery_drain_attribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "battery_drain_rate" + ], + "semantic_step": "battery_drain_rate", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_drain_rate_summary", + "type": "skill", + "target": "battery_drain_rate_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_drain_rate_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "drain_rate" + ], + "semantic_step": "drain_rate", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "definition-gpu_power_state_analysis", + "type": "skill", + "target": "gpu_power_state_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_power_state_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-mali_gpu_power_state", + "type": "skill", + "target": "mali_gpu_power_state", + "mode": "semantic", + "source_file": "backend/skills/atomic/mali_gpu_power_state.skill.yaml", + "parameters": {}, + "required_steps": [ + "mali_state" + ], + "semantic_step": "mali_state", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_consumption_overview", + "type": "skill", + "target": "power_consumption_overview", + "mode": "semantic", + "source_file": "backend/skills/composite/power_consumption_overview.skill.yaml", + "parameters": {}, + "required_steps": [ + "hardware_rails" + ], + "semantic_step": "hardware_rails", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_module", + "type": "skill", + "target": "power_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/power_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_overview" + ], + "semantic_step": "wakelock_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_rails_energy_breakdown", + "type": "skill", + "target": "power_rails_energy_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/power_rails_energy_breakdown.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "rail_energy" + ], + "semantic_step": "rail_energy", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-suspend_wakeup_analysis", + "type": "skill", + "target": "suspend_wakeup_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/suspend_wakeup_analysis.skill.yaml", + "parameters": { + "frequent_wakeup_critical": 50, + "frequent_wakeup_warning": 20, + "abort_pct_critical": 30, + "abort_pct_warning": 10, + "low_suspend_pct": 30 + }, + "required_steps": [ + "check_suspend_data", + "check_wakeup_data", + "check_suspend_slices", + "suspend_overview" + ], + "semantic_step": "suspend_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_module", + "type": "skill", + "target": "thermal_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/thermal_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "temperature_overview" + ], + "semantic_step": "temperature_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "definition-thermal_predictor", + "type": "skill", + "target": "thermal_predictor", + "mode": "definition", + "source_file": "backend/skills/atomic/thermal_predictor.skill.yaml", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_throttling", + "type": "skill", + "target": "thermal_throttling", + "mode": "semantic", + "source_file": "backend/skills/composite/thermal_throttling.skill.yaml", + "parameters": { + "enable_expert_probes": true, + "thermal_predictor_high_drop_pct": 30, + "thermal_predictor_medium_drop_pct": 15, + "thermal_predictor_high_core_ratio_pct": 50, + "thermal_predictor_medium_core_ratio_pct": 25, + "thermal_predictor_core_drop_threshold_pct": 30, + "gpu_transition_threshold_pct": 12, + "gpu_downshift_warning_pct": 25 + }, + "required_steps": [ + "data_check", + "expert_analysis_window", + "thermal_predictor_probe", + "thermal_overview" + ], + "semantic_step": "thermal_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_throttling_chain", + "type": "skill", + "target": "thermal_throttling_chain", + "mode": "semantic", + "source_file": "backend/skills/composite/thermal_throttling_chain.skill.yaml", + "parameters": {}, + "required_steps": [ + "thermal" + ], + "semantic_step": "thermal", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wakeup_frequency_summary", + "type": "skill", + "target": "wakeup_frequency_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/wakeup_frequency_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakeup_frequency" + ], + "semantic_step": "wakeup_frequency", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_app_startup_power", + "type": "skill", + "target": "wattson_app_startup_power", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/wattson_app_startup_power.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_power" + ], + "semantic_step": "startup_power", + "limitation_reason": "Wattson startup attribution is device-model gated and unsupported by this base device.", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_rails_power_breakdown", + "type": "skill", + "target": "wattson_rails_power_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/wattson_rails_power_breakdown.skill.yaml", + "parameters": { + "top_n": 20 + }, + "required_steps": [ + "rails_aggregation" + ], + "semantic_step": "rails_aggregation", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_thread_power_attribution", + "type": "skill", + "target": "wattson_thread_power_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/wattson_thread_power_attribution.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "thread_attribution" + ], + "semantic_step": "thread_attribution", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "strategy-power", + "type": "strategy", + "target": "power", + "query": "功耗", + "expected_strategy": "power", + "source_file": "backend/strategies/power.strategy.md", + "required_marker": "SmartPerfetto::CASE::power-thermal" + } + ] +} diff --git a/Trace/constructed/power-thermal/case.json b/Trace/constructed/power-thermal/case.json new file mode 100644 index 000000000..f3e7ff70f --- /dev/null +++ b/Trace/constructed/power-thermal/case.json @@ -0,0 +1,341 @@ +{ + "schema_version": 1, + "id": "power-thermal", + "kind": "constructed", + "title": "Power and thermal throttling", + "description": "Deterministic power and thermal throttling signals over the android-scroll-customer real base trace.", + "scene": "power", + "tags": [ + "android", + "constructed", + "power" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "d821be29b55da321f6c2578846bd8dc7acadee49d6be15757d2709c783b24246", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "power-thermal-v1", + "output": "Trace/.generated/constructed/power-thermal/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_dvfs_counter_stats", + "battery_charge_timeline", + "battery_drain_attribution", + "battery_drain_rate_summary", + "gpu_power_state_analysis", + "mali_gpu_power_state", + "power_consumption_overview", + "power_module", + "power_rails_energy_breakdown", + "suspend_wakeup_analysis", + "thermal_module", + "thermal_predictor", + "thermal_throttling", + "thermal_throttling_chain", + "wakeup_frequency_summary", + "wattson_app_startup_power", + "wattson_rails_power_breakdown", + "wattson_thread_power_attribution" + ], + "strategies": [ + "power" + ], + "expectations": [ + { + "id": "execute-android_dvfs_counter_stats", + "type": "skill", + "target": "android_dvfs_counter_stats", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_dvfs_counter_stats.skill.yaml", + "parameters": {}, + "required_steps": [ + "dvfs_stats" + ], + "semantic_step": "dvfs_stats", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_charge_timeline", + "type": "skill", + "target": "battery_charge_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_charge_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "battery_samples" + ], + "semantic_step": "battery_samples", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_drain_attribution", + "type": "skill", + "target": "battery_drain_attribution", + "mode": "semantic", + "source_file": "backend/skills/composite/battery_drain_attribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "battery_drain_rate" + ], + "semantic_step": "battery_drain_rate", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-battery_drain_rate_summary", + "type": "skill", + "target": "battery_drain_rate_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/battery_drain_rate_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "drain_rate" + ], + "semantic_step": "drain_rate", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "definition-gpu_power_state_analysis", + "type": "skill", + "target": "gpu_power_state_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_power_state_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-mali_gpu_power_state", + "type": "skill", + "target": "mali_gpu_power_state", + "mode": "semantic", + "source_file": "backend/skills/atomic/mali_gpu_power_state.skill.yaml", + "parameters": {}, + "required_steps": [ + "mali_state" + ], + "semantic_step": "mali_state", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_consumption_overview", + "type": "skill", + "target": "power_consumption_overview", + "mode": "semantic", + "source_file": "backend/skills/composite/power_consumption_overview.skill.yaml", + "parameters": {}, + "required_steps": [ + "hardware_rails" + ], + "semantic_step": "hardware_rails", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_module", + "type": "skill", + "target": "power_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/power_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakelock_overview" + ], + "semantic_step": "wakelock_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-power_rails_energy_breakdown", + "type": "skill", + "target": "power_rails_energy_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/power_rails_energy_breakdown.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "rail_energy" + ], + "semantic_step": "rail_energy", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-suspend_wakeup_analysis", + "type": "skill", + "target": "suspend_wakeup_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/suspend_wakeup_analysis.skill.yaml", + "parameters": { + "frequent_wakeup_critical": 50, + "frequent_wakeup_warning": 20, + "abort_pct_critical": 30, + "abort_pct_warning": 10, + "low_suspend_pct": 30 + }, + "required_steps": [ + "check_suspend_data", + "check_wakeup_data", + "check_suspend_slices", + "suspend_overview" + ], + "semantic_step": "suspend_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_module", + "type": "skill", + "target": "thermal_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/thermal_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "temperature_overview" + ], + "semantic_step": "temperature_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "definition-thermal_predictor", + "type": "skill", + "target": "thermal_predictor", + "mode": "definition", + "source_file": "backend/skills/atomic/thermal_predictor.skill.yaml", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_throttling", + "type": "skill", + "target": "thermal_throttling", + "mode": "semantic", + "source_file": "backend/skills/composite/thermal_throttling.skill.yaml", + "parameters": { + "enable_expert_probes": true, + "thermal_predictor_high_drop_pct": 30, + "thermal_predictor_medium_drop_pct": 15, + "thermal_predictor_high_core_ratio_pct": 50, + "thermal_predictor_medium_core_ratio_pct": 25, + "thermal_predictor_core_drop_threshold_pct": 30, + "gpu_transition_threshold_pct": 12, + "gpu_downshift_warning_pct": 25 + }, + "required_steps": [ + "data_check", + "expert_analysis_window", + "thermal_predictor_probe", + "thermal_overview" + ], + "semantic_step": "thermal_overview", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-thermal_throttling_chain", + "type": "skill", + "target": "thermal_throttling_chain", + "mode": "semantic", + "source_file": "backend/skills/composite/thermal_throttling_chain.skill.yaml", + "parameters": {}, + "required_steps": [ + "thermal" + ], + "semantic_step": "thermal", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wakeup_frequency_summary", + "type": "skill", + "target": "wakeup_frequency_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/wakeup_frequency_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "wakeup_frequency" + ], + "semantic_step": "wakeup_frequency", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_app_startup_power", + "type": "skill", + "target": "wattson_app_startup_power", + "mode": "graceful_empty", + "source_file": "backend/skills/atomic/wattson_app_startup_power.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_power" + ], + "semantic_step": "startup_power", + "limitation_reason": "Wattson startup attribution is device-model gated and unsupported by this base device.", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_rails_power_breakdown", + "type": "skill", + "target": "wattson_rails_power_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/wattson_rails_power_breakdown.skill.yaml", + "parameters": { + "top_n": 20 + }, + "required_steps": [ + "rails_aggregation" + ], + "semantic_step": "rails_aggregation", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "execute-wattson_thread_power_attribution", + "type": "skill", + "target": "wattson_thread_power_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/wattson_thread_power_attribution.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "thread_attribution" + ], + "semantic_step": "thread_attribution", + "required_marker": "SmartPerfetto::CASE::power-thermal" + }, + { + "id": "strategy-power", + "type": "strategy", + "target": "power", + "query": "功耗", + "expected_strategy": "power", + "source_file": "backend/strategies/power.strategy.md", + "required_marker": "SmartPerfetto::CASE::power-thermal" + } + ] + } +} diff --git a/Trace/constructed/power-thermal/scenario.json b/Trace/constructed/power-thermal/scenario.json new file mode 100644 index 000000000..b2eadb150 --- /dev/null +++ b/Trace/constructed/power-thermal/scenario.json @@ -0,0 +1,223 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::power-thermal" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "thermal_throttling" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "WakeLock" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "device_suspend" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.power-thermal.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "battery-counters", + "at_ns": "400000000", + "capacity_percent": 80, + "charge_counter_uah": "4000000", + "current_ua": "-500000", + "voltage_uv": "4000000" + }, + { + "type": "battery-counters", + "at_ns": "600000000", + "capacity_percent": 79, + "charge_counter_uah": "3999000", + "current_ua": "-550000", + "voltage_uv": "3980000" + }, + { + "type": "atrace-counter", + "at_ns": "410000000", + "process": "system", + "thread": "system-main", + "name": "DozeDeepState", + "value": 5 + }, + { + "type": "atrace-counter", + "at_ns": "610000000", + "process": "system", + "thread": "system-main", + "name": "DozeDeepState", + "value": 0 + }, + { + "type": "atrace-counter", + "at_ns": "420000000", + "process": "system", + "thread": "system-main", + "name": "domain@0 Frequency", + "value": 800000 + }, + { + "type": "atrace-counter", + "at_ns": "620000000", + "process": "system", + "thread": "system-main", + "name": "domain@0 Frequency", + "value": 1600000 + }, + { + "type": "power-rail", + "at_ns": "430000000", + "duration_ns": "200000000", + "name": "SYNTHETIC_CPU", + "subsystem": "CPU", + "start_energy_uws": "1000000", + "end_energy_uws": "1300000" + }, + { + "type": "gpu-work-period", + "at_ns": "450000000", + "duration_ns": "30000000", + "gpu_id": 0, + "uid": 10999, + "active_duration_ns": "24000000", + "cpu": 0 + }, + { + "type": "gpu-frequency", + "at_ns": "450000000", + "gpu_id": 0, + "value": 700000, + "cpu": 0 + }, + { + "type": "gpu-power-state", + "at_ns": "450000000", + "old_state": 0, + "new_state": 2, + "cpu": 0 + }, + { + "type": "cpu-frequency", + "at_ns": "460000000", + "cpu_id": 0, + "value": 1800000, + "cpu": 0 + }, + { + "type": "cpu-idle", + "at_ns": "470000000", + "cpu_id": 0, + "state": 1, + "cpu": 0 + } + ] +} diff --git a/Trace/constructed/power-thermal/trace.overlay.pftrace b/Trace/constructed/power-thermal/trace.overlay.pftrace new file mode 100644 index 000000000..df9f5d288 Binary files /dev/null and b/Trace/constructed/power-thermal/trace.overlay.pftrace differ diff --git a/Trace/constructed/rendering-jank/analysis/expected.json b/Trace/constructed/rendering-jank/analysis/expected.json new file mode 100644 index 000000000..195f6fb26 --- /dev/null +++ b/Trace/constructed/rendering-jank/analysis/expected.json @@ -0,0 +1,582 @@ +{ + "schema_version": 1, + "case_id": "rendering-jank", + "marker": "SmartPerfetto::CASE::rendering-jank", + "expectations": [ + { + "id": "execute-app_frame_production", + "type": "skill", + "target": "app_frame_production", + "mode": "semantic", + "source_file": "backend/skills/atomic/app_frame_production.skill.yaml", + "parameters": {}, + "required_steps": [ + "app_production_stats" + ], + "semantic_step": "app_production_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-buffer_transaction_lifecycle", + "type": "skill", + "target": "buffer_transaction_lifecycle", + "mode": "semantic", + "source_file": "backend/skills/atomic/buffer_transaction_lifecycle.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "apply_transaction_summary" + ], + "semantic_step": "apply_transaction_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-choreographer_module", + "type": "skill", + "target": "choreographer_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/choreographer_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "doframe_overview" + ], + "semantic_step": "doframe_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-chrome_scroll_jank_frame_timeline", + "type": "skill", + "target": "chrome_scroll_jank_frame_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/chrome_scroll_jank_frame_timeline.skill.yaml", + "parameters": { + "min_janky_frames": 1 + }, + "required_steps": [ + "chrome_trace_availability" + ], + "semantic_step": "chrome_trace_availability", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-consumer_jank_detection", + "type": "skill", + "target": "consumer_jank_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/consumer_jank_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_config", + "consumer_jank_frames" + ], + "semantic_step": "consumer_jank_frames", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-flutter_scrolling_analysis", + "type": "skill", + "target": "flutter_scrolling_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/flutter_scrolling_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "flutter_frame_overview" + ], + "semantic_step": "flutter_frame_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-fpsgo_analysis", + "type": "skill", + "target": "fpsgo_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/fpsgo_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-frame_blocking_calls", + "type": "skill", + "target": "frame_blocking_calls", + "mode": "definition", + "source_file": "backend/skills/atomic/frame_blocking_calls.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_overrun_summary", + "type": "skill", + "target": "frame_overrun_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_overrun_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "overrun_frames" + ], + "semantic_step": "overrun_frames", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-frame_pipeline_variance", + "type": "skill", + "target": "frame_pipeline_variance", + "mode": "definition", + "source_file": "backend/skills/atomic/frame_pipeline_variance.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_production_gap", + "type": "skill", + "target": "frame_production_gap", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_production_gap.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "gap_summary" + ], + "semantic_step": "gap_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_ui_time_breakdown", + "type": "skill", + "target": "frame_ui_time_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_ui_time_breakdown.skill.yaml", + "parameters": {}, + "required_steps": [ + "ui_time" + ], + "semantic_step": "ui_time", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-game_fps_analysis", + "type": "skill", + "target": "game_fps_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/game_fps_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_target_fps" + ], + "semantic_step": "detect_target_fps", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-game_main_loop_jank", + "type": "skill", + "target": "game_main_loop_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/game_main_loop_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "engine_loop_summary" + ], + "semantic_step": "engine_loop_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-gl_standalone_swap_jank", + "type": "skill", + "target": "gl_standalone_swap_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/gl_standalone_swap_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "swap_interval_summary" + ], + "semantic_step": "swap_interval_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-gpu_render_in_range", + "type": "skill", + "target": "gpu_render_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_render_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-input_to_frame_latency", + "type": "skill", + "target": "input_to_frame_latency", + "mode": "semantic", + "source_file": "backend/skills/atomic/input_to_frame_latency.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_period" + ], + "semantic_step": "vsync_period", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-jank_frame_detail", + "type": "skill", + "target": "jank_frame_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/jank_frame_detail.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture", + "pid": 700000, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "monitor_contention_check", + "gc_table_check", + "gpu_table_check", + "binder_table_check", + "quadrant_analysis" + ], + "semantic_step": "quadrant_analysis", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_flutter_textureview", + "type": "skill", + "target": "pipeline_flutter_textureview", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_textureview.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_hardware_buffer_renderer", + "type": "skill", + "target": "pipeline_hardware_buffer_renderer", + "mode": "definition", + "source_file": "backend/skills/pipelines/hardware_buffer_renderer.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_textureview_standard", + "type": "skill", + "target": "pipeline_textureview_standard", + "mode": "definition", + "source_file": "backend/skills/pipelines/textureview_standard.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_webview_textureview_custom", + "type": "skill", + "target": "pipeline_webview_textureview_custom", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_textureview_custom.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-present_fence_timing", + "type": "skill", + "target": "present_fence_timing", + "mode": "semantic", + "source_file": "backend/skills/atomic/present_fence_timing.skill.yaml", + "parameters": {}, + "required_steps": [ + "present_fence_stats" + ], + "semantic_step": "present_fence_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-render_pipeline_latency", + "type": "skill", + "target": "render_pipeline_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/render_pipeline_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-render_thread_slices", + "type": "skill", + "target": "render_thread_slices", + "mode": "semantic", + "source_file": "backend/skills/atomic/render_thread_slices.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "render_slices" + ], + "semantic_step": "render_slices", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rendering_pipeline_detection", + "type": "skill", + "target": "rendering_pipeline_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/rendering_pipeline_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "thread_signals" + ], + "semantic_step": "thread_signals", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rn_bridge_to_frame_jank", + "type": "skill", + "target": "rn_bridge_to_frame_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/rn_bridge_to_frame_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "rn_bridge_frame_overlap" + ], + "semantic_step": "rn_bridge_frame_overlap", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rn_fabric_render_jank", + "type": "skill", + "target": "rn_fabric_render_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/rn_fabric_render_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "fabric_frame_overlap" + ], + "semantic_step": "fabric_frame_overlap", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-scroll_response_latency", + "type": "skill", + "target": "scroll_response_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/scroll_response_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-scroll_session_analysis", + "type": "skill", + "target": "scroll_session_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/scroll_session_analysis.skill.yaml", + "parameters": { + "session_id": 1, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "full_session_stats" + ], + "semantic_step": "full_session_stats", + "limitation_reason": "The generator does not yet emit Winscope android_input_event packets.", + "expected_error": "Trace is missing required tables: android_input_event", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-scrolling_analysis", + "type": "skill", + "target": "scrolling_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/scrolling_analysis.skill.yaml", + "parameters": { + "enable_expert_probes": true, + "frame_variance_probe_min_janky_frames": 1, + "frame_variance_transition_threshold_ms": 1, + "input_handling_budget_ratio": 0.5, + "input_event_backlog_threshold": 3 + }, + "required_steps": [ + "init_cpu_topology", + "frame_timeline_check", + "vsync_config" + ], + "semantic_step": "vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-sf_composition_in_range", + "type": "skill", + "target": "sf_composition_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/sf_composition_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-sf_frame_consumption", + "type": "skill", + "target": "sf_frame_consumption", + "mode": "semantic", + "source_file": "backend/skills/atomic/sf_frame_consumption.skill.yaml", + "parameters": {}, + "required_steps": [ + "sf_consumption_stats" + ], + "semantic_step": "sf_consumption_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-sf_layer_count_in_range", + "type": "skill", + "target": "sf_layer_count_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/sf_layer_count_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "layer_summary" + ], + "semantic_step": "layer_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-surfaceflinger_analysis", + "type": "skill", + "target": "surfaceflinger_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/surfaceflinger_analysis.skill.yaml", + "parameters": { + "slow_composition_multiplier": 1.5, + "composition_rating_poor_ms": 12, + "composition_rating_fair_ms": 8, + "composition_rating_good_ms": 4, + "long_fence_threshold_ms": 10, + "fence_critical_ms": 16, + "fence_warning_ms": 8, + "slow_pct_threshold": 10, + "gpu_comp_ratio_threshold": 0.5 + }, + "required_steps": [ + "data_check", + "vsync_config" + ], + "semantic_step": "vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-surfaceflinger_module", + "type": "skill", + "target": "surfaceflinger_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/surfaceflinger_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "frame_overview" + ], + "semantic_step": "frame_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-textureview_producer_frame_timing", + "type": "skill", + "target": "textureview_producer_frame_timing", + "mode": "semantic", + "source_file": "backend/skills/atomic/textureview_producer_frame_timing.skill.yaml", + "parameters": {}, + "required_steps": [ + "textureview_signal_summary" + ], + "semantic_step": "textureview_signal_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vrr_detection", + "type": "skill", + "target": "vrr_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/vrr_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_interval_distribution" + ], + "semantic_step": "vsync_interval_distribution", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-vsync_alignment_in_range", + "type": "skill", + "target": "vsync_alignment_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/vsync_alignment_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_config", + "type": "skill", + "target": "vsync_config", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_config.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_vsync_config" + ], + "semantic_step": "detect_vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_period_detection", + "type": "skill", + "target": "vsync_period_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_period_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_vsync_period" + ], + "semantic_step": "detect_vsync_period", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_phase_alignment", + "type": "skill", + "target": "vsync_phase_alignment", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_phase_alignment.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_timeline" + ], + "semantic_step": "vsync_timeline", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-webview_drawfunctor_jank_chain", + "type": "skill", + "target": "webview_drawfunctor_jank_chain", + "mode": "semantic", + "source_file": "backend/skills/composite/webview_drawfunctor_jank_chain.skill.yaml", + "parameters": {}, + "required_steps": [ + "drawfunctor_signal_summary" + ], + "semantic_step": "drawfunctor_signal_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-game", + "type": "strategy", + "target": "game", + "query": "游戏", + "expected_strategy": "game", + "source_file": "backend/strategies/game.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-scroll_response", + "type": "strategy", + "target": "scroll_response", + "query": "滑动响应", + "expected_strategy": "scroll_response", + "source_file": "backend/strategies/scroll-response.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-scrolling", + "type": "strategy", + "target": "scrolling", + "query": "滑动", + "expected_strategy": "scrolling", + "source_file": "backend/strategies/scrolling.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + } + ] +} diff --git a/Trace/constructed/rendering-jank/case.json b/Trace/constructed/rendering-jank/case.json new file mode 100644 index 000000000..322e8108c --- /dev/null +++ b/Trace/constructed/rendering-jank/case.json @@ -0,0 +1,682 @@ +{ + "schema_version": 1, + "id": "rendering-jank", + "kind": "constructed", + "title": "Rendering pipeline jank", + "description": "Deterministic rendering pipeline jank signals over the android-scroll-customer real base trace.", + "scene": "scrolling", + "tags": [ + "android", + "constructed", + "scrolling" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "3545492fc56cacbc73022aae8db4528c9c8f3fe54df733ff6273d95a416b2f55", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "rendering-jank-v1", + "output": "Trace/.generated/constructed/rendering-jank/trace.pftrace" + }, + "coverage": { + "skills": [ + "app_frame_production", + "buffer_transaction_lifecycle", + "choreographer_module", + "chrome_scroll_jank_frame_timeline", + "consumer_jank_detection", + "flutter_scrolling_analysis", + "fpsgo_analysis", + "frame_blocking_calls", + "frame_overrun_summary", + "frame_pipeline_variance", + "frame_production_gap", + "frame_ui_time_breakdown", + "game_fps_analysis", + "game_main_loop_jank", + "gl_standalone_swap_jank", + "gpu_render_in_range", + "input_to_frame_latency", + "jank_frame_detail", + "pipeline_flutter_textureview", + "pipeline_hardware_buffer_renderer", + "pipeline_textureview_standard", + "pipeline_webview_textureview_custom", + "present_fence_timing", + "render_pipeline_latency", + "render_thread_slices", + "rendering_pipeline_detection", + "rn_bridge_to_frame_jank", + "rn_fabric_render_jank", + "scroll_response_latency", + "scroll_session_analysis", + "scrolling_analysis", + "sf_composition_in_range", + "sf_frame_consumption", + "sf_layer_count_in_range", + "surfaceflinger_analysis", + "surfaceflinger_module", + "textureview_producer_frame_timing", + "vrr_detection", + "vsync_alignment_in_range", + "vsync_config", + "vsync_period_detection", + "vsync_phase_alignment", + "webview_drawfunctor_jank_chain" + ], + "strategies": [ + "game", + "scroll_response", + "scrolling" + ], + "expectations": [ + { + "id": "execute-app_frame_production", + "type": "skill", + "target": "app_frame_production", + "mode": "semantic", + "source_file": "backend/skills/atomic/app_frame_production.skill.yaml", + "parameters": {}, + "required_steps": [ + "app_production_stats" + ], + "semantic_step": "app_production_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-buffer_transaction_lifecycle", + "type": "skill", + "target": "buffer_transaction_lifecycle", + "mode": "semantic", + "source_file": "backend/skills/atomic/buffer_transaction_lifecycle.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "apply_transaction_summary" + ], + "semantic_step": "apply_transaction_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-choreographer_module", + "type": "skill", + "target": "choreographer_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/choreographer_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "doframe_overview" + ], + "semantic_step": "doframe_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-chrome_scroll_jank_frame_timeline", + "type": "skill", + "target": "chrome_scroll_jank_frame_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/chrome_scroll_jank_frame_timeline.skill.yaml", + "parameters": { + "min_janky_frames": 1 + }, + "required_steps": [ + "chrome_trace_availability" + ], + "semantic_step": "chrome_trace_availability", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-consumer_jank_detection", + "type": "skill", + "target": "consumer_jank_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/consumer_jank_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_config", + "consumer_jank_frames" + ], + "semantic_step": "consumer_jank_frames", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-flutter_scrolling_analysis", + "type": "skill", + "target": "flutter_scrolling_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/flutter_scrolling_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "flutter_frame_overview" + ], + "semantic_step": "flutter_frame_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-fpsgo_analysis", + "type": "skill", + "target": "fpsgo_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/fpsgo_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-frame_blocking_calls", + "type": "skill", + "target": "frame_blocking_calls", + "mode": "definition", + "source_file": "backend/skills/atomic/frame_blocking_calls.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_overrun_summary", + "type": "skill", + "target": "frame_overrun_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_overrun_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "overrun_frames" + ], + "semantic_step": "overrun_frames", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-frame_pipeline_variance", + "type": "skill", + "target": "frame_pipeline_variance", + "mode": "definition", + "source_file": "backend/skills/atomic/frame_pipeline_variance.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_production_gap", + "type": "skill", + "target": "frame_production_gap", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_production_gap.skill.yaml", + "parameters": { + "process_name": "com.smartperfetto.fixture" + }, + "required_steps": [ + "gap_summary" + ], + "semantic_step": "gap_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-frame_ui_time_breakdown", + "type": "skill", + "target": "frame_ui_time_breakdown", + "mode": "semantic", + "source_file": "backend/skills/atomic/frame_ui_time_breakdown.skill.yaml", + "parameters": {}, + "required_steps": [ + "ui_time" + ], + "semantic_step": "ui_time", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-game_fps_analysis", + "type": "skill", + "target": "game_fps_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/game_fps_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_target_fps" + ], + "semantic_step": "detect_target_fps", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-game_main_loop_jank", + "type": "skill", + "target": "game_main_loop_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/game_main_loop_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "engine_loop_summary" + ], + "semantic_step": "engine_loop_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-gl_standalone_swap_jank", + "type": "skill", + "target": "gl_standalone_swap_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/gl_standalone_swap_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "swap_interval_summary" + ], + "semantic_step": "swap_interval_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-gpu_render_in_range", + "type": "skill", + "target": "gpu_render_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/gpu_render_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-input_to_frame_latency", + "type": "skill", + "target": "input_to_frame_latency", + "mode": "semantic", + "source_file": "backend/skills/atomic/input_to_frame_latency.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_period" + ], + "semantic_step": "vsync_period", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-jank_frame_detail", + "type": "skill", + "target": "jank_frame_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/jank_frame_detail.skill.yaml", + "parameters": { + "package": "com.smartperfetto.fixture", + "pid": 700000, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "monitor_contention_check", + "gc_table_check", + "gpu_table_check", + "binder_table_check", + "quadrant_analysis" + ], + "semantic_step": "quadrant_analysis", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_flutter_textureview", + "type": "skill", + "target": "pipeline_flutter_textureview", + "mode": "definition", + "source_file": "backend/skills/pipelines/flutter_textureview.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_hardware_buffer_renderer", + "type": "skill", + "target": "pipeline_hardware_buffer_renderer", + "mode": "definition", + "source_file": "backend/skills/pipelines/hardware_buffer_renderer.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_textureview_standard", + "type": "skill", + "target": "pipeline_textureview_standard", + "mode": "definition", + "source_file": "backend/skills/pipelines/textureview_standard.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-pipeline_webview_textureview_custom", + "type": "skill", + "target": "pipeline_webview_textureview_custom", + "mode": "definition", + "source_file": "backend/skills/pipelines/webview_textureview_custom.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-present_fence_timing", + "type": "skill", + "target": "present_fence_timing", + "mode": "semantic", + "source_file": "backend/skills/atomic/present_fence_timing.skill.yaml", + "parameters": {}, + "required_steps": [ + "present_fence_stats" + ], + "semantic_step": "present_fence_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-render_pipeline_latency", + "type": "skill", + "target": "render_pipeline_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/render_pipeline_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-render_thread_slices", + "type": "skill", + "target": "render_thread_slices", + "mode": "semantic", + "source_file": "backend/skills/atomic/render_thread_slices.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "render_slices" + ], + "semantic_step": "render_slices", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rendering_pipeline_detection", + "type": "skill", + "target": "rendering_pipeline_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/rendering_pipeline_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "thread_signals" + ], + "semantic_step": "thread_signals", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rn_bridge_to_frame_jank", + "type": "skill", + "target": "rn_bridge_to_frame_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/rn_bridge_to_frame_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "rn_bridge_frame_overlap" + ], + "semantic_step": "rn_bridge_frame_overlap", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-rn_fabric_render_jank", + "type": "skill", + "target": "rn_fabric_render_jank", + "mode": "semantic", + "source_file": "backend/skills/atomic/rn_fabric_render_jank.skill.yaml", + "parameters": {}, + "required_steps": [ + "fabric_frame_overlap" + ], + "semantic_step": "fabric_frame_overlap", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-scroll_response_latency", + "type": "skill", + "target": "scroll_response_latency", + "mode": "definition", + "source_file": "backend/skills/atomic/scroll_response_latency.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-scroll_session_analysis", + "type": "skill", + "target": "scroll_session_analysis", + "mode": "unavailable", + "source_file": "backend/skills/composite/scroll_session_analysis.skill.yaml", + "parameters": { + "session_id": 1, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "full_session_stats" + ], + "semantic_step": "full_session_stats", + "limitation_reason": "The generator does not yet emit Winscope android_input_event packets.", + "expected_error": "Trace is missing required tables: android_input_event", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-scrolling_analysis", + "type": "skill", + "target": "scrolling_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/scrolling_analysis.skill.yaml", + "parameters": { + "enable_expert_probes": true, + "frame_variance_probe_min_janky_frames": 1, + "frame_variance_transition_threshold_ms": 1, + "input_handling_budget_ratio": 0.5, + "input_event_backlog_threshold": 3 + }, + "required_steps": [ + "init_cpu_topology", + "frame_timeline_check", + "vsync_config" + ], + "semantic_step": "vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-sf_composition_in_range", + "type": "skill", + "target": "sf_composition_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/sf_composition_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-sf_frame_consumption", + "type": "skill", + "target": "sf_frame_consumption", + "mode": "semantic", + "source_file": "backend/skills/atomic/sf_frame_consumption.skill.yaml", + "parameters": {}, + "required_steps": [ + "sf_consumption_stats" + ], + "semantic_step": "sf_consumption_stats", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-sf_layer_count_in_range", + "type": "skill", + "target": "sf_layer_count_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/sf_layer_count_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "layer_summary" + ], + "semantic_step": "layer_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-surfaceflinger_analysis", + "type": "skill", + "target": "surfaceflinger_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/surfaceflinger_analysis.skill.yaml", + "parameters": { + "slow_composition_multiplier": 1.5, + "composition_rating_poor_ms": 12, + "composition_rating_fair_ms": 8, + "composition_rating_good_ms": 4, + "long_fence_threshold_ms": 10, + "fence_critical_ms": 16, + "fence_warning_ms": 8, + "slow_pct_threshold": 10, + "gpu_comp_ratio_threshold": 0.5 + }, + "required_steps": [ + "data_check", + "vsync_config" + ], + "semantic_step": "vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-surfaceflinger_module", + "type": "skill", + "target": "surfaceflinger_module", + "mode": "semantic", + "source_file": "backend/skills/modules/framework/surfaceflinger_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "frame_overview" + ], + "semantic_step": "frame_overview", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-textureview_producer_frame_timing", + "type": "skill", + "target": "textureview_producer_frame_timing", + "mode": "semantic", + "source_file": "backend/skills/atomic/textureview_producer_frame_timing.skill.yaml", + "parameters": {}, + "required_steps": [ + "textureview_signal_summary" + ], + "semantic_step": "textureview_signal_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vrr_detection", + "type": "skill", + "target": "vrr_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/vrr_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_interval_distribution" + ], + "semantic_step": "vsync_interval_distribution", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "definition-vsync_alignment_in_range", + "type": "skill", + "target": "vsync_alignment_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/vsync_alignment_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_config", + "type": "skill", + "target": "vsync_config", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_config.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_vsync_config" + ], + "semantic_step": "detect_vsync_config", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_period_detection", + "type": "skill", + "target": "vsync_period_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_period_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "detect_vsync_period" + ], + "semantic_step": "detect_vsync_period", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-vsync_phase_alignment", + "type": "skill", + "target": "vsync_phase_alignment", + "mode": "semantic", + "source_file": "backend/skills/atomic/vsync_phase_alignment.skill.yaml", + "parameters": {}, + "required_steps": [ + "vsync_timeline" + ], + "semantic_step": "vsync_timeline", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "execute-webview_drawfunctor_jank_chain", + "type": "skill", + "target": "webview_drawfunctor_jank_chain", + "mode": "semantic", + "source_file": "backend/skills/composite/webview_drawfunctor_jank_chain.skill.yaml", + "parameters": {}, + "required_steps": [ + "drawfunctor_signal_summary" + ], + "semantic_step": "drawfunctor_signal_summary", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-game", + "type": "strategy", + "target": "game", + "query": "游戏", + "expected_strategy": "game", + "source_file": "backend/strategies/game.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-scroll_response", + "type": "strategy", + "target": "scroll_response", + "query": "滑动响应", + "expected_strategy": "scroll_response", + "source_file": "backend/strategies/scroll-response.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + }, + { + "id": "strategy-scrolling", + "type": "strategy", + "target": "scrolling", + "query": "滑动", + "expected_strategy": "scrolling", + "source_file": "backend/strategies/scrolling.strategy.md", + "required_marker": "SmartPerfetto::CASE::rendering-jank" + } + ] + } +} diff --git a/Trace/constructed/rendering-jank/scenario.json b/Trace/constructed/rendering-jank/scenario.json new file mode 100644 index 000000000..c67d036a0 --- /dev/null +++ b/Trace/constructed/rendering-jank/scenario.json @@ -0,0 +1,227 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::rendering-jank" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "Choreographer#doFrame" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "DrawFrame" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "SurfaceFlinger::commit" + }, + { + "type": "atrace-slice", + "at_ns": "130000000", + "duration_ns": "22000000", + "process": "app", + "thread": "main", + "name": "queueBuffer" + }, + { + "type": "atrace-slice", + "at_ns": "200000000", + "duration_ns": "22000000", + "process": "app", + "thread": "game", + "name": "PlayerLoop" + }, + { + "type": "atrace-slice", + "at_ns": "225000000", + "duration_ns": "23000000", + "process": "app", + "thread": "game", + "name": "eglSwapBuffers" + }, + { + "type": "atrace-slice", + "at_ns": "250000000", + "duration_ns": "24000000", + "process": "app", + "thread": "game", + "name": "eglSwapBuffers" + }, + { + "type": "atrace-slice", + "at_ns": "275000000", + "duration_ns": "25000000", + "process": "app", + "thread": "game", + "name": "eglSwapBuffers" + }, + { + "type": "atrace-slice", + "at_ns": "300000000", + "duration_ns": "26000000", + "process": "app", + "thread": "rn-js", + "name": "BatchedBridge::callFunctionReturnFlushedQueue" + }, + { + "type": "atrace-slice", + "at_ns": "325000000", + "duration_ns": "27000000", + "process": "app", + "thread": "rn-native", + "name": "UIManager::dispatchViewUpdates" + }, + { + "type": "atrace-slice", + "at_ns": "350000000", + "duration_ns": "28000000", + "process": "app", + "thread": "rn-native", + "name": "FabricMount::executeMount" + }, + { + "type": "atrace-slice", + "at_ns": "375000000", + "duration_ns": "29000000", + "process": "app", + "thread": "webview", + "name": "DrawGL::DrawFunctor" + }, + { + "type": "atrace-slice", + "at_ns": "400000000", + "duration_ns": "30000000", + "process": "app", + "thread": "webview", + "name": "V8.GCCompactor" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.rendering-jank.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "frame-timeline", + "at_ns": "280000000", + "duration_ns": "160000000", + "process": "app", + "cookie": 101, + "token": 201, + "display_frame_token": 301, + "layer_name": "SyntheticJankLayer", + "jank_type": 64 + }, + { + "type": "gpu-frequency", + "at_ns": "500000000", + "gpu_id": 0, + "value": 700000, + "cpu": 0 + } + ] +} diff --git a/Trace/constructed/rendering-jank/trace.overlay.pftrace b/Trace/constructed/rendering-jank/trace.overlay.pftrace new file mode 100644 index 000000000..45cb9efcf Binary files /dev/null and b/Trace/constructed/rendering-jank/trace.overlay.pftrace differ diff --git a/Trace/constructed/scheduler-cpu-contention/analysis/expected.json b/Trace/constructed/scheduler-cpu-contention/analysis/expected.json new file mode 100644 index 000000000..6205e7659 --- /dev/null +++ b/Trace/constructed/scheduler-cpu-contention/analysis/expected.json @@ -0,0 +1,482 @@ +{ + "schema_version": 1, + "case_id": "scheduler-cpu-contention", + "marker": "SmartPerfetto::CASE::scheduler-cpu-contention", + "expectations": [ + { + "id": "execute-android_job_scheduler_events", + "type": "skill", + "target": "android_job_scheduler_events", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_job_scheduler_events.skill.yaml", + "parameters": {}, + "required_steps": [ + "job_events" + ], + "semantic_step": "job_events", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-cache_miss_impact", + "type": "skill", + "target": "cache_miss_impact", + "mode": "definition", + "source_file": "backend/skills/atomic/cache_miss_impact.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_analysis", + "type": "skill", + "target": "cpu_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/cpu_analysis.skill.yaml", + "parameters": { + "big_core_threshold_pct": 30, + "running_threshold_pct": 50, + "sched_delay_critical_ms": 16, + "blocked_function_threshold_ms": 50, + "enable_expert_probes": true, + "affinity_probe_migration_ratio_threshold_pct": 25, + "affinity_violation_warning_threshold_pct": 35, + "cache_miss_high_avg_delta_threshold": 500000, + "cache_miss_medium_avg_delta_threshold": 100000 + }, + "required_steps": [ + "init_cpu_topology", + "get_process" + ], + "semantic_step": "get_process", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_cluster_load_in_range", + "type": "skill", + "target": "cpu_cluster_load_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_cluster_load_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cluster_load" + ], + "semantic_step": "cluster_load", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_cluster_mapping_view", + "type": "skill", + "target": "cpu_cluster_mapping_view", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_cluster_mapping_view.skill.yaml", + "parameters": {}, + "required_steps": [ + "cluster_mapping" + ], + "semantic_step": "cluster_mapping", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_freq_residency_summary", + "type": "skill", + "target": "cpu_freq_residency_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_freq_residency_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "freq_residency" + ], + "semantic_step": "freq_residency", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_freq_timeline", + "type": "skill", + "target": "cpu_freq_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_freq_timeline.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cpu_freq_changes", + "cpu_freq_summary" + ], + "semantic_step": "cpu_freq_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-cpu_idle_analysis", + "type": "skill", + "target": "cpu_idle_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/cpu_idle_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_idle_state_residency", + "type": "skill", + "target": "cpu_idle_state_residency", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_idle_state_residency.skill.yaml", + "parameters": {}, + "required_steps": [ + "idle_residency" + ], + "semantic_step": "idle_residency", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_load_in_range", + "type": "skill", + "target": "cpu_load_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_load_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cpu_utilization" + ], + "semantic_step": "cpu_utilization", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_module", + "type": "skill", + "target": "cpu_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/cpu_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "frequency_overview" + ], + "semantic_step": "frequency_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_process_utilization_period", + "type": "skill", + "target": "cpu_process_utilization_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_process_utilization_period.skill.yaml", + "parameters": {}, + "required_steps": [ + "process_util" + ], + "semantic_step": "process_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_profiling", + "type": "skill", + "target": "cpu_profiling", + "mode": "semantic", + "source_file": "backend/skills/deep/cpu_profiling.skill.yaml", + "parameters": { + "min_runtime_ms": 1 + }, + "required_steps": [ + "init_cpu_topology", + "cpu_usage_overview" + ], + "semantic_step": "cpu_usage_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_slice_analysis", + "type": "skill", + "target": "cpu_slice_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_slice_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "cpu_time_by_core" + ], + "semantic_step": "cpu_time_by_core", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_thread_utilization_period", + "type": "skill", + "target": "cpu_thread_utilization_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_thread_utilization_period.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "thread_util" + ], + "semantic_step": "thread_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_throttling_in_range", + "type": "skill", + "target": "cpu_throttling_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_throttling_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "throttle_detection" + ], + "semantic_step": "throttle_detection", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_time_per_frame", + "type": "skill", + "target": "cpu_time_per_frame", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_time_per_frame.skill.yaml", + "parameters": {}, + "required_steps": [ + "cpu_time" + ], + "semantic_step": "cpu_time", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_topology_detection", + "type": "skill", + "target": "cpu_topology_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_topology_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "detect_cpu_clusters" + ], + "semantic_step": "detect_cpu_clusters", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_topology_view", + "type": "skill", + "target": "cpu_topology_view", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_topology_view.skill.yaml", + "parameters": {}, + "required_steps": [ + "inspect_existing_topology_object", + "drop_existing_topology_view", + "drop_existing_topology_table", + "create_topology_view", + "read_topology" + ], + "semantic_step": "read_topology", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_utilization_per_period", + "type": "skill", + "target": "cpu_utilization_per_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_utilization_per_period.skill.yaml", + "parameters": {}, + "required_steps": [ + "system_util" + ], + "semantic_step": "system_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-futex_wait_distribution", + "type": "skill", + "target": "futex_wait_distribution", + "mode": "definition", + "source_file": "backend/skills/atomic/futex_wait_distribution.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-irq_analysis", + "type": "skill", + "target": "irq_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/irq_analysis.skill.yaml", + "parameters": { + "hard_irq_long_threshold_us": 1000, + "soft_irq_long_threshold_us": 10000, + "irq_rate_heavy_threshold": 10000, + "irq_dur_heavy_threshold_ms": 50 + }, + "required_steps": [ + "data_check", + "irq_overview" + ], + "semantic_step": "irq_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_irq_summary", + "type": "skill", + "target": "linux_irq_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_irq_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "irq_summary" + ], + "semantic_step": "irq_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_runqueue_depth_timeline", + "type": "skill", + "target": "linux_runqueue_depth_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_runqueue_depth_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "runqueue_pressure_summary" + ], + "semantic_step": "runqueue_pressure_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_sched_latency_distribution", + "type": "skill", + "target": "linux_sched_latency_distribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_sched_latency_distribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "sched_latency_distribution" + ], + "semantic_step": "sched_latency_distribution", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-main_thread_sched_latency_in_range", + "type": "skill", + "target": "main_thread_sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-process_slice_cpu_hotspots", + "type": "skill", + "target": "process_slice_cpu_hotspots", + "mode": "definition", + "source_file": "backend/skills/atomic/process_slice_cpu_hotspots.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-sched_latency_in_range", + "type": "skill", + "target": "sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-scheduler_module", + "type": "skill", + "target": "scheduler_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/scheduler_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "runnable_analysis" + ], + "semantic_step": "runnable_analysis", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-scheduling_analysis", + "type": "skill", + "target": "scheduling_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/scheduling_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-screen_off_background_cpu_attribution", + "type": "skill", + "target": "screen_off_background_cpu_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/screen_off_background_cpu_attribution.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "screen_off_cpu" + ], + "semantic_step": "screen_off_cpu", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-selection_range_cpu_sched_summary", + "type": "skill", + "target": "selection_range_cpu_sched_summary", + "mode": "semantic", + "source_file": "backend/skills/composite/selection_range_cpu_sched_summary.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "running_thread_quadrants" + ], + "semantic_step": "running_thread_quadrants", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-system_load_in_range", + "type": "skill", + "target": "system_load_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/system_load_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-task_migration_in_range", + "type": "skill", + "target": "task_migration_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/task_migration_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "migration_analysis" + ], + "semantic_step": "migration_analysis", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-thread_affinity_violation", + "type": "skill", + "target": "thread_affinity_violation", + "mode": "definition", + "source_file": "backend/skills/atomic/thread_affinity_violation.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-util_tracking_analysis", + "type": "skill", + "target": "util_tracking_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/util_tracking_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + } + ] +} diff --git a/Trace/constructed/scheduler-cpu-contention/case.json b/Trace/constructed/scheduler-cpu-contention/case.json new file mode 100644 index 000000000..5cb5352c2 --- /dev/null +++ b/Trace/constructed/scheduler-cpu-contention/case.json @@ -0,0 +1,571 @@ +{ + "schema_version": 1, + "id": "scheduler-cpu-contention", + "kind": "constructed", + "title": "Scheduler and CPU contention", + "description": "Deterministic scheduler and cpu contention signals over the android-scroll-customer real base trace.", + "scene": "cpu", + "tags": [ + "android", + "constructed", + "cpu" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "f28f29a672ea9aa39c16533feeb06e003b1ae29084374364250fd7736ac33c51", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110 base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-scroll-customer", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "scheduler-cpu-contention-v1", + "output": "Trace/.generated/constructed/scheduler-cpu-contention/trace.pftrace" + }, + "coverage": { + "skills": [ + "android_job_scheduler_events", + "cache_miss_impact", + "cpu_analysis", + "cpu_cluster_load_in_range", + "cpu_cluster_mapping_view", + "cpu_freq_residency_summary", + "cpu_freq_timeline", + "cpu_idle_analysis", + "cpu_idle_state_residency", + "cpu_load_in_range", + "cpu_module", + "cpu_process_utilization_period", + "cpu_profiling", + "cpu_slice_analysis", + "cpu_thread_utilization_period", + "cpu_throttling_in_range", + "cpu_time_per_frame", + "cpu_topology_detection", + "cpu_topology_view", + "cpu_utilization_per_period", + "futex_wait_distribution", + "irq_analysis", + "linux_irq_summary", + "linux_runqueue_depth_timeline", + "linux_sched_latency_distribution", + "main_thread_sched_latency_in_range", + "process_slice_cpu_hotspots", + "sched_latency_in_range", + "scheduler_module", + "scheduling_analysis", + "screen_off_background_cpu_attribution", + "selection_range_cpu_sched_summary", + "system_load_in_range", + "task_migration_in_range", + "thread_affinity_violation", + "util_tracking_analysis" + ], + "strategies": [], + "expectations": [ + { + "id": "execute-android_job_scheduler_events", + "type": "skill", + "target": "android_job_scheduler_events", + "mode": "semantic", + "source_file": "backend/skills/atomic/android_job_scheduler_events.skill.yaml", + "parameters": {}, + "required_steps": [ + "job_events" + ], + "semantic_step": "job_events", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-cache_miss_impact", + "type": "skill", + "target": "cache_miss_impact", + "mode": "definition", + "source_file": "backend/skills/atomic/cache_miss_impact.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_analysis", + "type": "skill", + "target": "cpu_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/cpu_analysis.skill.yaml", + "parameters": { + "big_core_threshold_pct": 30, + "running_threshold_pct": 50, + "sched_delay_critical_ms": 16, + "blocked_function_threshold_ms": 50, + "enable_expert_probes": true, + "affinity_probe_migration_ratio_threshold_pct": 25, + "affinity_violation_warning_threshold_pct": 35, + "cache_miss_high_avg_delta_threshold": 500000, + "cache_miss_medium_avg_delta_threshold": 100000 + }, + "required_steps": [ + "init_cpu_topology", + "get_process" + ], + "semantic_step": "get_process", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_cluster_load_in_range", + "type": "skill", + "target": "cpu_cluster_load_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_cluster_load_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cluster_load" + ], + "semantic_step": "cluster_load", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_cluster_mapping_view", + "type": "skill", + "target": "cpu_cluster_mapping_view", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_cluster_mapping_view.skill.yaml", + "parameters": {}, + "required_steps": [ + "cluster_mapping" + ], + "semantic_step": "cluster_mapping", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_freq_residency_summary", + "type": "skill", + "target": "cpu_freq_residency_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_freq_residency_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "freq_residency" + ], + "semantic_step": "freq_residency", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_freq_timeline", + "type": "skill", + "target": "cpu_freq_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_freq_timeline.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cpu_freq_changes", + "cpu_freq_summary" + ], + "semantic_step": "cpu_freq_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-cpu_idle_analysis", + "type": "skill", + "target": "cpu_idle_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/cpu_idle_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_idle_state_residency", + "type": "skill", + "target": "cpu_idle_state_residency", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_idle_state_residency.skill.yaml", + "parameters": {}, + "required_steps": [ + "idle_residency" + ], + "semantic_step": "idle_residency", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_load_in_range", + "type": "skill", + "target": "cpu_load_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_load_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "cpu_utilization" + ], + "semantic_step": "cpu_utilization", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_module", + "type": "skill", + "target": "cpu_module", + "mode": "semantic", + "source_file": "backend/skills/modules/hardware/cpu_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "frequency_overview" + ], + "semantic_step": "frequency_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_process_utilization_period", + "type": "skill", + "target": "cpu_process_utilization_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_process_utilization_period.skill.yaml", + "parameters": {}, + "required_steps": [ + "process_util" + ], + "semantic_step": "process_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_profiling", + "type": "skill", + "target": "cpu_profiling", + "mode": "semantic", + "source_file": "backend/skills/deep/cpu_profiling.skill.yaml", + "parameters": { + "min_runtime_ms": 1 + }, + "required_steps": [ + "init_cpu_topology", + "cpu_usage_overview" + ], + "semantic_step": "cpu_usage_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_slice_analysis", + "type": "skill", + "target": "cpu_slice_analysis", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_slice_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "cpu_time_by_core" + ], + "semantic_step": "cpu_time_by_core", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_thread_utilization_period", + "type": "skill", + "target": "cpu_thread_utilization_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_thread_utilization_period.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "thread_util" + ], + "semantic_step": "thread_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_throttling_in_range", + "type": "skill", + "target": "cpu_throttling_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_throttling_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "throttle_detection" + ], + "semantic_step": "throttle_detection", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_time_per_frame", + "type": "skill", + "target": "cpu_time_per_frame", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_time_per_frame.skill.yaml", + "parameters": {}, + "required_steps": [ + "cpu_time" + ], + "semantic_step": "cpu_time", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_topology_detection", + "type": "skill", + "target": "cpu_topology_detection", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_topology_detection.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "detect_cpu_clusters" + ], + "semantic_step": "detect_cpu_clusters", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_topology_view", + "type": "skill", + "target": "cpu_topology_view", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_topology_view.skill.yaml", + "parameters": {}, + "required_steps": [ + "inspect_existing_topology_object", + "drop_existing_topology_view", + "drop_existing_topology_table", + "create_topology_view", + "read_topology" + ], + "semantic_step": "read_topology", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-cpu_utilization_per_period", + "type": "skill", + "target": "cpu_utilization_per_period", + "mode": "semantic", + "source_file": "backend/skills/atomic/cpu_utilization_per_period.skill.yaml", + "parameters": {}, + "required_steps": [ + "system_util" + ], + "semantic_step": "system_util", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-futex_wait_distribution", + "type": "skill", + "target": "futex_wait_distribution", + "mode": "definition", + "source_file": "backend/skills/atomic/futex_wait_distribution.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-irq_analysis", + "type": "skill", + "target": "irq_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/irq_analysis.skill.yaml", + "parameters": { + "hard_irq_long_threshold_us": 1000, + "soft_irq_long_threshold_us": 10000, + "irq_rate_heavy_threshold": 10000, + "irq_dur_heavy_threshold_ms": 50 + }, + "required_steps": [ + "data_check", + "irq_overview" + ], + "semantic_step": "irq_overview", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_irq_summary", + "type": "skill", + "target": "linux_irq_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_irq_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "irq_summary" + ], + "semantic_step": "irq_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_runqueue_depth_timeline", + "type": "skill", + "target": "linux_runqueue_depth_timeline", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_runqueue_depth_timeline.skill.yaml", + "parameters": {}, + "required_steps": [ + "runqueue_pressure_summary" + ], + "semantic_step": "runqueue_pressure_summary", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-linux_sched_latency_distribution", + "type": "skill", + "target": "linux_sched_latency_distribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/linux_sched_latency_distribution.skill.yaml", + "parameters": {}, + "required_steps": [ + "sched_latency_distribution" + ], + "semantic_step": "sched_latency_distribution", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-main_thread_sched_latency_in_range", + "type": "skill", + "target": "main_thread_sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/main_thread_sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-process_slice_cpu_hotspots", + "type": "skill", + "target": "process_slice_cpu_hotspots", + "mode": "definition", + "source_file": "backend/skills/atomic/process_slice_cpu_hotspots.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-sched_latency_in_range", + "type": "skill", + "target": "sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-scheduler_module", + "type": "skill", + "target": "scheduler_module", + "mode": "semantic", + "source_file": "backend/skills/modules/kernel/scheduler_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "init_cpu_topology", + "runnable_analysis" + ], + "semantic_step": "runnable_analysis", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-scheduling_analysis", + "type": "skill", + "target": "scheduling_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/scheduling_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-screen_off_background_cpu_attribution", + "type": "skill", + "target": "screen_off_background_cpu_attribution", + "mode": "semantic", + "source_file": "backend/skills/atomic/screen_off_background_cpu_attribution.skill.yaml", + "parameters": { + "top_n": 30 + }, + "required_steps": [ + "screen_off_cpu" + ], + "semantic_step": "screen_off_cpu", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-selection_range_cpu_sched_summary", + "type": "skill", + "target": "selection_range_cpu_sched_summary", + "mode": "semantic", + "source_file": "backend/skills/composite/selection_range_cpu_sched_summary.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "running_thread_quadrants" + ], + "semantic_step": "running_thread_quadrants", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-system_load_in_range", + "type": "skill", + "target": "system_load_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/system_load_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "execute-task_migration_in_range", + "type": "skill", + "target": "task_migration_in_range", + "mode": "semantic", + "source_file": "backend/skills/atomic/task_migration_in_range.skill.yaml", + "parameters": { + "start_ts": "${trace_start}", + "end_ts": "${trace_end}" + }, + "required_steps": [ + "init_cpu_topology", + "migration_analysis" + ], + "semantic_step": "migration_analysis", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-thread_affinity_violation", + "type": "skill", + "target": "thread_affinity_violation", + "mode": "definition", + "source_file": "backend/skills/atomic/thread_affinity_violation.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "id": "definition-util_tracking_analysis", + "type": "skill", + "target": "util_tracking_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/util_tracking_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::scheduler-cpu-contention" + } + ] + } +} diff --git a/Trace/constructed/scheduler-cpu-contention/scenario.json b/Trace/constructed/scheduler-cpu-contention/scenario.json new file mode 100644 index 000000000..f52f1294b --- /dev/null +++ b/Trace/constructed/scheduler-cpu-contention/scenario.json @@ -0,0 +1,162 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::scheduler-cpu-contention" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "RunnableWait" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "CPU contention" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "sched_blocked_reason" + }, + { + "type": "atrace-slice", + "at_ns": "200000000", + "duration_ns": "22000000", + "process": "system", + "thread": "system-main", + "name": "sched_blocked_reason" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.scheduler-cpu-contention.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "cpu-frequency", + "at_ns": "520000000", + "cpu_id": 0, + "value": 1800000, + "cpu": 0 + }, + { + "type": "irq-span", + "at_ns": "540000000", + "duration_ns": "4000000", + "irq": 42, + "name": "synthetic_irq", + "cpu": 0 + }, + { + "type": "atrace-async-track-slice", + "at_ns": "560000000", + "duration_ns": "30000000", + "process": "system", + "thread": "system-main", + "track_name": "JobScheduler", + "name": "scheduled job #12#<10999>com.smartperfetto.fixture/.SyntheticJob#", + "cookie": 12 + } + ] +} diff --git a/Trace/constructed/scheduler-cpu-contention/trace.overlay.pftrace b/Trace/constructed/scheduler-cpu-contention/trace.overlay.pftrace new file mode 100644 index 000000000..3e4ef7581 Binary files /dev/null and b/Trace/constructed/scheduler-cpu-contention/trace.overlay.pftrace differ diff --git a/Trace/constructed/startup-lifecycle/analysis/expected.json b/Trace/constructed/startup-lifecycle/analysis/expected.json new file mode 100644 index 000000000..fcd31313d --- /dev/null +++ b/Trace/constructed/startup-lifecycle/analysis/expected.json @@ -0,0 +1,234 @@ +{ + "schema_version": 1, + "case_id": "startup-lifecycle", + "marker": "SmartPerfetto::CASE::startup-lifecycle", + "expectations": [ + { + "id": "execute-app_process_starts_summary", + "type": "skill", + "target": "app_process_starts_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/app_process_starts_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "process_starts" + ], + "semantic_step": "process_starts", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-launcher_module", + "type": "skill", + "target": "launcher_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/launcher_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "launcher_process" + ], + "semantic_step": "launcher_process", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_analysis", + "type": "skill", + "target": "startup_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/startup_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "get_startups", + "startup_quality" + ], + "semantic_step": "startup_quality", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_binder_in_range", + "type": "skill", + "target": "startup_binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_binder_pool_analysis", + "type": "skill", + "target": "startup_binder_pool_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_binder_pool_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_breakdown_in_range", + "type": "skill", + "target": "startup_breakdown_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_breakdown_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_class_loading_in_range", + "type": "skill", + "target": "startup_class_loading_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_class_loading_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_cpu_placement_timeline", + "type": "skill", + "target": "startup_cpu_placement_timeline", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_cpu_placement_timeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_critical_tasks", + "type": "skill", + "target": "startup_critical_tasks", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_critical_tasks.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_detail", + "type": "skill", + "target": "startup_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/startup_detail.skill.yaml", + "parameters": { + "startup_id": 1, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}", + "dur_ms": 1, + "package": "com.smartperfetto.fixture", + "startup_type": "cold" + }, + "required_steps": [ + "init_cpu_topology", + "startup_info" + ], + "semantic_step": "startup_info", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_events_in_range", + "type": "skill", + "target": "startup_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_freq_rampup", + "type": "skill", + "target": "startup_freq_rampup", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_freq_rampup.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_gc_in_range", + "type": "skill", + "target": "startup_gc_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_gc_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_hot_slice_states", + "type": "skill", + "target": "startup_hot_slice_states", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_hot_slice_states.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_jit_analysis", + "type": "skill", + "target": "startup_jit_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_jit_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_binder_blocking_in_range", + "type": "skill", + "target": "startup_main_thread_binder_blocking_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_binder_blocking_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_file_io_in_range", + "type": "skill", + "target": "startup_main_thread_file_io_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_file_io_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_slices_in_range", + "type": "skill", + "target": "startup_main_thread_slices_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_slices_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_states_in_range", + "type": "skill", + "target": "startup_main_thread_states_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_states_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_sync_binder_in_range", + "type": "skill", + "target": "startup_main_thread_sync_binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_sync_binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_sched_latency_in_range", + "type": "skill", + "target": "startup_sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_slow_reasons", + "type": "skill", + "target": "startup_slow_reasons", + "mode": "semantic", + "source_file": "backend/skills/atomic/startup_slow_reasons.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_overview" + ], + "semantic_step": "startup_overview", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_thread_blocking_graph", + "type": "skill", + "target": "startup_thread_blocking_graph", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_thread_blocking_graph.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "strategy-startup", + "type": "strategy", + "target": "startup", + "query": "启动", + "expected_strategy": "startup", + "source_file": "backend/strategies/startup.strategy.md", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + } + ] +} diff --git a/Trace/constructed/startup-lifecycle/case.json b/Trace/constructed/startup-lifecycle/case.json new file mode 100644 index 000000000..c96769051 --- /dev/null +++ b/Trace/constructed/startup-lifecycle/case.json @@ -0,0 +1,312 @@ +{ + "schema_version": 1, + "id": "startup-lifecycle", + "kind": "constructed", + "title": "Startup and process lifecycle", + "description": "Deterministic startup and process lifecycle signals over the android-startup-heavy real base trace.", + "scene": "startup", + "tags": [ + "android", + "constructed", + "startup" + ], + "aliases": [], + "trace": { + "file": "trace.overlay.pftrace", + "format": "perfetto-protobuf", + "sha256": "884d969ba9a0e477e862c1178b25afca25c4a0c845cc77cc01db0371c4e40de7", + "materialization": "base-plus-overlay" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Xiaomi pandora base", + "build_fingerprint": null, + "compatibility": { + "min_api": 35, + "max_api": 36 + } + }, + "source": { + "origin": "SmartPerfetto deterministic trace generator", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": "AGPL-3.0-or-later", + "consent": "generated", + "privacy_review": "not-applicable", + "sanitization_review": "not-applicable", + "publication": "public" + }, + "analysis": { + "results": [ + "analysis/expected.json" + ], + "logs": [] + }, + "construction": { + "base_case_id": "android-startup-heavy", + "scenario_file": "scenario.json", + "generator_version": 1, + "seed": "startup-lifecycle-v1", + "output": "Trace/.generated/constructed/startup-lifecycle/trace.pftrace" + }, + "coverage": { + "skills": [ + "app_process_starts_summary", + "launcher_module", + "startup_analysis", + "startup_binder_in_range", + "startup_binder_pool_analysis", + "startup_breakdown_in_range", + "startup_class_loading_in_range", + "startup_cpu_placement_timeline", + "startup_critical_tasks", + "startup_detail", + "startup_events_in_range", + "startup_freq_rampup", + "startup_gc_in_range", + "startup_hot_slice_states", + "startup_jit_analysis", + "startup_main_thread_binder_blocking_in_range", + "startup_main_thread_file_io_in_range", + "startup_main_thread_slices_in_range", + "startup_main_thread_states_in_range", + "startup_main_thread_sync_binder_in_range", + "startup_sched_latency_in_range", + "startup_slow_reasons", + "startup_thread_blocking_graph" + ], + "strategies": [ + "startup" + ], + "expectations": [ + { + "id": "execute-app_process_starts_summary", + "type": "skill", + "target": "app_process_starts_summary", + "mode": "semantic", + "source_file": "backend/skills/atomic/app_process_starts_summary.skill.yaml", + "parameters": {}, + "required_steps": [ + "process_starts" + ], + "semantic_step": "process_starts", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-launcher_module", + "type": "skill", + "target": "launcher_module", + "mode": "semantic", + "source_file": "backend/skills/modules/app/launcher_module.skill.yaml", + "parameters": {}, + "required_steps": [ + "launcher_process" + ], + "semantic_step": "launcher_process", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_analysis", + "type": "skill", + "target": "startup_analysis", + "mode": "semantic", + "source_file": "backend/skills/composite/startup_analysis.skill.yaml", + "parameters": {}, + "required_steps": [ + "get_startups", + "startup_quality" + ], + "semantic_step": "startup_quality", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_binder_in_range", + "type": "skill", + "target": "startup_binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_binder_pool_analysis", + "type": "skill", + "target": "startup_binder_pool_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_binder_pool_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_breakdown_in_range", + "type": "skill", + "target": "startup_breakdown_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_breakdown_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_class_loading_in_range", + "type": "skill", + "target": "startup_class_loading_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_class_loading_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_cpu_placement_timeline", + "type": "skill", + "target": "startup_cpu_placement_timeline", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_cpu_placement_timeline.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_critical_tasks", + "type": "skill", + "target": "startup_critical_tasks", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_critical_tasks.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_detail", + "type": "skill", + "target": "startup_detail", + "mode": "semantic", + "source_file": "backend/skills/composite/startup_detail.skill.yaml", + "parameters": { + "startup_id": 1, + "start_ts": "${trace_start}", + "end_ts": "${trace_end}", + "dur_ms": 1, + "package": "com.smartperfetto.fixture", + "startup_type": "cold" + }, + "required_steps": [ + "init_cpu_topology", + "startup_info" + ], + "semantic_step": "startup_info", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_events_in_range", + "type": "skill", + "target": "startup_events_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_events_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_freq_rampup", + "type": "skill", + "target": "startup_freq_rampup", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_freq_rampup.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_gc_in_range", + "type": "skill", + "target": "startup_gc_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_gc_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_hot_slice_states", + "type": "skill", + "target": "startup_hot_slice_states", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_hot_slice_states.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_jit_analysis", + "type": "skill", + "target": "startup_jit_analysis", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_jit_analysis.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_binder_blocking_in_range", + "type": "skill", + "target": "startup_main_thread_binder_blocking_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_binder_blocking_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_file_io_in_range", + "type": "skill", + "target": "startup_main_thread_file_io_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_file_io_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_slices_in_range", + "type": "skill", + "target": "startup_main_thread_slices_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_slices_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_states_in_range", + "type": "skill", + "target": "startup_main_thread_states_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_states_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_main_thread_sync_binder_in_range", + "type": "skill", + "target": "startup_main_thread_sync_binder_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_main_thread_sync_binder_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_sched_latency_in_range", + "type": "skill", + "target": "startup_sched_latency_in_range", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_sched_latency_in_range.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "execute-startup_slow_reasons", + "type": "skill", + "target": "startup_slow_reasons", + "mode": "semantic", + "source_file": "backend/skills/atomic/startup_slow_reasons.skill.yaml", + "parameters": {}, + "required_steps": [ + "startup_overview" + ], + "semantic_step": "startup_overview", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "definition-startup_thread_blocking_graph", + "type": "skill", + "target": "startup_thread_blocking_graph", + "mode": "definition", + "source_file": "backend/skills/atomic/startup_thread_blocking_graph.skill.yaml", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "id": "strategy-startup", + "type": "strategy", + "target": "startup", + "query": "启动", + "expected_strategy": "startup", + "source_file": "backend/strategies/startup.strategy.md", + "required_marker": "SmartPerfetto::CASE::startup-lifecycle" + } + ] + } +} diff --git a/Trace/constructed/startup-lifecycle/scenario.json b/Trace/constructed/startup-lifecycle/scenario.json new file mode 100644 index 000000000..b4c6fdd2d --- /dev/null +++ b/Trace/constructed/startup-lifecycle/scenario.json @@ -0,0 +1,129 @@ +{ + "schema_version": 1, + "clock": { + "anchor": "trace-middle", + "duration_ns": "1000000000" + }, + "actors": { + "processes": [ + { + "id": "app", + "name": "com.smartperfetto.fixture", + "uid": 10999 + }, + { + "id": "system", + "name": "system_server", + "uid": 1000 + }, + { + "id": "sf", + "name": "/system/bin/surfaceflinger", + "uid": 1000 + } + ], + "threads": [ + { + "id": "main", + "process": "app", + "name": "main", + "is_main": true + }, + { + "id": "render", + "process": "app", + "name": "RenderThread" + }, + { + "id": "game", + "process": "app", + "name": "GameThread" + }, + { + "id": "rn-js", + "process": "app", + "name": "mqt_js" + }, + { + "id": "rn-native", + "process": "app", + "name": "mqt_native_modules" + }, + { + "id": "webview", + "process": "app", + "name": "CrRendererMain" + }, + { + "id": "system-main", + "process": "system", + "name": "android.fg", + "is_main": true + }, + { + "id": "sf-main", + "process": "sf", + "name": "surfaceflinger", + "is_main": true + } + ] + }, + "signals": [ + { + "type": "atrace-slice", + "at_ns": "10000000", + "duration_ns": "18000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto::CASE::startup-lifecycle" + }, + { + "type": "atrace-slice", + "at_ns": "40000000", + "duration_ns": "19000000", + "process": "app", + "thread": "render", + "name": "MetricsLogger:launchObserverNotifyIntentStarted" + }, + { + "type": "atrace-slice", + "at_ns": "70000000", + "duration_ns": "20000000", + "process": "app", + "thread": "main", + "name": "activityStart" + }, + { + "type": "atrace-slice", + "at_ns": "100000000", + "duration_ns": "21000000", + "process": "app", + "thread": "render", + "name": "bindApplication" + }, + { + "type": "atrace-counter", + "at_ns": "250000000", + "process": "app", + "thread": "main", + "name": "SmartPerfetto.startup-lifecycle.pressure", + "value": 73 + }, + { + "type": "sched-running", + "at_ns": "300000000", + "duration_ns": "45000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + }, + { + "type": "sched-running", + "at_ns": "380000000", + "duration_ns": "20000000", + "thread": "main", + "cpu": 0, + "end_state": "S" + } + ] +} diff --git a/Trace/constructed/startup-lifecycle/trace.overlay.pftrace b/Trace/constructed/startup-lifecycle/trace.overlay.pftrace new file mode 100644 index 000000000..71cee122a Binary files /dev/null and b/Trace/constructed/startup-lifecycle/trace.overlay.pftrace differ diff --git a/Trace/coverage.json b/Trace/coverage.json new file mode 100644 index 000000000..f5cada527 --- /dev/null +++ b/Trace/coverage.json @@ -0,0 +1,510 @@ +{ + "schema_version": 1, + "missing": { + "skills": [], + "strategies": [] + }, + "stale": { + "skills": [], + "strategies": [] + }, + "covered": { + "skills": [ + "ams_module", + "android_bitmap_memory_per_process", + "android_dvfs_counter_stats", + "android_gpu_work_period_track", + "android_heap_graph_leak_candidates", + "android_heap_graph_summary", + "android_job_scheduler_events", + "android_kernel_wakelock_summary", + "android_memory_v57_ai_diagnostics", + "anr_analysis", + "anr_context_in_range", + "anr_detail", + "anr_main_thread_blocking", + "app_frame_production", + "app_lifecycle_in_range", + "app_process_starts_summary", + "art_module", + "battery_charge_timeline", + "battery_doze_state_timeline", + "battery_drain_attribution", + "battery_drain_rate_summary", + "binder_analysis", + "binder_blocking_in_range", + "binder_detail", + "binder_in_range", + "binder_module", + "binder_root_cause", + "binder_storm_detection", + "block_io_analysis", + "blocking_chain_analysis", + "buffer_transaction_lifecycle", + "cache_miss_impact", + "callstack_analysis", + "camera_trace_evidence", + "choreographer_module", + "chrome_scroll_jank_frame_timeline", + "click_response_analysis", + "click_response_detail", + "code_pinpoint", + "compose_recomposition_hotspot", + "consumer_jank_detection", + "cpu_analysis", + "cpu_cluster_load_in_range", + "cpu_cluster_mapping_view", + "cpu_freq_residency_summary", + "cpu_freq_timeline", + "cpu_idle_analysis", + "cpu_idle_state_residency", + "cpu_load_in_range", + "cpu_module", + "cpu_process_utilization_period", + "cpu_profiling", + "cpu_slice_analysis", + "cpu_thread_utilization_period", + "cpu_throttling_in_range", + "cpu_time_per_frame", + "cpu_topology_detection", + "cpu_topology_view", + "cpu_utilization_per_period", + "device_state_snapshot", + "device_state_timeline", + "dmabuf_analysis", + "fence_wait_decomposition", + "filesystem_module", + "flutter_scrolling_analysis", + "fpsgo_analysis", + "frame_blocking_calls", + "frame_overrun_summary", + "frame_pipeline_variance", + "frame_production_gap", + "frame_ui_time_breakdown", + "futex_wait_distribution", + "game_fps_analysis", + "game_main_loop_jank", + "gc_analysis", + "gc_events_in_range", + "gl_standalone_swap_jank", + "global_trace_sanity_check", + "gpu_analysis", + "gpu_freq_in_range", + "gpu_frequency_analysis", + "gpu_metrics", + "gpu_module", + "gpu_power_state_analysis", + "gpu_render_in_range", + "gpu_v57_ai_diagnostics", + "input_events_in_range", + "input_module", + "input_to_frame_latency", + "io_pressure", + "irq_analysis", + "jank_frame_detail", + "launcher_module", + "linux_irq_summary", + "linux_perf_counter_hotspots", + "linux_process_rss_swap_timeline", + "linux_runqueue_depth_timeline", + "linux_sched_latency_distribution", + "linux_systemd_journald_analysis", + "lmk_analysis", + "lmk_kill_attribution", + "lock_binder_wait", + "lock_contention_analysis", + "lock_contention_in_range", + "lock_contention_module", + "logcat_analysis", + "main_thread_file_io_in_range", + "main_thread_sched_latency_in_range", + "main_thread_slices_in_range", + "main_thread_states_in_range", + "mali_gpu_power_state", + "media_codec_activity", + "memory_analysis", + "memory_growth_detector", + "memory_module", + "memory_pressure_in_range", + "memory_rss_high_watermark", + "modem_network_correlation_summary", + "multi_trace_result_comparison", + "native_heap_breakdown", + "navigation_analysis", + "network_analysis", + "oom_adjuster_score_timeline", + "page_fault_in_range", + "pipeline_4feature_scoring", + "pipeline_android_pip_freeform", + "pipeline_android_view_mixed", + "pipeline_android_view_multi_window", + "pipeline_android_view_software", + "pipeline_android_view_standard_blast", + "pipeline_android_view_standard_legacy", + "pipeline_angle_gles_vulkan", + "pipeline_camera_pipeline", + "pipeline_chrome_browser_viz", + "pipeline_compose_standard", + "pipeline_flutter_surfaceview_impeller", + "pipeline_flutter_surfaceview_skia", + "pipeline_flutter_textureview", + "pipeline_game_engine", + "pipeline_hardware_buffer_renderer", + "pipeline_imagereader_pipeline", + "pipeline_key_slices_overlay", + "pipeline_opengl_es", + "pipeline_rn_new_arch", + "pipeline_rn_old_arch", + "pipeline_rn_skia", + "pipeline_software_compositing", + "pipeline_surface_control_api", + "pipeline_surfaceview_blast", + "pipeline_textureview_standard", + "pipeline_variable_refresh_rate", + "pipeline_video_overlay_hwc", + "pipeline_vulkan_native", + "pipeline_webview_gl_functor", + "pipeline_webview_surface_control", + "pipeline_webview_surfaceview_wrapper", + "pipeline_webview_textureview_custom", + "power_consumption_overview", + "power_module", + "power_rails_energy_breakdown", + "present_fence_timing", + "process_identity_resolver", + "process_slice_cpu_hotspots", + "render_pipeline_latency", + "render_thread_slices", + "rendering_pipeline_detection", + "rn_bridge_to_frame_jank", + "rn_fabric_render_jank", + "scene_reconstruction", + "sched_latency_in_range", + "scheduler_module", + "scheduling_analysis", + "screen_off_background_cpu_attribution", + "scroll_response_latency", + "scroll_session_analysis", + "scrolling_analysis", + "selection_range_cpu_sched_summary", + "sf_composition_in_range", + "sf_frame_consumption", + "sf_layer_count_in_range", + "startup_analysis", + "startup_binder_in_range", + "startup_binder_pool_analysis", + "startup_breakdown_in_range", + "startup_class_loading_in_range", + "startup_cpu_placement_timeline", + "startup_critical_tasks", + "startup_detail", + "startup_events_in_range", + "startup_freq_rampup", + "startup_gc_in_range", + "startup_hot_slice_states", + "startup_jit_analysis", + "startup_main_thread_binder_blocking_in_range", + "startup_main_thread_file_io_in_range", + "startup_main_thread_slices_in_range", + "startup_main_thread_states_in_range", + "startup_main_thread_sync_binder_in_range", + "startup_sched_latency_in_range", + "startup_slow_reasons", + "startup_thread_blocking_graph", + "state_timeline", + "surfaceflinger_analysis", + "surfaceflinger_module", + "suspend_wakeup_analysis", + "system_load_in_range", + "systemui_module", + "task_migration_in_range", + "textureview_producer_frame_timing", + "thermal_module", + "thermal_predictor", + "thermal_throttling", + "thermal_throttling_chain", + "third_party_module", + "thread_affinity_violation", + "touch_to_display_latency", + "trace_state_track_summary", + "util_tracking_analysis", + "vrr_detection", + "vsync_alignment_in_range", + "vsync_config", + "vsync_period_detection", + "vsync_phase_alignment", + "wakelock_tracking", + "wakeup_frequency_summary", + "wattson_app_startup_power", + "wattson_rails_power_breakdown", + "wattson_thread_power_attribution", + "webview_drawfunctor_jank_chain", + "webview_v8_analysis", + "wms_module" + ], + "strategies": [ + "anr", + "game", + "general", + "interaction", + "io", + "linux", + "media", + "memory", + "multi_trace_result_comparison", + "network", + "overview", + "pipeline", + "power", + "runtime_correctness", + "scroll_response", + "scrolling", + "smart", + "startup", + "teaching", + "touch_tracking", + "verifier_misdiagnosis" + ] + }, + "quality": { + "semantic": [ + "ams_module", + "android_bitmap_memory_per_process", + "android_dvfs_counter_stats", + "android_gpu_work_period_track", + "android_heap_graph_summary", + "android_job_scheduler_events", + "anr_analysis", + "anr_detail", + "anr_main_thread_blocking", + "app_frame_production", + "app_process_starts_summary", + "art_module", + "battery_charge_timeline", + "battery_doze_state_timeline", + "battery_drain_attribution", + "battery_drain_rate_summary", + "binder_analysis", + "binder_detail", + "binder_module", + "binder_storm_detection", + "blocking_chain_analysis", + "buffer_transaction_lifecycle", + "camera_trace_evidence", + "choreographer_module", + "chrome_scroll_jank_frame_timeline", + "click_response_analysis", + "click_response_detail", + "code_pinpoint", + "compose_recomposition_hotspot", + "consumer_jank_detection", + "cpu_analysis", + "cpu_cluster_load_in_range", + "cpu_cluster_mapping_view", + "cpu_freq_residency_summary", + "cpu_freq_timeline", + "cpu_idle_state_residency", + "cpu_load_in_range", + "cpu_module", + "cpu_process_utilization_period", + "cpu_profiling", + "cpu_slice_analysis", + "cpu_thread_utilization_period", + "cpu_throttling_in_range", + "cpu_time_per_frame", + "cpu_topology_detection", + "cpu_topology_view", + "cpu_utilization_per_period", + "device_state_snapshot", + "device_state_timeline", + "fence_wait_decomposition", + "filesystem_module", + "flutter_scrolling_analysis", + "frame_overrun_summary", + "frame_production_gap", + "frame_ui_time_breakdown", + "game_fps_analysis", + "game_main_loop_jank", + "gl_standalone_swap_jank", + "global_trace_sanity_check", + "gpu_analysis", + "gpu_metrics", + "gpu_module", + "gpu_v57_ai_diagnostics", + "input_module", + "input_to_frame_latency", + "irq_analysis", + "jank_frame_detail", + "launcher_module", + "linux_irq_summary", + "linux_process_rss_swap_timeline", + "linux_runqueue_depth_timeline", + "linux_sched_latency_distribution", + "lmk_analysis", + "lmk_kill_attribution", + "lock_binder_wait", + "lock_contention_analysis", + "lock_contention_in_range", + "lock_contention_module", + "mali_gpu_power_state", + "media_codec_activity", + "memory_analysis", + "memory_growth_detector", + "memory_module", + "memory_pressure_in_range", + "memory_rss_high_watermark", + "modem_network_correlation_summary", + "navigation_analysis", + "network_analysis", + "oom_adjuster_score_timeline", + "pipeline_4feature_scoring", + "pipeline_key_slices_overlay", + "power_consumption_overview", + "power_module", + "power_rails_energy_breakdown", + "present_fence_timing", + "render_thread_slices", + "rendering_pipeline_detection", + "rn_bridge_to_frame_jank", + "rn_fabric_render_jank", + "scene_reconstruction", + "scheduler_module", + "screen_off_background_cpu_attribution", + "scrolling_analysis", + "selection_range_cpu_sched_summary", + "sf_frame_consumption", + "sf_layer_count_in_range", + "startup_analysis", + "startup_detail", + "startup_slow_reasons", + "state_timeline", + "surfaceflinger_analysis", + "surfaceflinger_module", + "suspend_wakeup_analysis", + "systemui_module", + "task_migration_in_range", + "textureview_producer_frame_timing", + "thermal_module", + "thermal_throttling", + "thermal_throttling_chain", + "third_party_module", + "vrr_detection", + "vsync_config", + "vsync_period_detection", + "vsync_phase_alignment", + "wakelock_tracking", + "wakeup_frequency_summary", + "wattson_rails_power_breakdown", + "wattson_thread_power_attribution", + "webview_drawfunctor_jank_chain", + "webview_v8_analysis", + "wms_module" + ], + "graceful_empty": [ + "android_heap_graph_leak_candidates", + "android_kernel_wakelock_summary", + "android_memory_v57_ai_diagnostics", + "binder_root_cause", + "block_io_analysis", + "callstack_analysis", + "io_pressure", + "linux_perf_counter_hotspots", + "native_heap_breakdown", + "wattson_app_startup_power" + ], + "unavailable": [ + "dmabuf_analysis", + "gc_analysis", + "scroll_session_analysis" + ], + "definition": [ + "anr_context_in_range", + "app_lifecycle_in_range", + "binder_blocking_in_range", + "binder_in_range", + "cache_miss_impact", + "cpu_idle_analysis", + "fpsgo_analysis", + "frame_blocking_calls", + "frame_pipeline_variance", + "futex_wait_distribution", + "gc_events_in_range", + "gpu_freq_in_range", + "gpu_frequency_analysis", + "gpu_power_state_analysis", + "gpu_render_in_range", + "input_events_in_range", + "linux_systemd_journald_analysis", + "logcat_analysis", + "main_thread_file_io_in_range", + "main_thread_sched_latency_in_range", + "main_thread_slices_in_range", + "main_thread_states_in_range", + "multi_trace_result_comparison", + "page_fault_in_range", + "pipeline_android_pip_freeform", + "pipeline_android_view_mixed", + "pipeline_android_view_multi_window", + "pipeline_android_view_software", + "pipeline_android_view_standard_blast", + "pipeline_android_view_standard_legacy", + "pipeline_angle_gles_vulkan", + "pipeline_camera_pipeline", + "pipeline_chrome_browser_viz", + "pipeline_compose_standard", + "pipeline_flutter_surfaceview_impeller", + "pipeline_flutter_surfaceview_skia", + "pipeline_flutter_textureview", + "pipeline_game_engine", + "pipeline_hardware_buffer_renderer", + "pipeline_imagereader_pipeline", + "pipeline_opengl_es", + "pipeline_rn_new_arch", + "pipeline_rn_old_arch", + "pipeline_rn_skia", + "pipeline_software_compositing", + "pipeline_surface_control_api", + "pipeline_surfaceview_blast", + "pipeline_textureview_standard", + "pipeline_variable_refresh_rate", + "pipeline_video_overlay_hwc", + "pipeline_vulkan_native", + "pipeline_webview_gl_functor", + "pipeline_webview_surface_control", + "pipeline_webview_surfaceview_wrapper", + "pipeline_webview_textureview_custom", + "process_identity_resolver", + "process_slice_cpu_hotspots", + "render_pipeline_latency", + "sched_latency_in_range", + "scheduling_analysis", + "scroll_response_latency", + "sf_composition_in_range", + "startup_binder_in_range", + "startup_binder_pool_analysis", + "startup_breakdown_in_range", + "startup_class_loading_in_range", + "startup_cpu_placement_timeline", + "startup_critical_tasks", + "startup_events_in_range", + "startup_freq_rampup", + "startup_gc_in_range", + "startup_hot_slice_states", + "startup_jit_analysis", + "startup_main_thread_binder_blocking_in_range", + "startup_main_thread_file_io_in_range", + "startup_main_thread_slices_in_range", + "startup_main_thread_states_in_range", + "startup_main_thread_sync_binder_in_range", + "startup_sched_latency_in_range", + "startup_thread_blocking_graph", + "system_load_in_range", + "thermal_predictor", + "thread_affinity_violation", + "touch_to_display_latency", + "trace_state_track_summary", + "util_tracking_analysis", + "vsync_alignment_in_range" + ] + } +} diff --git a/Trace/real/README.md b/Trace/real/README.md new file mode 100644 index 000000000..4f9a39a49 --- /dev/null +++ b/Trace/real/README.md @@ -0,0 +1,13 @@ + +# Real Trace Cases + +New captures are first imported under ignored `.private/` staging and require explicit review before promotion. + +| Case | Scene | Android | Publication | Analysis | +| --- | --- | --- | --- | --- | +| [Android 16 customer scroll](./android-scroll-customer/) | scrolling | Android 16 / API 36 | legacy-tracked | 1 file(s) | +| [Android 15 standard AOSP scroll](./android-scroll-standard/) | scrolling | Android 15 / API 35 | legacy-tracked | 1 file(s) | +| [Android 16 heavy startup](./android-startup-heavy/) | startup | Android 16 / API 36 | legacy-tracked | 1 file(s) | +| [Android 16 light startup](./android-startup-light/) | startup | Android 16 / API 36 | legacy-tracked | 1 file(s) | +| [Android 15 Flutter SurfaceView scroll](./flutter-scroll-surface-view/) | scrolling | Android 15 / API 35 | legacy-tracked | 1 file(s) | +| [Android 15 Flutter TextureView scroll](./flutter-scroll-texture-view/) | scrolling | Android 15 / API 35 | legacy-tracked | 1 file(s) | diff --git a/test-traces/scroll-demo-customer-scroll.pftrace.fps_report.txt b/Trace/real/android-scroll-customer/analysis/scroll-demo-customer-scroll.pftrace.fps_report.txt similarity index 100% rename from test-traces/scroll-demo-customer-scroll.pftrace.fps_report.txt rename to Trace/real/android-scroll-customer/analysis/scroll-demo-customer-scroll.pftrace.fps_report.txt diff --git a/Trace/real/android-scroll-customer/case.json b/Trace/real/android-scroll-customer/case.json new file mode 100644 index 000000000..160de230b --- /dev/null +++ b/Trace/real/android-scroll-customer/case.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "id": "android-scroll-customer", + "kind": "real", + "title": "Android 16 customer scroll", + "description": "Existing customer scrolling fixture with multiple gesture and jank events.", + "scene": "scrolling", + "tags": ["android", "scrolling", "customer", "jank"], + "aliases": ["scroll-demo-customer-scroll.pftrace"], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "1015ba963c49cd57d08d8997c4c48027026f2d6e41f7eb72b4b1356e39f7af3a", + "materialization": "committed" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "OPPO PKH110", + "build_fingerprint": "OPPO/PKH110/OP5DC1L1:16/AP3A.240617.008/V.2a01376-7328d8-769621:user/release-keys", + "compatibility": {"min_api": 36, "max_api": 36} + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": ["analysis/scroll-demo-customer-scroll.pftrace.fps_report.txt"], + "logs": [] + }, + "coverage": {"skills": [], "strategies": [], "expectations": []} +} diff --git a/test-traces/scroll-demo-customer-scroll.pftrace b/Trace/real/android-scroll-customer/trace.pftrace similarity index 100% rename from test-traces/scroll-demo-customer-scroll.pftrace rename to Trace/real/android-scroll-customer/trace.pftrace diff --git a/test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace.fps_report.txt b/Trace/real/android-scroll-standard/analysis/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace.fps_report.txt similarity index 100% rename from test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace.fps_report.txt rename to Trace/real/android-scroll-standard/analysis/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace.fps_report.txt diff --git a/Trace/real/android-scroll-standard/case.json b/Trace/real/android-scroll-standard/case.json new file mode 100644 index 000000000..018cbd52a --- /dev/null +++ b/Trace/real/android-scroll-standard/case.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "id": "android-scroll-standard", + "kind": "real", + "title": "Android 15 standard AOSP scroll", + "description": "Existing standard Android View scrolling fixture without pre-animation.", + "scene": "scrolling", + "tags": ["android", "scrolling", "aosp", "view"], + "aliases": ["scroll_Standard-AOSP-App-Without-PreAnimation.pftrace"], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "df7eebc5c6ff19b48331f8f1bca346612d86a5ae26eae202d46842a83f87a653", + "materialization": "committed" + }, + "android": { + "release": "15", + "api_level": 35, + "device": "nubia pacific", + "build_fingerprint": "nubia/pacific/pacific:15/1.4.1.0/101:user/release-keys", + "compatibility": {"min_api": 35, "max_api": 35} + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": ["analysis/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace.fps_report.txt"], + "logs": [] + }, + "coverage": {"skills": [], "strategies": [], "expectations": []} +} diff --git a/test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace b/Trace/real/android-scroll-standard/trace.pftrace similarity index 100% rename from test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace rename to Trace/real/android-scroll-standard/trace.pftrace diff --git a/test-traces/lacunh_heavy.pftrace.fps_report.txt b/Trace/real/android-startup-heavy/analysis/lacunh_heavy.pftrace.fps_report.txt similarity index 100% rename from test-traces/lacunh_heavy.pftrace.fps_report.txt rename to Trace/real/android-startup-heavy/analysis/lacunh_heavy.pftrace.fps_report.txt diff --git a/Trace/real/android-startup-heavy/case.json b/Trace/real/android-startup-heavy/case.json new file mode 100644 index 000000000..f24173eb9 --- /dev/null +++ b/Trace/real/android-startup-heavy/case.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "id": "android-startup-heavy", + "kind": "real", + "title": "Android 16 heavy startup", + "description": "Existing SmartPerfetto heavy application startup fixture captured on a Xiaomi pandora device.", + "scene": "startup", + "tags": ["android", "startup", "heavy"], + "aliases": ["lacunh_heavy.pftrace"], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "2a0c7b85e7f4a14e43b7f8d6de21172e05eba1ed3ad8ef7df539bb5e59b6ba63", + "materialization": "committed" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Xiaomi pandora", + "build_fingerprint": "Xiaomi/pandora/pandora:16/BP2A.250605.031.A3/OS3.0.34.0.WBLCNXM:user/release-keys", + "compatibility": {"min_api": 36, "max_api": 36} + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": ["analysis/lacunh_heavy.pftrace.fps_report.txt"], + "logs": [] + }, + "coverage": {"skills": [], "strategies": [], "expectations": []} +} diff --git a/test-traces/lacunh_heavy.pftrace b/Trace/real/android-startup-heavy/trace.pftrace similarity index 100% rename from test-traces/lacunh_heavy.pftrace rename to Trace/real/android-startup-heavy/trace.pftrace diff --git a/test-traces/launch_light.pftrace.fps_report.txt b/Trace/real/android-startup-light/analysis/launch_light.pftrace.fps_report.txt similarity index 100% rename from test-traces/launch_light.pftrace.fps_report.txt rename to Trace/real/android-startup-light/analysis/launch_light.pftrace.fps_report.txt diff --git a/Trace/real/android-startup-light/case.json b/Trace/real/android-startup-light/case.json new file mode 100644 index 000000000..6996be94e --- /dev/null +++ b/Trace/real/android-startup-light/case.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "id": "android-startup-light", + "kind": "real", + "title": "Android 16 light startup", + "description": "Existing SmartPerfetto light application startup fixture captured on a Google raven device.", + "scene": "startup", + "tags": ["android", "startup", "light"], + "aliases": ["launch_light.pftrace"], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "6c5479fd1b765ee4d29692c43a8204b972bc0f97eb373aca98ea7e11e99fd8b4", + "materialization": "committed" + }, + "android": { + "release": "16", + "api_level": 36, + "device": "Google raven", + "build_fingerprint": "google/raven_beta/raven:16/BP31.250523.010/13667654:user/release-keys", + "compatibility": {"min_api": 36, "max_api": 36} + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": ["analysis/launch_light.pftrace.fps_report.txt"], + "logs": [] + }, + "coverage": {"skills": [], "strategies": [], "expectations": []} +} diff --git a/test-traces/launch_light.pftrace b/Trace/real/android-startup-light/trace.pftrace similarity index 100% rename from test-traces/launch_light.pftrace rename to Trace/real/android-startup-light/trace.pftrace diff --git a/test-traces/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace.fps_report.txt b/Trace/real/flutter-scroll-surface-view/analysis/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace.fps_report.txt similarity index 100% rename from test-traces/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace.fps_report.txt rename to Trace/real/flutter-scroll-surface-view/analysis/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace.fps_report.txt diff --git a/Trace/real/flutter-scroll-surface-view/case.json b/Trace/real/flutter-scroll-surface-view/case.json new file mode 100644 index 000000000..6446e0be0 --- /dev/null +++ b/Trace/real/flutter-scroll-surface-view/case.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "id": "flutter-scroll-surface-view", + "kind": "real", + "title": "Android 15 Flutter SurfaceView scroll", + "description": "Existing WeChat Flutter scrolling fixture rendered through SurfaceView.", + "scene": "scrolling", + "tags": ["android", "flutter", "scrolling", "surfaceview", "wechat"], + "aliases": ["Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace"], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "e876c56b5d7290acda596e73929c1a127df4255e9ca6951605dad40833e67a53", + "materialization": "committed" + }, + "android": { + "release": "15", + "api_level": 35, + "device": "nubia pacific", + "build_fingerprint": "nubia/pacific/pacific:15/1.4.1.0/101:user/release-keys", + "compatibility": {"min_api": 35, "max_api": 35} + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": ["analysis/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace.fps_report.txt"], + "logs": [] + }, + "coverage": {"skills": [], "strategies": [], "expectations": []} +} diff --git a/test-traces/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace b/Trace/real/flutter-scroll-surface-view/trace.pftrace similarity index 100% rename from test-traces/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace rename to Trace/real/flutter-scroll-surface-view/trace.pftrace diff --git a/test-traces/Scroll-Flutter-327-TextureView.pftrace.fps_report.txt b/Trace/real/flutter-scroll-texture-view/analysis/Scroll-Flutter-327-TextureView.pftrace.fps_report.txt similarity index 100% rename from test-traces/Scroll-Flutter-327-TextureView.pftrace.fps_report.txt rename to Trace/real/flutter-scroll-texture-view/analysis/Scroll-Flutter-327-TextureView.pftrace.fps_report.txt diff --git a/Trace/real/flutter-scroll-texture-view/case.json b/Trace/real/flutter-scroll-texture-view/case.json new file mode 100644 index 000000000..77e0e4a8c --- /dev/null +++ b/Trace/real/flutter-scroll-texture-view/case.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "id": "flutter-scroll-texture-view", + "kind": "real", + "title": "Android 15 Flutter TextureView scroll", + "description": "Existing Flutter scrolling fixture rendered through TextureView.", + "scene": "scrolling", + "tags": ["android", "flutter", "scrolling", "textureview"], + "aliases": ["Scroll-Flutter-327-TextureView.pftrace"], + "known_issues": [ + "The original capture reports seven trace_sorter_negative_timestamp_dropped health events." + ], + "trace": { + "file": "trace.pftrace", + "format": "perfetto-protobuf", + "sha256": "645deaa9fc564e0ea33072c83f5ecf1d0a6f53a97055cc98c640e8294deb315e", + "materialization": "committed" + }, + "android": { + "release": "15", + "api_level": 35, + "device": "nubia pacific", + "build_fingerprint": "nubia/pacific/pacific:15/1.4.1.0/101:user/release-keys", + "compatibility": {"min_api": 35, "max_api": 35} + }, + "source": { + "origin": "Legacy SmartPerfetto test-traces fixture; original capture provenance was not recorded", + "captured_at": null, + "imported_at": "2026-07-13T00:00:00.000Z", + "license": null, + "consent": null, + "privacy_review": "pending", + "sanitization_review": "pending", + "publication": "legacy-tracked" + }, + "analysis": { + "results": ["analysis/Scroll-Flutter-327-TextureView.pftrace.fps_report.txt"], + "logs": [] + }, + "coverage": {"skills": [], "strategies": [], "expectations": []} +} diff --git a/test-traces/Scroll-Flutter-327-TextureView.pftrace b/Trace/real/flutter-scroll-texture-view/trace.pftrace similarity index 100% rename from test-traces/Scroll-Flutter-327-TextureView.pftrace rename to Trace/real/flutter-scroll-texture-view/trace.pftrace diff --git a/Trace/schema/case.schema.json b/Trace/schema/case.schema.json new file mode 100644 index 000000000..9fc07a267 --- /dev/null +++ b/Trace/schema/case.schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://smartperfetto.dev/schemas/trace-case-v1.json", + "title": "SmartPerfetto Trace Case", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "id", + "kind", + "title", + "description", + "scene", + "tags", + "trace", + "android", + "source", + "analysis", + "coverage" + ], + "properties": { + "schema_version": {"const": 1}, + "id": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"}, + "kind": {"enum": ["real", "constructed"]}, + "title": {"type": "string", "minLength": 1}, + "description": {"type": "string", "minLength": 1}, + "scene": {"type": "string", "minLength": 1}, + "tags": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "known_issues": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "aliases": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "trace": { + "type": "object", + "additionalProperties": false, + "required": ["file", "format", "sha256", "materialization"], + "properties": { + "file": {"type": "string", "minLength": 1}, + "format": {"const": "perfetto-protobuf"}, + "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, + "materialization": {"enum": ["committed", "base-plus-overlay"]} + } + }, + "android": { + "type": "object", + "required": ["release", "api_level", "device", "build_fingerprint", "compatibility"], + "properties": { + "release": {"type": ["string", "null"]}, + "api_level": {"type": ["integer", "null"], "minimum": 1}, + "device": {"type": ["string", "null"]}, + "build_fingerprint": {"type": ["string", "null"]}, + "compatibility": { + "type": "object", + "required": ["min_api", "max_api"], + "properties": { + "min_api": {"type": ["integer", "null"], "minimum": 1}, + "max_api": {"type": ["integer", "null"], "minimum": 1} + } + } + } + }, + "source": { + "type": "object", + "required": ["origin", "captured_at", "imported_at", "license", "consent", "privacy_review", "sanitization_review", "publication"], + "properties": { + "origin": {"type": "string", "minLength": 1}, + "captured_at": {"type": ["string", "null"]}, + "imported_at": {"type": "string", "minLength": 1}, + "license": {"type": ["string", "null"]}, + "consent": {"type": ["string", "null"]}, + "privacy_review": {"enum": ["approved", "not-applicable", "pending", null]}, + "sanitization_review": {"enum": ["approved", "not-applicable", "pending", null]}, + "publication": {"enum": ["public", "private", "legacy-tracked"]} + } + }, + "analysis": { + "type": "object", + "required": ["results", "logs"], + "properties": { + "results": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "logs": {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + } + }, + "trace_probe": { + "type": "object", + "additionalProperties": false, + "required": ["start_ns", "end_ns", "process_count_hint"], + "properties": { + "start_ns": {"type": "string", "pattern": "^[0-9]+$"}, + "end_ns": {"type": "string", "pattern": "^[0-9]+$"}, + "process_count_hint": {"type": "integer", "minimum": 0} + } + }, + "construction": { + "type": "object", + "required": ["base_case_id", "scenario_file", "generator_version", "seed", "output"], + "properties": { + "base_case_id": {"type": "string"}, + "scenario_file": {"type": "string"}, + "generator_version": {"type": "integer", "minimum": 1}, + "seed": {"type": "string", "minLength": 1}, + "output": {"type": "string", "minLength": 1} + } + }, + "coverage": { + "type": "object", + "required": ["skills", "strategies", "expectations"], + "properties": { + "skills": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "strategies": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "expectations": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "type", "target"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "type": {"enum": ["skill", "strategy", "sql"]}, + "target": {"type": "string", "minLength": 1}, + "mode": {"enum": ["semantic", "graceful_empty", "unavailable", "definition"]}, + "semantic_step": {"type": ["string", "null"]}, + "limitation_reason": {"type": "string", "minLength": 1}, + "expected_error": {"type": "string", "minLength": 1} + } + } + } + } + } + }, + "allOf": [ + { + "if": {"properties": {"kind": {"const": "constructed"}}}, + "then": {"required": ["construction"]} + } + ] +} diff --git a/Trace/schema/scenario.schema.json b/Trace/schema/scenario.schema.json new file mode 100644 index 000000000..c3fedce94 --- /dev/null +++ b/Trace/schema/scenario.schema.json @@ -0,0 +1,110 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://smartperfetto.dev/schemas/trace-scenario-v1.json", + "title": "SmartPerfetto Constructed Trace Scenario", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "clock", "actors", "signals"], + "properties": { + "schema_version": {"const": 1}, + "clock": { + "type": "object", + "additionalProperties": false, + "required": ["anchor", "duration_ns"], + "properties": { + "anchor": {"enum": ["trace-start", "trace-middle", "trace-end"]}, + "duration_ns": {"type": "string", "pattern": "^[1-9][0-9]*$"} + } + }, + "actors": { + "type": "object", + "additionalProperties": false, + "required": ["processes", "threads"], + "properties": { + "processes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "name"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "name": {"type": "string", "minLength": 1}, + "uid": {"type": "integer", "minimum": 0} + } + } + }, + "threads": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "process", "name"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "process": {"type": "string", "minLength": 1}, + "name": {"type": "string", "minLength": 1}, + "is_main": {"type": "boolean"} + } + } + } + } + }, + "signals": { + "type": "array", + "items": { + "type": "object", + "required": ["type", "at_ns"], + "properties": { + "type": { + "enum": [ + "atrace-slice", "atrace-counter", "atrace-async-slice", "atrace-async-track-slice", + "sched-running", "process-stats", "battery-counters", "power-rail", + "gpu-work-period", "gpu-frequency", "gpu-power-state", "cpu-frequency", "cpu-idle", + "irq-span", "frame-timeline", "lmk-kill" + ] + }, + "at_ns": {"type": "string", "pattern": "^[0-9]+$"}, + "duration_ns": {"type": "string", "pattern": "^[0-9]+$"}, + "process": {"type": "string"}, + "thread": {"type": "string"}, + "name": {"type": "string"}, + "track_name": {"type": "string"}, + "subsystem": {"type": "string"}, + "layer_name": {"type": "string"}, + "value": {"type": "number"}, + "cpu": {"type": "integer", "minimum": 0}, + "cpu_id": {"type": "integer", "minimum": 0}, + "gpu_id": {"type": "integer", "minimum": 0}, + "uid": {"type": "integer", "minimum": 0}, + "irq": {"type": "integer", "minimum": 0}, + "cookie": {"type": "integer", "minimum": 0}, + "token": {"type": "integer", "minimum": 0}, + "display_frame_token": {"type": "integer", "minimum": 0}, + "jank_type": {"type": "integer", "minimum": 0}, + "old_state": {"type": "integer", "minimum": 0}, + "new_state": {"type": "integer", "minimum": 0}, + "state": {"type": "integer", "minimum": 0}, + "kill_reason": {"type": "integer", "minimum": 0}, + "oom_score_adj": {"type": "integer"}, + "vm_rss_kb": {"type": "integer", "minimum": 0}, + "rss_anon_kb": {"type": "integer", "minimum": 0}, + "rss_file_kb": {"type": "integer", "minimum": 0}, + "rss_shmem_kb": {"type": "integer", "minimum": 0}, + "vm_swap_kb": {"type": "integer", "minimum": 0}, + "vm_hwm_kb": {"type": "integer", "minimum": 0}, + "capacity_percent": {"type": "number"}, + "charge_counter_uah": {"type": "string", "pattern": "^-?[0-9]+$"}, + "current_ua": {"type": "string", "pattern": "^-?[0-9]+$"}, + "current_avg_ua": {"type": "string", "pattern": "^-?[0-9]+$"}, + "energy_counter_uwh": {"type": "string", "pattern": "^-?[0-9]+$"}, + "voltage_uv": {"type": "string", "pattern": "^-?[0-9]+$"}, + "start_energy_uws": {"type": "string", "pattern": "^[0-9]+$"}, + "end_energy_uws": {"type": "string", "pattern": "^[0-9]+$"}, + "active_duration_ns": {"type": "string", "pattern": "^[0-9]+$"}, + "end_state": {"enum": ["R", "S", "D"]} + } + } + } + } +} diff --git a/Trace/tools/__tests__/catalog.test.cjs b/Trace/tools/__tests__/catalog.test.cjs new file mode 100644 index 000000000..7ee653336 --- /dev/null +++ b/Trace/tools/__tests__/catalog.test.cjs @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const test = require('node:test'); + +const { + discoverCoverageTargets, + loadCatalog, + resolveCaseTrace, + validateCatalog, +} = require('../lib/catalog.cjs'); + +function writeJson(filePath, value) { + fs.mkdirSync(path.dirname(filePath), {recursive: true}); + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function sha256(content) { + return crypto.createHash('sha256').update(content).digest('hex'); +} + +function createFixture() { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-catalog-')); + const trace = Buffer.from([0x0a, 0x00]); + const overlay = Buffer.from([0x0a, 0x00]); + + fs.mkdirSync(path.join(repoRoot, 'backend/skills/atomic'), {recursive: true}); + fs.mkdirSync(path.join(repoRoot, 'backend/skills/_template'), {recursive: true}); + fs.mkdirSync(path.join(repoRoot, 'backend/skills/pipelines'), {recursive: true}); + fs.mkdirSync(path.join(repoRoot, 'backend/strategies'), {recursive: true}); + fs.writeFileSync( + path.join(repoRoot, 'backend/skills/atomic/cpu_probe.skill.yaml'), + 'name: cpu_probe\ntype: atomic\n', + ); + fs.writeFileSync( + path.join(repoRoot, 'backend/skills/_template/ignored.skill.yaml'), + 'name: "{{SKILL_ID}}"\n', + ); + fs.writeFileSync( + path.join(repoRoot, 'backend/skills/pipelines/_base.skill.yaml'), + 'name: "${PIPELINE_ID}"\n', + ); + fs.writeFileSync( + path.join(repoRoot, 'backend/strategies/startup.strategy.md'), + '---\nscene: startup\n---\n', + ); + + const realDir = path.join(repoRoot, 'Trace/real/real-startup'); + fs.mkdirSync(path.join(realDir, 'analysis'), {recursive: true}); + fs.writeFileSync(path.join(realDir, 'trace.pftrace'), trace); + fs.writeFileSync(path.join(realDir, 'analysis/result.json'), '{}\n'); + writeJson(path.join(realDir, 'case.json'), { + schema_version: 1, + id: 'real-startup', + kind: 'real', + title: 'Real startup', + description: 'Captured startup trace', + scene: 'startup', + tags: ['startup'], + aliases: ['legacy-startup.pftrace'], + trace: { + file: 'trace.pftrace', + format: 'perfetto-protobuf', + sha256: sha256(trace), + materialization: 'committed', + }, + android: { + release: '15', + api_level: 35, + device: 'fixture', + build_fingerprint: null, + compatibility: {min_api: 34, max_api: 35}, + }, + source: { + origin: 'test fixture', + captured_at: null, + imported_at: '2026-07-13T00:00:00.000Z', + license: 'Apache-2.0', + consent: 'fixture-owned', + privacy_review: 'approved', + sanitization_review: 'approved', + publication: 'public', + }, + analysis: {results: ['analysis/result.json'], logs: []}, + coverage: { + skills: [], + strategies: ['startup'], + expectations: [ + {id: 'strategy-startup', type: 'strategy', target: 'startup', query: '分析启动性能'}, + ], + }, + }); + + const constructedDir = path.join(repoRoot, 'Trace/constructed/cpu-contention'); + fs.mkdirSync(path.join(constructedDir, 'analysis'), {recursive: true}); + fs.writeFileSync(path.join(constructedDir, 'trace.overlay.pftrace'), overlay); + fs.writeFileSync(path.join(constructedDir, 'scenario.json'), '{}\n'); + fs.writeFileSync(path.join(constructedDir, 'analysis/expected.json'), '{}\n'); + writeJson(path.join(constructedDir, 'case.json'), { + schema_version: 1, + id: 'cpu-contention', + kind: 'constructed', + title: 'CPU contention', + description: 'Deterministic CPU contention overlay', + scene: 'cpu', + tags: ['cpu', 'scheduler'], + aliases: [], + trace: { + file: 'trace.overlay.pftrace', + format: 'perfetto-protobuf', + sha256: sha256(overlay), + materialization: 'base-plus-overlay', + }, + android: { + release: '15', + api_level: 35, + device: 'synthetic', + build_fingerprint: null, + compatibility: {min_api: 34, max_api: 36}, + }, + source: { + origin: 'SmartPerfetto deterministic generator', + captured_at: null, + imported_at: '2026-07-13T00:00:00.000Z', + license: 'AGPL-3.0-or-later', + consent: 'generated', + privacy_review: 'not-applicable', + sanitization_review: 'not-applicable', + publication: 'public', + }, + analysis: {results: ['analysis/expected.json'], logs: []}, + construction: { + base_case_id: 'real-startup', + scenario_file: 'scenario.json', + generator_version: 1, + seed: 'cpu-contention-v1', + output: 'Trace/.generated/constructed/cpu-contention/trace.pftrace', + }, + coverage: { + skills: ['cpu_probe'], + strategies: [], + expectations: [ + { + id: 'skill-cpu-probe', + type: 'skill', + target: 'cpu_probe', + mode: 'semantic', + required_steps: ['summary'], + semantic_step: 'summary', + }, + ], + }, + }); + + return {repoRoot, realDir, constructedDir}; +} + +test('discovers runtime Skills and Strategies from source truth', () => { + const fixture = createFixture(); + const targets = discoverCoverageTargets(fixture.repoRoot); + + assert.deepEqual(targets.skills, ['cpu_probe']); + assert.deepEqual(targets.strategies, ['startup']); +}); + +test('loads and validates a complete two-kind catalog', () => { + const fixture = createFixture(); + const catalog = loadCatalog(fixture.repoRoot); + const validation = validateCatalog(fixture.repoRoot); + + assert.deepEqual(catalog.cases.map((entry) => entry.id), ['cpu-contention', 'real-startup']); + assert.equal(validation.ok, true, JSON.stringify(validation.issues, null, 2)); + assert.deepEqual(validation.coverage.missing, {skills: [], strategies: []}); + assert.equal(resolveCaseTrace(fixture.repoRoot, 'real-startup'), path.join(fixture.realDir, 'trace.pftrace')); + assert.equal(resolveCaseTrace(fixture.repoRoot, 'legacy-startup.pftrace'), path.join(fixture.realDir, 'trace.pftrace')); +}); + +test('rejects duplicate ids, unsafe paths, hash drift, and tracked private cases', () => { + const fixture = createFixture(); + const duplicateDir = path.join(fixture.repoRoot, 'Trace/real/duplicate'); + fs.cpSync(fixture.realDir, duplicateDir, {recursive: true}); + const duplicateManifestPath = path.join(duplicateDir, 'case.json'); + const duplicate = JSON.parse(fs.readFileSync(duplicateManifestPath, 'utf8')); + duplicate.trace.sha256 = '0'.repeat(64); + duplicate.analysis.results = ['../real-startup/analysis/result.json']; + duplicate.source.publication = 'private'; + writeJson(duplicateManifestPath, duplicate); + fs.writeFileSync(path.join(fixture.repoRoot, 'backend/legacy-path.ts'), "const trace = 'test-traces/old.pftrace';\n"); + + const validation = validateCatalog(fixture.repoRoot); + const codes = validation.issues.map((issue) => issue.code); + + assert.equal(validation.ok, false); + assert.ok(codes.includes('duplicate-case-id')); + assert.ok(codes.includes('unsafe-path')); + assert.ok(codes.includes('hash-mismatch')); + assert.ok(codes.includes('tracked-private-case')); + assert.ok(codes.includes('legacy-trace-reference')); +}); + +test('reports missing, stale, and expectation-free coverage targets', () => { + const fixture = createFixture(); + const manifestPath = path.join(fixture.constructedDir, 'case.json'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + manifest.coverage.skills = ['removed_skill']; + manifest.coverage.expectations = []; + writeJson(manifestPath, manifest); + + const validation = validateCatalog(fixture.repoRoot); + + assert.equal(validation.ok, false); + assert.deepEqual(validation.coverage.missing.skills, ['cpu_probe']); + assert.deepEqual(validation.coverage.stale.skills, ['removed_skill']); + assert.ok(validation.issues.some((issue) => issue.code === 'coverage-without-expectation')); +}); + +test('repository catalog preserves all six legacy trace fixtures and FPS reports', () => { + const repoRoot = path.resolve(__dirname, '../../..'); + const expected = new Map([ + ['launch_light.pftrace', 'android-startup-light'], + ['lacunh_heavy.pftrace', 'android-startup-heavy'], + ['scroll_Standard-AOSP-App-Without-PreAnimation.pftrace', 'android-scroll-standard'], + ['scroll-demo-customer-scroll.pftrace', 'android-scroll-customer'], + ['Scroll-Flutter-327-TextureView.pftrace', 'flutter-scroll-texture-view'], + ['Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace', 'flutter-scroll-surface-view'], + ]); + const catalog = loadCatalog(repoRoot); + + assert.equal(catalog.cases.filter((entry) => entry.kind === 'real').length, 6); + for (const [legacyName, caseId] of expected) { + const entry = catalog.cases.find((candidate) => candidate.id === caseId); + assert.ok(entry, `missing real case ${caseId}`); + assert.ok(entry.aliases.includes(legacyName)); + assert.equal(resolveCaseTrace(repoRoot, legacyName), path.join(entry.case_dir, 'trace.pftrace')); + assert.equal(entry.analysis.results.length, 1); + assert.match(entry.analysis.results[0], /fps_report\.txt$/); + assert.deepEqual(entry.analysis.logs, []); + } +}); + +test('repository ignores private imports and materialized constructed traces', () => { + const repoRoot = path.resolve(__dirname, '../../..'); + const gitignore = fs.readFileSync(path.join(repoRoot, '.gitignore'), 'utf8'); + + assert.match(gitignore, /^\/Trace\/real\/\.private\/$/m); + assert.match(gitignore, /^\/Trace\/\.generated\/$/m); +}); + +test('repository catalog explicitly covers every runtime Skill and Strategy', () => { + const repoRoot = path.resolve(__dirname, '../../..'); + const validation = validateCatalog(repoRoot); + + assert.deepEqual(validation.coverage.missing, {skills: [], strategies: []}); + assert.deepEqual(validation.coverage.stale, {skills: [], strategies: []}); + assert.equal( + validation.issues.filter((issue) => issue.code === 'coverage-without-expectation').length, + 0, + ); +}); diff --git a/Trace/tools/__tests__/cli.test.cjs b/Trace/tools/__tests__/cli.test.cjs new file mode 100644 index 000000000..020fc6455 --- /dev/null +++ b/Trace/tools/__tests__/cli.test.cjs @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const {spawnSync} = require('node:child_process'); +const test = require('node:test'); + +const {writeIndexes} = require('../lib/indexer.cjs'); +const {importRealCase, promoteRealCase} = require('../lib/import-real.cjs'); +const {parseArgs} = require('../trace-corpus.cjs'); + +const cliPath = path.resolve(__dirname, '../trace-corpus.cjs'); + +function writeJson(filePath, value) { + fs.mkdirSync(path.dirname(filePath), {recursive: true}); + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function sha256(content) { + return crypto.createHash('sha256').update(content).digest('hex'); +} + +function createIndexFixture() { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-index-')); + fs.mkdirSync(path.join(repoRoot, 'backend/skills'), {recursive: true}); + fs.mkdirSync(path.join(repoRoot, 'backend/strategies'), {recursive: true}); + const caseDir = path.join(repoRoot, 'Trace/real/android-startup'); + const trace = Buffer.from([0x0a, 0x00]); + fs.mkdirSync(path.join(caseDir, 'analysis'), {recursive: true}); + fs.writeFileSync(path.join(caseDir, 'trace.pftrace'), trace); + fs.writeFileSync(path.join(caseDir, 'analysis/result.txt'), 'result\n'); + writeJson(path.join(caseDir, 'case.json'), { + schema_version: 1, + id: 'android-startup', + kind: 'real', + title: 'Android Startup', + description: 'Representative startup', + scene: 'startup', + tags: ['startup'], + aliases: ['launch.pftrace'], + trace: { + file: 'trace.pftrace', + format: 'perfetto-protobuf', + sha256: sha256(trace), + materialization: 'committed', + }, + android: { + release: '15', + api_level: 35, + device: 'Pixel fixture', + build_fingerprint: null, + compatibility: {min_api: 34, max_api: 35}, + }, + source: { + origin: 'fixture', + captured_at: null, + imported_at: '2026-07-13T00:00:00.000Z', + license: 'Apache-2.0', + consent: 'fixture-owned', + privacy_review: 'approved', + sanitization_review: 'approved', + publication: 'public', + }, + analysis: {results: ['analysis/result.txt'], logs: []}, + coverage: {skills: [], strategies: [], expectations: []}, + }); + return {repoRoot}; +} + +test('writes deterministic root, real, constructed, catalog, and coverage indexes', () => { + const fixture = createIndexFixture(); + + const first = writeIndexes(fixture.repoRoot); + const second = writeIndexes(fixture.repoRoot, {check: true}); + + assert.equal(first.changed.length, 5); + assert.deepEqual(second.changed, []); + const rootReadme = fs.readFileSync(path.join(fixture.repoRoot, 'Trace/README.md'), 'utf8'); + const realReadme = fs.readFileSync(path.join(fixture.repoRoot, 'Trace/real/README.md'), 'utf8'); + assert.match(rootReadme, /Android Startup/); + assert.match(rootReadme, /\.\/real\/android-startup\//); + assert.match(realReadme, /API 35/); + assert.match(realReadme, /\.\/android-startup\//); + const catalog = JSON.parse(fs.readFileSync(path.join(fixture.repoRoot, 'Trace/catalog.json'), 'utf8')); + assert.equal(catalog.cases[0].id, 'android-startup'); + assert.equal(catalog.cases[0].case_dir, 'Trace/real/android-startup'); +}); + +test('check mode rejects a stale generated index without rewriting it', () => { + const fixture = createIndexFixture(); + writeIndexes(fixture.repoRoot); + const readmePath = path.join(fixture.repoRoot, 'Trace/README.md'); + fs.appendFileSync(readmePath, 'stale\n'); + + assert.throws(() => writeIndexes(fixture.repoRoot, {check: true}), /stale generated file/); + assert.match(fs.readFileSync(readmePath, 'utf8'), /stale/); +}); + +test('imports real cases into ignored private staging with results and logs', () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-import-')); + const sourceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-import-source-')); + const tracePath = path.join(sourceDir, 'typical.pftrace'); + const resultPath = path.join(sourceDir, 'analysis.json'); + const logPath = path.join(sourceDir, 'session.log'); + fs.writeFileSync(tracePath, Buffer.from([0x0a, 0x00])); + fs.writeFileSync(resultPath, '{}\n'); + fs.writeFileSync(logPath, 'analysis log\n'); + + const imported = importRealCase(repoRoot, { + id: 'typical-startup', + title: 'Typical startup', + description: 'A typical captured startup trace', + scene: 'startup', + tracePath, + resultPaths: [resultPath], + logPaths: [logPath], + origin: 'local capture', + android: {release: '15', api_level: 35, device: 'Pixel fixture'}, + now: '2026-07-13T00:00:00.000Z', + probeTrace: () => ({start_ns: '1', end_ns: '2', used_pids: new Set([1])}), + }); + + assert.equal(imported.manifest.source.publication, 'private'); + assert.equal(imported.caseDir, path.join(repoRoot, 'Trace/real/.private/typical-startup')); + assert.ok(fs.existsSync(path.join(imported.caseDir, 'trace.pftrace'))); + assert.ok(fs.existsSync(path.join(imported.caseDir, 'analysis/results/analysis.json'))); + assert.ok(fs.existsSync(path.join(imported.caseDir, 'analysis/logs/session.log'))); + assert.deepEqual(imported.manifest.analysis.results, ['analysis/results/analysis.json']); + assert.deepEqual(imported.manifest.analysis.logs, ['analysis/logs/session.log']); +}); + +test('failed imports leave no final or staging directory', () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-import-fail-')); + const tracePath = path.join(repoRoot, 'trace.pftrace'); + fs.writeFileSync(tracePath, Buffer.from([0x0a, 0x00])); + + assert.throws( + () => importRealCase(repoRoot, { + id: 'broken-import', + title: 'Broken import', + description: 'Probe fails', + scene: 'startup', + tracePath, + resultPaths: [], + logPaths: [], + origin: 'local capture', + android: {release: null, api_level: null, device: null}, + now: '2026-07-13T00:00:00.000Z', + probeTrace: () => { throw new Error('probe failed'); }, + }), + /probe failed/, + ); + const privateRoot = path.join(repoRoot, 'Trace/real/.private'); + assert.deepEqual(fs.existsSync(privateRoot) ? fs.readdirSync(privateRoot) : [], []); +}); + +test('promotion requires explicit publication approvals and rolls back failed validation', () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-promote-')); + const tracePath = path.join(repoRoot, 'source.pftrace'); + fs.writeFileSync(tracePath, Buffer.from([0x0a, 0x00])); + importRealCase(repoRoot, { + id: 'reviewed-case', + title: 'Reviewed case', + description: 'Ready for explicit review', + scene: 'startup', + tracePath, + origin: 'maintainer capture', + probeTrace: () => ({start_ns: '1', end_ns: '2', used_pids: new Set([1])}), + }); + + assert.throws( + () => promoteRealCase(repoRoot, {id: 'reviewed-case', license: '', consent: 'owner'}), + /license must be a non-empty string/, + ); + const privateDir = path.join(repoRoot, 'Trace/real/.private/reviewed-case'); + assert.ok(fs.existsSync(privateDir)); + + assert.throws( + () => promoteRealCase(repoRoot, { + id: 'reviewed-case', + license: 'CC-BY-4.0', + consent: 'owner-approved', + privacyReview: 'approved', + sanitizationReview: 'approved', + validateCatalog: () => ({ok: false, issues: [{message: 'fixture validation failed'}]}), + }), + /fixture validation failed/, + ); + assert.ok(fs.existsSync(privateDir)); + assert.equal(JSON.parse(fs.readFileSync(path.join(privateDir, 'case.json'), 'utf8')).source.publication, 'private'); + + const promoted = promoteRealCase(repoRoot, { + id: 'reviewed-case', + license: 'CC-BY-4.0', + consent: 'owner-approved', + privacyReview: 'approved', + sanitizationReview: 'approved', + validateCatalog: () => ({ok: true, issues: []}), + writeIndexes: () => ({changed: []}), + }); + assert.equal(promoted.manifest.source.publication, 'public'); + assert.equal(promoted.caseDir, path.join(repoRoot, 'Trace/real/reviewed-case')); + assert.ok(fs.existsSync(path.join(promoted.caseDir, 'trace.pftrace'))); + assert.equal(fs.existsSync(privateDir), false); +}); + +test('CLI indexes, validates, reports coverage, and resolves selectors', () => { + const fixture = createIndexFixture(); + const run = (...args) => spawnSync(process.execPath, [cliPath, ...args, '--repo', fixture.repoRoot], { + encoding: 'utf8', + }); + + const index = run('index'); + assert.equal(index.status, 0, index.stderr); + assert.match(index.stdout, /generated 5 file/); + + const validate = run('validate', '--check-generated'); + assert.equal(validate.status, 0, validate.stderr); + assert.match(validate.stdout, /PASS/); + + const coverage = run('coverage'); + assert.equal(coverage.status, 0, coverage.stderr); + assert.match(coverage.stdout, /Skills: 0\/0/); + + const build = run('build', '--check'); + assert.equal(build.status, 0, build.stderr); + assert.match(build.stdout, /built 0 constructed case/); + + const resolve = run('resolve', 'launch.pftrace'); + assert.equal(resolve.status, 0, resolve.stderr); + assert.equal(resolve.stdout.trim(), path.join(fixture.repoRoot, 'Trace/real/android-startup/trace.pftrace')); +}); + +test('CLI parser preserves repeated evidence flags without treating values as positionals', () => { + const parsed = parseArgs([ + 'import-real', '--id', 'typical-startup', '--result', 'one.json', '--result', 'two.json', + '--log', 'run.log', '--repo', '/tmp/trace-repo', + ]); + assert.equal(parsed.command, 'import-real'); + assert.equal(parsed.value('--id'), 'typical-startup'); + assert.deepEqual(parsed.values('--result'), ['one.json', 'two.json']); + assert.deepEqual(parsed.values('--log'), ['run.log']); + assert.deepEqual(parsed.positional, []); + assert.equal(parsed.repoRoot, '/tmp/trace-repo'); +}); diff --git a/Trace/tools/__tests__/generator.test.cjs b/Trace/tools/__tests__/generator.test.cjs new file mode 100644 index 000000000..e9c52da9c --- /dev/null +++ b/Trace/tools/__tests__/generator.test.cjs @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const {spawnSync} = require('node:child_process'); +const test = require('node:test'); + +const { + buildConstructedTrace, + encodeScenarioOverlay, + materializeTrace, + probeTrace, +} = require('../lib/generator.cjs'); +const {resolveCaseTrace} = require('../lib/catalog.cjs'); +const {buildCatalogCases} = require('../lib/builder.cjs'); + +const repoRoot = path.resolve(__dirname, '../../..'); +const traceProcessor = path.join( + repoRoot, + 'backend/prebuilts/trace_processor/darwin-arm64/trace_processor_shell', +); + +function fixtureScenario() { + return { + schema_version: 1, + clock: {anchor: 'trace-start', duration_ns: '500000000'}, + actors: { + processes: [{id: 'app', name: 'com.smartperfetto.fixture'}], + threads: [{id: 'main', process: 'app', name: 'main'}], + }, + signals: [ + { + type: 'atrace-slice', + at_ns: '1000000', + duration_ns: '4000000', + process: 'app', + thread: 'main', + name: 'SmartPerfetto::CPU_CONTENTION', + }, + { + type: 'atrace-counter', + at_ns: '2000000', + process: 'app', + thread: 'main', + name: 'SmartPerfettoPressure', + value: 73, + }, + { + type: 'sched-running', + at_ns: '1000000', + duration_ns: '3000000', + thread: 'main', + cpu: 0, + end_state: 'S', + }, + ], + }; +} + +function queryTrace(tracePath, sql) { + const result = spawnSync(traceProcessor, ['-Q', sql, tracePath], { + cwd: repoRoot, + encoding: 'utf8', + }); + assert.equal(result.status, 0, result.stderr); + return result.stdout; +} + +test('encodes deterministic isolated overlay packets with lossless timestamps', () => { + const options = { + anchorNs: '9007199254740993000', + usedPids: new Set([700000, 700001]), + sequenceId: 987654, + }; + const first = encodeScenarioOverlay(repoRoot, fixtureScenario(), options); + const second = encodeScenarioOverlay(repoRoot, fixtureScenario(), options); + + assert.deepEqual(first.buffer, second.buffer); + assert.equal(first.identities.processes.app, 700002); + assert.equal(first.identities.threads.main, 700003); + assert.equal(first.provenance.anchor_ns, '9007199254740993000'); + assert.equal(first.provenance.sequence_id, 987654); + assert.ok(first.buffer.length > 0); +}); + +test('materializes a parseable trace with slice, counter, and sched evidence', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-generator-')); + const outputPath = path.join(tempDir, 'combined.pftrace'); + const overlay = encodeScenarioOverlay(repoRoot, fixtureScenario(), { + anchorNs: '1000000000', + usedPids: new Set(), + sequenceId: 424242, + }); + + materializeTrace(Buffer.alloc(0), overlay.buffer, outputPath); + + const output = queryTrace( + outputPath, + `SELECT + (SELECT COUNT(*) FROM slice WHERE name = 'SmartPerfetto::CPU_CONTENTION') AS slices, + (SELECT COUNT(*) FROM counter_track WHERE name = 'SmartPerfettoPressure') AS counters, + (SELECT COUNT(*) FROM sched s JOIN thread t USING (utid) WHERE t.name = 'main') AS sched_rows`, + ); + assert.match(output, /1,1,[1-9][0-9]*/); +}); + +test('materializes memory, battery, power, GPU, CPU frequency, IRQ, and async evidence', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-generator-signals-')); + const outputPath = path.join(tempDir, 'combined.pftrace'); + const scenario = fixtureScenario(); + scenario.signals.push( + { + type: 'process-stats', + at_ns: '10000000', + process: 'app', + vm_rss_kb: 102400, + rss_anon_kb: 81920, + rss_file_kb: 20480, + vm_swap_kb: 1024, + vm_hwm_kb: 122880, + oom_score_adj: 200, + }, + { + type: 'process-stats', + at_ns: '20000000', + process: 'app', + vm_rss_kb: 133120, + rss_anon_kb: 102400, + rss_file_kb: 30720, + vm_swap_kb: 2048, + vm_hwm_kb: 143360, + oom_score_adj: 900, + }, + { + type: 'battery-counters', + at_ns: '30000000', + capacity_percent: 80, + charge_counter_uah: '4000000', + current_ua: '-500000', + voltage_uv: '4000000', + }, + { + type: 'battery-counters', + at_ns: '40000000', + capacity_percent: 79, + charge_counter_uah: '3999000', + current_ua: '-550000', + voltage_uv: '3980000', + }, + { + type: 'power-rail', + at_ns: '50000000', + duration_ns: '10000000', + name: 'SYNTHETIC_CPU', + subsystem: 'CPU', + start_energy_uws: '1000000', + end_energy_uws: '1100000', + }, + { + type: 'gpu-work-period', + at_ns: '70000000', + duration_ns: '10000000', + gpu_id: 0, + uid: 10999, + active_duration_ns: '8000000', + cpu: 0, + }, + {type: 'gpu-frequency', at_ns: '90000000', gpu_id: 0, value: 700000, cpu: 0}, + {type: 'cpu-frequency', at_ns: '100000000', cpu_id: 0, value: 1800000, cpu: 0}, + { + type: 'irq-span', + at_ns: '110000000', + duration_ns: '2000000', + irq: 42, + name: 'synthetic_irq', + cpu: 0, + }, + { + type: 'frame-timeline', + at_ns: '115000000', + duration_ns: '20000000', + process: 'app', + cookie: 100, + token: 200, + display_frame_token: 300, + layer_name: 'SyntheticLayer', + jank_type: 64, + }, + {type: 'gpu-power-state', at_ns: '116000000', old_state: 0, new_state: 2, cpu: 0}, + {type: 'cpu-idle', at_ns: '117000000', cpu_id: 0, state: 1, cpu: 0}, + { + type: 'atrace-async-slice', + at_ns: '120000000', + duration_ns: '5000000', + process: 'app', + thread: 'main', + name: 'SyntheticAsync', + cookie: 7, + }, + { + type: 'atrace-async-track-slice', + at_ns: '130000000', + duration_ns: '5000000', + process: 'app', + thread: 'main', + track_name: 'SyntheticTrack', + name: 'SyntheticTrackEvent', + cookie: 8, + }, + { + type: 'lmk-kill', + at_ns: '140000000', + duration_ns: '1000000', + process: 'app', + thread: 'main', + kill_reason: 3, + oom_score_adj: 900, + }, + ); + const overlay = encodeScenarioOverlay(repoRoot, scenario, { + anchorNs: '1000000000', + usedPids: new Set(), + sequenceId: 434343, + }); + materializeTrace(Buffer.alloc(0), overlay.buffer, outputPath); + + const output = queryTrace(outputPath, ` + INCLUDE PERFETTO MODULE android.battery; + INCLUDE PERFETTO MODULE android.gpu.work_period; + INCLUDE PERFETTO MODULE android.oom_adjuster; + INCLUDE PERFETTO MODULE linux.memory.process; + INCLUDE PERFETTO MODULE linux.irqs; + INCLUDE PERFETTO MODULE android.gpu.mali_power_state; + INCLUDE PERFETTO MODULE linux.cpu.idle; + INCLUDE PERFETTO MODULE android.memory.lmk; + SELECT + (SELECT COUNT(*) FROM memory_rss_and_swap_per_process) AS rss, + (SELECT COUNT(*) FROM android_battery_charge) AS battery, + (SELECT COUNT(*) FROM track WHERE type = 'power_rails') AS rails, + (SELECT COUNT(*) FROM android_gpu_work_period_track t JOIN slice s ON s.track_id = t.id) AS gpu_work, + (SELECT COUNT(*) FROM gpu_counter_track WHERE name GLOB '*freq*') AS gpu_freq, + (SELECT COUNT(*) FROM cpu_counter_track WHERE name = 'cpufreq') AS cpu_freq, + (SELECT COUNT(*) FROM linux_hard_irqs WHERE name GLOB '*synthetic_irq*') AS irq, + (SELECT COUNT(*) FROM actual_frame_timeline_slice WHERE layer_name = 'SyntheticLayer') AS frame, + (SELECT COUNT(*) FROM android_mali_gpu_power_state) AS gpu_power, + (SELECT COUNT(*) FROM cpu_idle_counters WHERE idle = 1) AS cpu_idle, + (SELECT COUNT(*) FROM slice WHERE name = 'SyntheticAsync') AS async_slice, + (SELECT COUNT(*) FROM slice s JOIN process_track pt ON pt.id = s.track_id WHERE s.name = 'SyntheticAsync') AS async_process_slice, + (SELECT COUNT(*) FROM slice s JOIN process_track pt ON pt.id = s.track_id WHERE pt.name = 'SyntheticTrack' AND s.name = 'SyntheticTrackEvent') AS named_async, + (SELECT COUNT(*) FROM android_lmk_events WHERE process_name = 'com.smartperfetto.fixture') AS lmk, + (SELECT COUNT(*) FROM android_oom_adj_intervals) AS oom_adj`); + assert.match(output, /\n(?:[1-9][0-9]*,){14}[1-9][0-9]*\s*$/); +}); + +test('rejects unsafe or imprecise scenario values', () => { + const invalid = fixtureScenario(); + invalid.signals[0].at_ns = 1; + + assert.throws( + () => encodeScenarioOverlay(repoRoot, invalid, {anchorNs: '1', usedPids: new Set(), sequenceId: 1}), + /decimal string/, + ); +}); + +test('materialization is exact protobuf concatenation', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-materialize-')); + const outputPath = path.join(tempDir, 'trace.pftrace'); + const base = Buffer.from([0x0a, 0x00]); + const overlay = Buffer.from([0x0a, 0x00, 0x0a, 0x00]); + + const result = materializeTrace(base, overlay, outputPath); + + assert.deepEqual(fs.readFileSync(outputPath), Buffer.concat([base, overlay])); + assert.equal(result.base_bytes, 2); + assert.equal(result.overlay_bytes, 4); + assert.equal(result.output_bytes, 6); +}); + +test('probes a real base and builds a combined trace inside its bounds', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trace-real-base-')); + const basePath = resolveCaseTrace(repoRoot, 'android-startup-light'); + const scenarioPath = path.join(tempDir, 'scenario.json'); + const overlayPath = path.join(tempDir, 'trace.overlay.pftrace'); + const outputPath = path.join(tempDir, 'trace.pftrace'); + fs.writeFileSync(scenarioPath, `${JSON.stringify(fixtureScenario(), null, 2)}\n`); + + const probe = probeTrace(repoRoot, basePath); + assert.ok(BigInt(probe.end_ns) > BigInt(probe.start_ns)); + assert.ok(probe.used_pids.size > 0); + + const build = buildConstructedTrace(repoRoot, { + caseId: 'cpu-contention', + basePath, + scenarioPath, + overlayPath, + outputPath, + }); + assert.ok(BigInt(build.provenance.anchor_ns) >= BigInt(probe.start_ns)); + assert.ok(BigInt(build.provenance.anchor_ns) + 500000000n <= BigInt(probe.end_ns)); + assert.equal(build.provenance.base_sha256.length, 64); + assert.equal(build.provenance.output_sha256.length, 64); + assert.deepEqual(fs.readFileSync(overlayPath), build.overlay.buffer); + + const output = queryTrace( + outputPath, + `SELECT + (SELECT COUNT(*) FROM slice WHERE name = 'SmartPerfetto::CPU_CONTENTION') AS markers, + COALESCE((SELECT value FROM stats WHERE name = 'mismatched_sched_switch_tids'), 0) AS sched_mismatches`, + ); + assert.match(output, /\n1,0\s*$/); + assert.ok(Object.values(build.provenance.cpu_map).every((cpu) => Number(cpu) > 0)); +}); + +test('rebuilds a repository constructed case with matching source hash and provenance', () => { + const result = buildCatalogCases(repoRoot, {caseIds: ['startup-lifecycle'], check: true}); + + assert.equal(result.length, 1); + assert.equal(result[0].case_id, 'startup-lifecycle'); + assert.equal(result[0].overlay_hash_matches, true); + assert.ok(fs.existsSync(path.join(repoRoot, result[0].output))); + assert.ok(fs.existsSync(path.join(repoRoot, result[0].provenance_file))); + const provenance = JSON.parse(fs.readFileSync(path.join(repoRoot, result[0].provenance_file), 'utf8')); + assert.equal(provenance.trace_processor.sha256.length, 64); + assert.match(provenance.trace_processor.version, /^Perfetto /); +}); diff --git a/Trace/tools/bootstrap-constructed-cases.cjs b/Trace/tools/bootstrap-constructed-cases.cjs new file mode 100644 index 000000000..883c7e38c --- /dev/null +++ b/Trace/tools/bootstrap-constructed-cases.cjs @@ -0,0 +1,598 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const fs = require('node:fs'); +const path = require('node:path'); + +const {discoverCoverageTargets, resolveCaseTrace} = require('./lib/catalog.cjs'); +const {buildConstructedTrace} = require('./lib/generator.cjs'); + +const repoRoot = path.resolve(__dirname, '../..'); +const yaml = require(require.resolve('js-yaml', {paths: [path.join(repoRoot, 'backend')]})); + +const FAMILIES = [ + { + id: 'startup-lifecycle', + title: 'Startup and process lifecycle', + scene: 'startup', + base: 'android-startup-heavy', + android: {release: '16', api_level: 36, device: 'Xiaomi pandora base'}, + skillPattern: /(^startup|launcher|app_process_start)/, + strategies: ['startup'], + signatures: ['MetricsLogger:launchObserverNotifyIntentStarted', 'activityStart', 'bindApplication'], + }, + { + id: 'scheduler-cpu-contention', + title: 'Scheduler and CPU contention', + scene: 'cpu', + base: 'android-scroll-customer', + android: {release: '16', api_level: 36, device: 'OPPO PKH110 base'}, + skillPattern: /(cpu|sched|thread_affinity|runqueue|irq|futex|cache_miss|system_load|util_tracking|task_migration)/, + strategies: [], + signatures: ['RunnableWait', 'CPU contention', 'sched_blocked_reason'], + }, + { + id: 'rendering-jank', + title: 'Rendering pipeline jank', + scene: 'scrolling', + base: 'android-scroll-customer', + android: {release: '16', api_level: 36, device: 'OPPO PKH110 base'}, + skillPattern: /(frame|jank|scroll|render|surfaceflinger|sf_|vsync|vrr|textureview|buffer_transaction|consumer|choreographer|fpsgo|game_fps|gl_standalone|present_fence)/, + strategies: ['game', 'scroll_response', 'scrolling'], + signatures: ['Choreographer#doFrame', 'DrawFrame', 'SurfaceFlinger::commit', 'queueBuffer'], + }, + { + id: 'input-interaction-latency', + title: 'Input and interaction latency', + scene: 'interaction', + base: 'android-scroll-customer', + android: {release: '16', api_level: 36, device: 'OPPO PKH110 base'}, + skillPattern: /(input|touch|click|navigation|app_lifecycle|scene_reconstruction|state_timeline)/, + strategies: ['interaction', 'touch_tracking'], + signatures: ['InputDispatcher::dispatch', 'deliverInputEvent', 'performClick'], + }, + { + id: 'binder-io-blocking', + title: 'Binder, lock, and I/O blocking', + scene: 'io', + base: 'android-startup-heavy', + android: {release: '16', api_level: 36, device: 'Xiaomi pandora base'}, + skillPattern: /(binder|blocking_chain|lock|file_io|page_fault|block_io|io_pressure|filesystem|anr)/, + strategies: ['anr', 'io'], + signatures: ['binder transaction', 'monitor contention', 'FileIO'], + }, + { + id: 'memory-gc-pressure', + title: 'Memory and GC pressure', + scene: 'memory', + base: 'android-startup-light', + android: {release: '16', api_level: 36, device: 'Google raven base'}, + skillPattern: /(memory|heap|rss|lmk|oom|gc|native_heap|bitmap|dmabuf)/, + strategies: ['memory'], + signatures: ['GC Young Concurrent', 'HeapTrim', 'memory pressure'], + }, + { + id: 'power-thermal', + title: 'Power and thermal throttling', + scene: 'power', + base: 'android-scroll-customer', + android: {release: '16', api_level: 36, device: 'OPPO PKH110 base'}, + skillPattern: /(power|battery|thermal|wakelock|wakeup|doze|dvfs|screen_off|suspend)/, + strategies: ['power'], + signatures: ['thermal_throttling', 'WakeLock', 'device_suspend'], + }, + { + id: 'gpu-workload', + title: 'GPU workload and frequency', + scene: 'gpu', + base: 'android-scroll-customer', + android: {release: '16', api_level: 36, device: 'OPPO PKH110 base'}, + skillPattern: /(gpu|mali|vulkan|opengl|angle)/, + strategies: [], + signatures: ['GPU completion', 'vkQueueSubmit', 'gpu_work_period'], + }, + { + id: 'linux-system-state', + title: 'Linux and Android system state', + scene: 'linux', + base: 'android-startup-light', + android: {release: '16', api_level: 36, device: 'Google raven base'}, + skillPattern: /(linux|systemd|device_state|trace_state|android_job|logcat)/, + strategies: ['linux'], + signatures: ['system_server state', 'journald', 'JobScheduler'], + }, + { + id: 'media-network-camera', + title: 'Media, network, and camera pipeline', + scene: 'media', + base: 'android-scroll-customer', + android: {release: '16', api_level: 36, device: 'OPPO PKH110 base'}, + skillPattern: /(media|network|modem|camera|webview_v8)/, + strategies: ['media', 'network'], + signatures: ['Camera3-Device', 'MediaCodec::queueInputBuffer', 'NetworkRequest'], + }, + { + id: 'framework-pipelines', + title: 'Framework rendering pipeline signatures', + scene: 'pipeline', + base: 'android-startup-heavy', + android: {release: '16', api_level: 36, device: 'Xiaomi pandora base'}, + skillPattern: /(^pipeline_|_module$|flutter_scrolling|rn_|compose_|webview_draw|code_pinpoint)/, + strategies: ['pipeline'], + signatures: ['Recomposer:recompose', 'Flutter::BeginFrame', 'RN::FabricCommit', 'WebView::DrawFun'], + }, + { + id: 'general-runtime-contracts', + title: 'General analysis and runtime contracts', + scene: 'general', + base: 'android-scroll-customer', + android: {release: '16', api_level: 36, device: 'OPPO PKH110 base'}, + skillPattern: /.*/, + strategies: [ + 'general', + 'multi_trace_result_comparison', + 'overview', + 'runtime_correctness', + 'smart', + 'teaching', + 'verifier_misdiagnosis', + ], + signatures: ['SmartPerfetto::general-analysis', 'trace overview', 'comparison baseline'], + }, +]; + +const SEMANTIC_STEP_OVERRIDES = new Map([ + ['cpu_topology_view', 'read_topology'], + ['network_analysis', 'network_slice_overview'], +]); + +const EXPECTED_LIMITATIONS = new Map([ + ['android_kernel_wakelock_summary', {mode: 'graceful_empty', reason: 'The current generator does not emit android_kernel_wakelock counter tracks.'}], + ['binder_root_cause', {mode: 'graceful_empty', reason: 'The fixture has blocking slices but no kernel Binder transaction packet chain.'}], + ['block_io_analysis', {mode: 'graceful_empty', reason: 'The fixture does not yet emit block_rq ftrace events.'}], + ['io_pressure', {mode: 'graceful_empty', reason: 'The fixture does not yet emit PSI I/O pressure counters.'}], + ['callstack_analysis', {mode: 'graceful_empty', reason: 'The fixture does not contain perf samples or interned callstacks.'}], + ['linux_perf_counter_hotspots', {mode: 'graceful_empty', reason: 'The fixture does not contain PMU perf sample/counter packets.'}], + ['android_heap_graph_leak_candidates', {mode: 'graceful_empty', reason: 'The fixture does not contain a managed heap graph dump.'}], + ['android_memory_v57_ai_diagnostics', {mode: 'graceful_empty', reason: 'Heap-graph diagnostics remain empty without a managed heap graph dump.'}], + ['native_heap_breakdown', {mode: 'graceful_empty', reason: 'The fixture does not contain heapprofd allocation packets.'}], + ['wattson_app_startup_power', {mode: 'graceful_empty', reason: 'Wattson startup attribution is device-model gated and unsupported by this base device.'}], + ['dmabuf_analysis', {mode: 'unavailable', reason: 'The fixture does not contain DMA-BUF allocation/residency events.', expected_error: 'Condition not met'}], + ['gc_analysis', {mode: 'unavailable', reason: 'The fixture has GC marker slices but not ART garbage-collection packets.', expected_error: 'Condition not met'}], + ['scroll_session_analysis', {mode: 'unavailable', reason: 'The generator does not yet emit Winscope android_input_event packets.', expected_error: 'Trace is missing required tables: android_input_event'}], +]); + +function listSkillFiles(root) { + const result = []; + const stack = [root]; + while (stack.length > 0) { + const current = stack.pop(); + for (const entry of fs.readdirSync(current, {withFileTypes: true})) { + const absolute = path.join(current, entry.name); + if (entry.isDirectory()) stack.push(absolute); + else if ( + entry.isFile() && + entry.name.endsWith('.skill.yaml') && + !entry.name.startsWith('_') && + !absolute.split(path.sep).includes('_template') + ) result.push(absolute); + } + } + return result.sort(); +} + +function loadSkills() { + return listSkillFiles(path.join(repoRoot, 'backend/skills')).map((filePath) => { + const definition = yaml.load(fs.readFileSync(filePath, 'utf8')); + return {filePath, definition}; + }); +} + +function loadStrategyFixtures() { + const strategiesRoot = path.join(repoRoot, 'backend/strategies'); + return new Map( + fs.readdirSync(strategiesRoot) + .filter((name) => name.endsWith('.strategy.md')) + .sort() + .map((name) => { + const filePath = path.join(strategiesRoot, name); + const source = fs.readFileSync(filePath, 'utf8'); + const frontmatter = source.match(/^---\s*$([\s\S]*?)^---\s*$/m); + if (!frontmatter) throw new Error(`Strategy has no frontmatter: ${name}`); + const definition = yaml.load(frontmatter[1]); + const keywords = Array.isArray(definition.keywords) + ? definition.keywords.filter((keyword) => typeof keyword === 'string' && keyword.trim() !== '') + : []; + const query = keywords[0] ?? (definition.scene === 'general' ? '分析这份 trace' : null); + if (definition.strategy_kind !== 'contract_only' && !query) { + throw new Error(`Strategy has no classifier fixture query: ${definition.scene}`); + } + return [definition.scene, {query, source_file: path.relative(repoRoot, filePath).split(path.sep).join('/')}]; + }), + ); +} + +function familyForSkill(skillId) { + return FAMILIES.find((family) => family.skillPattern.test(skillId)); +} + +function parameterValue(input, identities) { + const name = String(input.name ?? ''); + if (name === 'slice_names') return "'Choreographer#doFrame','DrawFrame','queueBuffer','latchBuffer'"; + if (name === 'package' || name.includes('process_name')) { + return input.required ? 'com.smartperfetto.fixture' : ''; + } + if (name === 'startup_type' || name === 'launch_type') return 'cold'; + if (name === 'event_type') return 'tap'; + if (name === 'event_action') return 'ACTION_UP'; + if (name === 'event_ts' || name.endsWith('start_ts')) return '${trace_start}'; + if (name === 'event_end_ts' || name.endsWith('end_ts')) return '${trace_end}'; + if (name === 'pid' || name.endsWith('_pid')) return identities.processes.app; + if (name === 'tid' || name.endsWith('_tid')) return identities.threads.main; + if (name === 'upid') return '${fixture_upid}'; + if (name === 'utid') return '${fixture_utid}'; + if (name.includes('frame') || name.includes('vsync') || name.endsWith('_id')) return 1; + if (input.default !== undefined) return input.default; + if (input.type === 'boolean') return false; + if (['number', 'integer', 'timestamp', 'duration'].includes(input.type)) return 1; + if (input.type === 'array') return []; + return ''; +} + +function skillExpectation(skill, family, identities) { + const definition = skill.definition; + const steps = Array.isArray(definition.steps) ? definition.steps : []; + const parameters = {}; + for (const input of Array.isArray(definition.inputs) ? definition.inputs : []) { + if (input.required || input.default !== undefined) { + parameters[input.name] = parameterValue(input, identities); + } + } + if (definition.name === 'jank_frame_detail') { + parameters.package = 'com.smartperfetto.fixture'; + parameters.pid = identities.processes.app; + parameters.start_ts = '${trace_start}'; + parameters.end_ts = '${trace_end}'; + } + if (steps.length === 0) { + return { + id: `definition-${definition.name}`, + type: 'skill', + target: definition.name, + mode: 'definition', + source_file: path.relative(repoRoot, skill.filePath).split(path.sep).join('/'), + required_marker: `SmartPerfetto::CASE::${family.id}`, + }; + } + const overrideStep = SEMANTIC_STEP_OVERRIDES.get(definition.name); + const semanticStepIndex = overrideStep + ? steps.findIndex((step) => step.id === overrideStep) + : steps.findIndex((step) => + typeof step?.id === 'string' && + step.type === 'atomic' && + typeof step.sql === 'string' && + step.sql.trim() !== '' && + step.display && + step.display !== false && + step.display?.level !== 'hidden', + ); + const selectedStepIndex = semanticStepIndex >= 0 + ? semanticStepIndex + : steps.findIndex((step) => typeof step?.id === 'string'); + const requiredSteps = selectedStepIndex >= 0 + ? steps.slice(0, selectedStepIndex + 1).map((step) => step.id).filter(Boolean) + : []; + const limitation = EXPECTED_LIMITATIONS.get(definition.name); + return { + id: `execute-${definition.name}`, + type: 'skill', + target: definition.name, + mode: limitation?.mode ?? 'semantic', + source_file: path.relative(repoRoot, skill.filePath).split(path.sep).join('/'), + parameters, + required_steps: requiredSteps, + semantic_step: selectedStepIndex >= 0 ? steps[selectedStepIndex].id : null, + ...(limitation ? {limitation_reason: limitation.reason} : {}), + ...(limitation?.expected_error ? {expected_error: limitation.expected_error} : {}), + required_marker: `SmartPerfetto::CASE::${family.id}`, + }; +} + +function strategyExpectation(strategy, family, fixture) { + return { + id: `strategy-${strategy}`, + type: 'strategy', + target: strategy, + query: fixture.query ?? `contract-only:${strategy}`, + expected_strategy: strategy, + source_file: fixture.source_file, + required_marker: `SmartPerfetto::CASE::${family.id}`, + }; +} + +function scenarioForFamily(family) { + const slices = [`SmartPerfetto::CASE::${family.id}`, ...family.signatures]; + const targetedSlices = { + 'scheduler-cpu-contention': [ + ['system', 'system-main', 'sched_blocked_reason'], + ], + 'rendering-jank': [ + ['app', 'game', 'PlayerLoop'], + ['app', 'game', 'eglSwapBuffers'], + ['app', 'game', 'eglSwapBuffers'], + ['app', 'game', 'eglSwapBuffers'], + ['app', 'rn-js', 'BatchedBridge::callFunctionReturnFlushedQueue'], + ['app', 'rn-native', 'UIManager::dispatchViewUpdates'], + ['app', 'rn-native', 'FabricMount::executeMount'], + ['app', 'webview', 'DrawGL::DrawFunctor'], + ['app', 'webview', 'V8.GCCompactor'], + ], + 'input-interaction-latency': [ + ['system', 'system-main', 'InputDispatcher::dispatchMotion'], + ['app', 'main', 'deliverInputEvent src=0x1002'], + ['app', 'main', 'performClick'], + ['app', 'main', 'ActivityThread::performCreate'], + ['app', 'main', 'ActivityThread::performResume'], + ], + 'binder-io-blocking': [ + ['system', 'system-main', 'binder transaction'], + ['app', 'main', 'monitor contention'], + ['app', 'main', 'FileIO::fsync'], + ], + 'gpu-workload': [ + ['app', 'render', 'vkQueueSubmit'], + ['app', 'render', 'eglSwapBuffers'], + ], + 'linux-system-state': [ + ['system', 'system-main', 'device_idle'], + ], + 'media-network-camera': [ + ['app', 'webview', 'V8.GCCompactor'], + ['app', 'webview', 'v8.run::LongTask'], + ['app', 'render', 'MediaCodec::queueInputBuffer'], + ['app', 'render', 'Camera3-Device::processCaptureRequest'], + ['app', 'main', 'NetworkRequest::TTFB'], + ], + 'framework-pipelines': [ + ['app', 'main', 'Choreographer#doFrame'], + ['app', 'render', 'DrawFrame'], + ['app', 'render', 'queueBuffer'], + ['sf', 'sf-main', 'latchBuffer'], + ['system', 'system-main', 'WindowAnimation'], + ['system', 'system-main', 'AppTransition'], + ['app', 'rn-native', 'FabricMount::executeMount'], + ['app', 'webview', 'DrawGL::DrawFunctor'], + ], + }[family.id] ?? []; + const familySignals = []; + if (family.id === 'scheduler-cpu-contention') { + familySignals.push( + {type: 'cpu-frequency', at_ns: '520000000', cpu_id: 0, value: 1800000, cpu: 0}, + {type: 'irq-span', at_ns: '540000000', duration_ns: '4000000', irq: 42, name: 'synthetic_irq', cpu: 0}, + { + type: 'atrace-async-track-slice', + at_ns: '560000000', + duration_ns: '30000000', + process: 'system', + thread: 'system-main', + track_name: 'JobScheduler', + name: 'scheduled job #12#<10999>com.smartperfetto.fixture/.SyntheticJob#', + cookie: 12, + }, + ); + } + if (family.id === 'rendering-jank') { + familySignals.push( + {type: 'frame-timeline', at_ns: '280000000', duration_ns: '160000000', process: 'app', cookie: 101, token: 201, display_frame_token: 301, layer_name: 'SyntheticJankLayer', jank_type: 64}, + {type: 'gpu-frequency', at_ns: '500000000', gpu_id: 0, value: 700000, cpu: 0}, + ); + } + if (family.id === 'input-interaction-latency' || family.id === 'linux-system-state') { + familySignals.push( + {type: 'atrace-counter', at_ns: '520000000', process: 'system', thread: 'system-main', name: 'ScreenState', value: 1}, + {type: 'battery-counters', at_ns: '540000000', capacity_percent: 80, charge_counter_uah: '4000000', current_ua: '-500000', voltage_uv: '4000000'}, + ); + if (family.id === 'input-interaction-latency') { + familySignals.push( + {type: 'atrace-counter', at_ns: '560000000', process: 'system', thread: 'system-main', name: 'DozeDeepState', value: 5}, + {type: 'atrace-counter', at_ns: '660000000', process: 'system', thread: 'system-main', name: 'DozeDeepState', value: 0}, + ); + } + } + if (family.id === 'memory-gc-pressure') { + familySignals.push( + {type: 'process-stats', at_ns: '400000000', process: 'app', vm_rss_kb: 102400, rss_anon_kb: 81920, rss_file_kb: 20480, rss_shmem_kb: 1024, vm_swap_kb: 1024, vm_hwm_kb: 122880, oom_score_adj: 200}, + {type: 'process-stats', at_ns: '600000000', process: 'app', vm_rss_kb: 174080, rss_anon_kb: 133120, rss_file_kb: 40960, rss_shmem_kb: 2048, vm_swap_kb: 4096, vm_hwm_kb: 184320, oom_score_adj: 900}, + {type: 'lmk-kill', at_ns: '700000000', duration_ns: '1000000', process: 'app', thread: 'main', kill_reason: 3, oom_score_adj: 900}, + ); + } + if (family.id === 'power-thermal') { + familySignals.push( + {type: 'battery-counters', at_ns: '400000000', capacity_percent: 80, charge_counter_uah: '4000000', current_ua: '-500000', voltage_uv: '4000000'}, + {type: 'battery-counters', at_ns: '600000000', capacity_percent: 79, charge_counter_uah: '3999000', current_ua: '-550000', voltage_uv: '3980000'}, + {type: 'atrace-counter', at_ns: '410000000', process: 'system', thread: 'system-main', name: 'DozeDeepState', value: 5}, + {type: 'atrace-counter', at_ns: '610000000', process: 'system', thread: 'system-main', name: 'DozeDeepState', value: 0}, + {type: 'atrace-counter', at_ns: '420000000', process: 'system', thread: 'system-main', name: 'domain@0 Frequency', value: 800000}, + {type: 'atrace-counter', at_ns: '620000000', process: 'system', thread: 'system-main', name: 'domain@0 Frequency', value: 1600000}, + {type: 'power-rail', at_ns: '430000000', duration_ns: '200000000', name: 'SYNTHETIC_CPU', subsystem: 'CPU', start_energy_uws: '1000000', end_energy_uws: '1300000'}, + {type: 'gpu-work-period', at_ns: '450000000', duration_ns: '30000000', gpu_id: 0, uid: 10999, active_duration_ns: '24000000', cpu: 0}, + {type: 'gpu-frequency', at_ns: '450000000', gpu_id: 0, value: 700000, cpu: 0}, + {type: 'gpu-power-state', at_ns: '450000000', old_state: 0, new_state: 2, cpu: 0}, + {type: 'cpu-frequency', at_ns: '460000000', cpu_id: 0, value: 1800000, cpu: 0}, + {type: 'cpu-idle', at_ns: '470000000', cpu_id: 0, state: 1, cpu: 0}, + ); + } + if (family.id === 'gpu-workload') { + familySignals.push( + {type: 'gpu-work-period', at_ns: '450000000', duration_ns: '30000000', gpu_id: 0, uid: 10999, active_duration_ns: '24000000', cpu: 0}, + {type: 'gpu-frequency', at_ns: '450000000', gpu_id: 0, value: 700000, cpu: 0}, + {type: 'gpu-power-state', at_ns: '460000000', old_state: 0, new_state: 2, cpu: 0}, + ); + } + return { + schema_version: 1, + clock: {anchor: 'trace-middle', duration_ns: '1000000000'}, + actors: { + processes: [ + {id: 'app', name: 'com.smartperfetto.fixture', uid: 10999}, + {id: 'system', name: 'system_server', uid: 1000}, + {id: 'sf', name: '/system/bin/surfaceflinger', uid: 1000}, + ], + threads: [ + {id: 'main', process: 'app', name: 'main', is_main: true}, + {id: 'render', process: 'app', name: 'RenderThread'}, + {id: 'game', process: 'app', name: 'GameThread'}, + {id: 'rn-js', process: 'app', name: 'mqt_js'}, + {id: 'rn-native', process: 'app', name: 'mqt_native_modules'}, + {id: 'webview', process: 'app', name: 'CrRendererMain'}, + {id: 'system-main', process: 'system', name: 'android.fg', is_main: true}, + {id: 'sf-main', process: 'sf', name: 'surfaceflinger', is_main: true}, + ], + }, + signals: [ + ...slices.map((name, index) => ({ + type: 'atrace-slice', + at_ns: String(10000000 + index * 30000000), + duration_ns: String(18000000 + index * 1000000), + process: 'app', + thread: index % 2 === 0 ? 'main' : 'render', + name, + })), + ...targetedSlices.map(([process, thread, name], index) => ({ + type: 'atrace-slice', + at_ns: String(200000000 + index * 25000000), + duration_ns: String(22000000 + index * 1000000), + process, + thread, + name, + })), + { + type: 'atrace-counter', + at_ns: '250000000', + process: 'app', + thread: 'main', + name: `SmartPerfetto.${family.id}.pressure`, + value: 73, + }, + { + type: 'sched-running', + at_ns: '300000000', + duration_ns: '45000000', + thread: 'main', + cpu: 0, + end_state: family.id === 'binder-io-blocking' ? 'D' : 'S', + }, + { + type: 'sched-running', + at_ns: '380000000', + duration_ns: '20000000', + thread: 'main', + cpu: 0, + end_state: family.id === 'binder-io-blocking' ? 'D' : 'S', + }, + ...familySignals, + ], + }; +} + +function writeJson(filePath, value) { + fs.mkdirSync(path.dirname(filePath), {recursive: true}); + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function main() { + const targets = discoverCoverageTargets(repoRoot); + const skills = loadSkills(); + const strategyFixtures = loadStrategyFixtures(); + const skillsByName = new Map(skills.map((skill) => [skill.definition.name, skill])); + if (skillsByName.size !== targets.skills.length) { + throw new Error(`Skill source mismatch: discovered=${targets.skills.length}, parsed=${skillsByName.size}`); + } + const assignedStrategies = new Set(FAMILIES.flatMap((family) => family.strategies)); + const missingStrategies = targets.strategies.filter((strategy) => !assignedStrategies.has(strategy)); + if (missingStrategies.length > 0) throw new Error(`Unassigned Strategies: ${missingStrategies.join(', ')}`); + + for (const family of FAMILIES) { + const caseDir = path.join(repoRoot, 'Trace/constructed', family.id); + const scenarioPath = path.join(caseDir, 'scenario.json'); + const overlayPath = path.join(caseDir, 'trace.overlay.pftrace'); + const outputPath = path.join(repoRoot, 'Trace/.generated/constructed', family.id, 'trace.pftrace'); + const scenario = scenarioForFamily(family); + writeJson(scenarioPath, scenario); + const build = buildConstructedTrace(repoRoot, { + caseId: family.id, + basePath: resolveCaseTrace(repoRoot, family.base), + scenarioPath, + overlayPath, + outputPath, + }); + const familySkills = targets.skills.filter((skillId) => familyForSkill(skillId).id === family.id); + const expectations = [ + ...familySkills.map((skillId) => skillExpectation(skillsByName.get(skillId), family, build.overlay.identities)), + ...family.strategies.map((strategy) => { + const fixture = strategyFixtures.get(strategy); + if (!fixture) throw new Error(`Missing Strategy fixture: ${strategy}`); + return strategyExpectation(strategy, family, fixture); + }), + ]; + const expectedPath = path.join(caseDir, 'analysis/expected.json'); + writeJson(expectedPath, { + schema_version: 1, + case_id: family.id, + marker: `SmartPerfetto::CASE::${family.id}`, + expectations, + }); + writeJson(path.join(caseDir, 'case.json'), { + schema_version: 1, + id: family.id, + kind: 'constructed', + title: family.title, + description: `Deterministic ${family.title.toLowerCase()} signals over the ${family.base} real base trace.`, + scene: family.scene, + tags: ['android', 'constructed', family.scene], + aliases: [], + trace: { + file: 'trace.overlay.pftrace', + format: 'perfetto-protobuf', + sha256: build.provenance.overlay_sha256, + materialization: 'base-plus-overlay', + }, + android: { + ...family.android, + build_fingerprint: null, + compatibility: {min_api: 35, max_api: 36}, + }, + source: { + origin: 'SmartPerfetto deterministic trace generator', + captured_at: null, + imported_at: '2026-07-13T00:00:00.000Z', + license: 'AGPL-3.0-or-later', + consent: 'generated', + privacy_review: 'not-applicable', + sanitization_review: 'not-applicable', + publication: 'public', + }, + analysis: {results: ['analysis/expected.json'], logs: []}, + construction: { + base_case_id: family.base, + scenario_file: 'scenario.json', + generator_version: 1, + seed: `${family.id}-v1`, + output: `Trace/.generated/constructed/${family.id}/trace.pftrace`, + }, + coverage: { + skills: familySkills, + strategies: family.strategies, + expectations, + }, + }); + writeJson( + path.join(repoRoot, 'Trace/.generated/constructed', family.id, 'build-provenance.json'), + build.provenance, + ); + console.log(`${family.id}: ${familySkills.length} Skills, ${family.strategies.length} Strategies`); + } +} + +main(); diff --git a/Trace/tools/lib/builder.cjs b/Trace/tools/lib/builder.cjs new file mode 100644 index 000000000..15af8d695 --- /dev/null +++ b/Trace/tools/lib/builder.cjs @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const {spawnSync} = require('node:child_process'); + +const {loadCatalog, resolveCaseTrace} = require('./catalog.cjs'); +const {buildConstructedTrace, resolveTraceProcessor} = require('./generator.cjs'); +const {sha256File} = require('./hash.cjs'); + +function traceProcessorProvenance(repoRoot) { + const executable = resolveTraceProcessor(repoRoot); + const version = spawnSync(executable, ['--version'], {encoding: 'utf8'}); + if (version.error) throw version.error; + if (version.status !== 0) throw new Error(`trace_processor_shell --version failed: ${version.stderr}`); + return { + path: path.relative(repoRoot, executable).split(path.sep).join('/'), + sha256: sha256File(executable), + version: version.stdout.trim().split(/\r?\n/)[0], + }; +} + +function safeGeneratedPath(repoRoot, relativePath, caseId) { + const generatedRoot = path.resolve(repoRoot, 'Trace/.generated/constructed', caseId); + const output = path.resolve(repoRoot, relativePath); + if (output !== path.join(generatedRoot, 'trace.pftrace')) { + throw new Error(`constructed output must be Trace/.generated/constructed/${caseId}/trace.pftrace`); + } + return output; +} + +function updateTraceHash(entry, sha256) { + const manifest = JSON.parse(fs.readFileSync(entry.manifest_path, 'utf8')); + manifest.trace.sha256 = sha256; + fs.writeFileSync(entry.manifest_path, `${JSON.stringify(manifest, null, 2)}\n`); +} + +function buildCatalogCases(repoRoot, options = {}) { + const catalog = loadCatalog(repoRoot); + const constructed = catalog.cases.filter((entry) => entry.kind === 'constructed'); + const requested = options.caseIds ? new Set(options.caseIds) : null; + if (requested) { + const known = new Set(constructed.map((entry) => entry.id)); + const unknown = [...requested].filter((id) => !known.has(id)); + if (unknown.length > 0) throw new Error(`Unknown constructed case(s): ${unknown.join(', ')}`); + } + const selected = requested ? constructed.filter((entry) => requested.has(entry.id)) : constructed; + const results = []; + // An empty selection is a valid no-op for catalog-only fixtures. Resolve the + // executable only when a trace will actually be built. + const traceProcessor = selected.length > 0 ? traceProcessorProvenance(repoRoot) : null; + + for (const entry of selected) { + const outputPath = safeGeneratedPath(repoRoot, entry.construction.output, entry.id); + const committedOverlayPath = path.resolve(entry.case_dir, entry.trace.file); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `trace-build-${entry.id}-`)); + const generatedOverlayPath = options.check + ? path.join(tempDir, 'trace.overlay.pftrace') + : committedOverlayPath; + try { + const build = buildConstructedTrace(repoRoot, { + caseId: entry.id, + basePath: resolveCaseTrace(repoRoot, entry.construction.base_case_id), + scenarioPath: path.resolve(entry.case_dir, entry.construction.scenario_file), + overlayPath: generatedOverlayPath, + outputPath, + }); + const overlayHashMatches = build.provenance.overlay_sha256 === entry.trace.sha256; + if (options.check && !overlayHashMatches) { + throw new Error( + `constructed overlay drift for ${entry.id}: manifest=${entry.trace.sha256}, generated=${build.provenance.overlay_sha256}`, + ); + } + if (!options.check && !overlayHashMatches) updateTraceHash(entry, build.provenance.overlay_sha256); + + const provenancePath = path.join(path.dirname(outputPath), 'build-provenance.json'); + const provenance = { + schema_version: 1, + generator_version: entry.construction.generator_version, + scenario_file: path.relative(repoRoot, path.resolve(entry.case_dir, entry.construction.scenario_file)).split(path.sep).join('/'), + overlay_file: path.relative(repoRoot, committedOverlayPath).split(path.sep).join('/'), + output_file: entry.construction.output, + trace_processor: traceProcessor, + ...build.provenance, + }; + fs.writeFileSync(provenancePath, `${JSON.stringify(provenance, null, 2)}\n`); + results.push({ + case_id: entry.id, + output: entry.construction.output, + provenance_file: path.relative(repoRoot, provenancePath).split(path.sep).join('/'), + overlay_hash_matches: options.check ? overlayHashMatches : true, + }); + } finally { + fs.rmSync(tempDir, {recursive: true, force: true}); + } + } + return results; +} + +module.exports = {buildCatalogCases}; diff --git a/Trace/tools/lib/catalog.cjs b/Trace/tools/lib/catalog.cjs new file mode 100644 index 000000000..709a99ac5 --- /dev/null +++ b/Trace/tools/lib/catalog.cjs @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const fs = require('node:fs'); +const path = require('node:path'); + +const {sha256File} = require('./hash.cjs'); + +const CASE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const HASH_PATTERN = /^[a-f0-9]{64}$/; + +function listFilesRecursive(root, predicate) { + if (!fs.existsSync(root)) return []; + const result = []; + const stack = [root]; + while (stack.length > 0) { + const current = stack.pop(); + for (const entry of fs.readdirSync(current, {withFileTypes: true})) { + const absolute = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(absolute); + } else if (entry.isFile() && predicate(absolute)) { + result.push(absolute); + } + } + } + return result.sort(); +} + +function parseScalarField(content, field) { + const pattern = new RegExp(`^${field}:\\s*["']?([^"'\\s#]+)`, 'm'); + return content.match(pattern)?.[1] ?? null; +} + +function discoverCoverageTargets(repoRoot) { + const skillsRoot = path.join(repoRoot, 'backend', 'skills'); + const strategiesRoot = path.join(repoRoot, 'backend', 'strategies'); + const skills = listFilesRecursive( + skillsRoot, + (filePath) => + filePath.endsWith('.skill.yaml') && + !filePath.split(path.sep).includes('_template') && + !path.basename(filePath).startsWith('_'), + ).map((filePath) => { + const name = parseScalarField(fs.readFileSync(filePath, 'utf8'), 'name'); + if (!name || name.includes('{{') || name.includes('${')) { + throw new Error(`Skill has no concrete name: ${path.relative(repoRoot, filePath)}`); + } + return name; + }); + const strategies = listFilesRecursive( + strategiesRoot, + (filePath) => filePath.endsWith('.strategy.md'), + ).map((filePath) => { + const scene = parseScalarField(fs.readFileSync(filePath, 'utf8'), 'scene'); + if (!scene) { + throw new Error(`Strategy has no scene: ${path.relative(repoRoot, filePath)}`); + } + return scene; + }); + return { + skills: [...new Set(skills)].sort(), + strategies: [...new Set(strategies)].sort(), + }; +} + +function caseManifestPaths(repoRoot) { + const traceRoot = path.join(repoRoot, 'Trace'); + return ['real', 'constructed'].flatMap((kind) => + listFilesRecursive( + path.join(traceRoot, kind), + (filePath) => + path.basename(filePath) === 'case.json' && + !path.relative(traceRoot, filePath).split(path.sep).includes('.private'), + ), + ); +} + +function loadCatalog(repoRoot) { + const cases = caseManifestPaths(repoRoot).map((manifestPath) => { + let manifest; + try { + manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + } catch (error) { + throw new Error(`Cannot parse ${path.relative(repoRoot, manifestPath)}: ${error.message}`); + } + return { + ...manifest, + case_dir: path.dirname(manifestPath), + manifest_path: manifestPath, + }; + }); + return {cases: cases.sort((a, b) => String(a.id).localeCompare(String(b.id)))}; +} + +function isSafeCasePath(caseDir, relativePath) { + if (typeof relativePath !== 'string' || relativePath.length === 0 || path.isAbsolute(relativePath)) { + return false; + } + const resolved = path.resolve(caseDir, relativePath); + const prefix = `${path.resolve(caseDir)}${path.sep}`; + return resolved.startsWith(prefix); +} + +function issue(code, manifestPath, message) { + return {code, file: manifestPath, message}; +} + +function validateRequiredShape(entry, issues) { + const file = entry.manifest_path; + if (entry.schema_version !== 1) issues.push(issue('invalid-schema-version', file, 'schema_version must be 1')); + if (!CASE_ID_PATTERN.test(String(entry.id ?? ''))) issues.push(issue('invalid-case-id', file, 'id must be kebab-case')); + if (!['real', 'constructed'].includes(entry.kind)) issues.push(issue('invalid-kind', file, 'kind must be real or constructed')); + for (const field of ['title', 'description', 'scene']) { + if (typeof entry[field] !== 'string' || entry[field].trim() === '') { + issues.push(issue('missing-field', file, `${field} must be a non-empty string`)); + } + } + if (!entry.trace || !HASH_PATTERN.test(String(entry.trace.sha256 ?? ''))) { + issues.push(issue('invalid-trace', file, 'trace.sha256 must be a lowercase SHA-256')); + } + if (!entry.analysis || !Array.isArray(entry.analysis.results) || !Array.isArray(entry.analysis.logs)) { + issues.push(issue('invalid-analysis', file, 'analysis.results and analysis.logs must be arrays')); + } + if (!entry.coverage || !Array.isArray(entry.coverage.skills) || !Array.isArray(entry.coverage.strategies) || !Array.isArray(entry.coverage.expectations)) { + issues.push(issue('invalid-coverage', file, 'coverage skills, strategies, and expectations must be arrays')); + } + if (entry.kind === 'constructed' && !entry.construction) { + issues.push(issue('missing-construction', file, 'constructed cases require construction')); + } + const expectedParent = path.basename(path.dirname(entry.case_dir)); + if (entry.kind && expectedParent !== entry.kind) { + issues.push(issue('kind-directory-mismatch', file, `kind ${entry.kind} is stored under ${expectedParent}`)); + } +} + +function validatePathsAndHashes(entry, issues) { + const paths = [ + entry.trace?.file, + ...(entry.analysis?.results ?? []), + ...(entry.analysis?.logs ?? []), + ...(entry.kind === 'constructed' ? [entry.construction?.scenario_file] : []), + ].filter((value) => value !== undefined); + for (const relativePath of paths) { + if (!isSafeCasePath(entry.case_dir, relativePath)) { + issues.push(issue('unsafe-path', entry.manifest_path, `path escapes case directory: ${relativePath}`)); + continue; + } + const absolute = path.resolve(entry.case_dir, relativePath); + if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) { + issues.push(issue('missing-file', entry.manifest_path, `missing case file: ${relativePath}`)); + } + } + if (isSafeCasePath(entry.case_dir, entry.trace?.file)) { + const tracePath = path.resolve(entry.case_dir, entry.trace.file); + if (fs.existsSync(tracePath) && HASH_PATTERN.test(String(entry.trace.sha256 ?? ''))) { + const actual = sha256File(tracePath); + if (actual !== entry.trace.sha256) { + issues.push(issue('hash-mismatch', entry.manifest_path, `trace hash is ${actual}, manifest has ${entry.trace.sha256}`)); + } + } + } +} + +function validatePublication(entry, issues) { + const source = entry.source ?? {}; + if (source.publication === 'private') { + issues.push(issue('tracked-private-case', entry.manifest_path, 'private cases belong under ignored Trace/real/.private')); + } + if (source.publication === 'public') { + for (const field of ['license', 'consent']) { + if (typeof source[field] !== 'string' || source[field].trim() === '') { + issues.push(issue('incomplete-publication-review', entry.manifest_path, `public case requires source.${field}`)); + } + } + for (const field of ['privacy_review', 'sanitization_review']) { + if (!['approved', 'not-applicable'].includes(source[field])) { + issues.push(issue('incomplete-publication-review', entry.manifest_path, `public case requires completed source.${field}`)); + } + } + } +} + +function validateNoLegacyTraceReferences(repoRoot, issues) { + const ignoredSegments = new Set(['.git', '.omo', '.worktrees', 'dist', 'node_modules', 'perfetto', 'Trace']); + const ignoredPrefixes = [ + path.join(repoRoot, 'docs', 'archive'), + path.join(repoRoot, 'docs', 'superpowers'), + ]; + const textExtensions = new Set([ + '.cjs', '.js', '.json', '.md', '.mjs', '.sh', '.ts', '.tsx', '.yaml', '.yml', + ]); + const files = listFilesRecursive(repoRoot, (filePath) => { + const relativeSegments = path.relative(repoRoot, filePath).split(path.sep); + if (relativeSegments.some((segment) => ignoredSegments.has(segment))) return false; + if (ignoredPrefixes.some((prefix) => filePath.startsWith(`${prefix}${path.sep}`))) return false; + return textExtensions.has(path.extname(filePath)); + }); + for (const filePath of files) { + const source = fs.readFileSync(filePath, 'utf8'); + if (/test-traces(?:\/|["'])/.test(source)) { + issues.push(issue( + 'legacy-trace-reference', + filePath, + `maintained source must resolve Trace/catalog.json instead of test-traces: ${path.relative(repoRoot, filePath)}`, + )); + } + } +} + +function validateCatalog(repoRoot) { + const catalog = loadCatalog(repoRoot); + const targets = discoverCoverageTargets(repoRoot); + const issues = []; + const ids = new Map(); + const baseIds = new Set(catalog.cases.filter((entry) => entry.kind === 'real').map((entry) => entry.id)); + const covered = {skills: new Set(), strategies: new Set()}; + const quality = {semantic: new Set(), graceful_empty: new Set(), unavailable: new Set(), definition: new Set()}; + + for (const entry of catalog.cases) { + validateRequiredShape(entry, issues); + validatePathsAndHashes(entry, issues); + validatePublication(entry, issues); + if (ids.has(entry.id)) { + issues.push(issue('duplicate-case-id', entry.manifest_path, `case id also used by ${ids.get(entry.id)}`)); + } else { + ids.set(entry.id, entry.manifest_path); + } + if (entry.kind === 'constructed' && entry.construction && !baseIds.has(entry.construction.base_case_id)) { + issues.push(issue('missing-base-case', entry.manifest_path, `unknown base case ${entry.construction.base_case_id}`)); + } + + const expectationTargets = new Set((entry.coverage?.expectations ?? []).map((item) => `${item.type}:${item.target}`)); + for (const expectation of entry.coverage?.expectations ?? []) { + if (expectation.type !== 'skill') continue; + const mode = expectation.mode; + if (!Object.hasOwn(quality, mode)) { + issues.push(issue('invalid-expectation-mode', entry.manifest_path, `Skill ${expectation.target} has invalid mode ${mode}`)); + continue; + } + quality[mode].add(expectation.target); + if (mode !== 'definition') { + if (!Array.isArray(expectation.required_steps) || expectation.required_steps.length === 0 || !expectation.semantic_step) { + issues.push(issue('incomplete-execution-expectation', entry.manifest_path, `Skill ${expectation.target} requires steps and semantic_step`)); + } + } + if (mode === 'graceful_empty' || mode === 'unavailable') { + if (typeof expectation.limitation_reason !== 'string' || expectation.limitation_reason.trim() === '') { + issues.push(issue('missing-limitation-reason', entry.manifest_path, `Skill ${expectation.target} requires limitation_reason`)); + } + } + if (mode === 'unavailable' && (typeof expectation.expected_error !== 'string' || expectation.expected_error.trim() === '')) { + issues.push(issue('missing-expected-error', entry.manifest_path, `Skill ${expectation.target} requires expected_error`)); + } + } + for (const skill of entry.coverage?.skills ?? []) { + covered.skills.add(skill); + if (!expectationTargets.has(`skill:${skill}`)) { + issues.push(issue('coverage-without-expectation', entry.manifest_path, `Skill ${skill} has no executable expectation`)); + } + } + for (const strategy of entry.coverage?.strategies ?? []) { + covered.strategies.add(strategy); + if (!expectationTargets.has(`strategy:${strategy}`)) { + issues.push(issue('coverage-without-expectation', entry.manifest_path, `Strategy ${strategy} has no executable expectation`)); + } + } + } + validateNoLegacyTraceReferences(repoRoot, issues); + + const coverage = { + missing: { + skills: targets.skills.filter((id) => !covered.skills.has(id)), + strategies: targets.strategies.filter((id) => !covered.strategies.has(id)), + }, + stale: { + skills: [...covered.skills].filter((id) => !targets.skills.includes(id)).sort(), + strategies: [...covered.strategies].filter((id) => !targets.strategies.includes(id)).sort(), + }, + covered: { + skills: [...covered.skills].filter((id) => targets.skills.includes(id)).sort(), + strategies: [...covered.strategies].filter((id) => targets.strategies.includes(id)).sort(), + }, + quality: Object.fromEntries( + Object.entries(quality).map(([mode, ids]) => [mode, [...ids].filter((id) => targets.skills.includes(id)).sort()]), + ), + }; + for (const category of ['skills', 'strategies']) { + for (const id of coverage.missing[category]) { + issues.push(issue('missing-coverage', null, `${category.slice(0, -1)} ${id} has no case`)); + } + for (const id of coverage.stale[category]) { + issues.push(issue('stale-coverage', null, `${category.slice(0, -1)} ${id} no longer exists`)); + } + } + + return {ok: issues.length === 0, issues, coverage, catalog}; +} + +function resolveCaseTrace(repoRoot, selector) { + const catalog = loadCatalog(repoRoot); + const matches = catalog.cases.filter( + (entry) => entry.id === selector || (entry.aliases ?? []).includes(selector), + ); + if (matches.length === 0) throw new Error(`Unknown trace case: ${selector}`); + if (matches.length > 1) throw new Error(`Ambiguous trace case selector: ${selector}`); + const entry = matches[0]; + return path.resolve(entry.case_dir, entry.trace.file); +} + +module.exports = { + discoverCoverageTargets, + loadCatalog, + resolveCaseTrace, + validateCatalog, +}; diff --git a/Trace/tools/lib/generator.cjs b/Trace/tools/lib/generator.cjs new file mode 100644 index 000000000..605a0d853 --- /dev/null +++ b/Trace/tools/lib/generator.cjs @@ -0,0 +1,580 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const fs = require('node:fs'); +const path = require('node:path'); +const {spawnSync} = require('node:child_process'); + +const {collectPacketSequenceIds, encodeTrace} = require('./perfetto-proto.cjs'); +const {sha256Buffer} = require('./hash.cjs'); + +const FIRST_SYNTHETIC_PID = 700000; +const END_STATES = new Map([ + ['R', '0'], + ['S', '1'], + ['D', '2'], +]); + +function decimalString(value, field) { + if (typeof value !== 'string' || !/^[0-9]+$/.test(value)) { + throw new Error(`${field} must be an unsigned decimal string`); + } + return value; +} + +function signedDecimalString(value, field) { + if (typeof value !== 'string' || !/^-?[0-9]+$/.test(value)) { + throw new Error(`${field} must be a signed decimal string`); + } + return value; +} + +function finiteNumber(value, field) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new Error(`${field} must be a finite number`); + } + return value; +} + +function nonNegativeInteger(value, field) { + if (!Number.isInteger(value) || value < 0) { + throw new Error(`${field} must be a non-negative integer`); + } + return value; +} + +function nonEmptyString(value, field) { + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`${field} must be a non-empty string`); + } + return value; +} + +function allocatePid(usedPids) { + let candidate = FIRST_SYNTHETIC_PID; + while (usedPids.has(candidate)) candidate += 1; + usedPids.add(candidate); + return candidate; +} + +function validateScenario(scenario) { + if (!scenario || scenario.schema_version !== 1) { + throw new Error('scenario.schema_version must be 1'); + } + decimalString(scenario.clock?.duration_ns, 'scenario.clock.duration_ns'); + if (!Array.isArray(scenario.actors?.processes) || !Array.isArray(scenario.actors?.threads)) { + throw new Error('scenario.actors.processes and threads must be arrays'); + } + if (!Array.isArray(scenario.signals)) throw new Error('scenario.signals must be an array'); + for (const [index, signal] of scenario.signals.entries()) { + nonEmptyString(signal.type, `scenario.signals[${index}].type`); + decimalString(signal.at_ns, `scenario.signals[${index}].at_ns`); + if (signal.duration_ns !== undefined) { + decimalString(signal.duration_ns, `scenario.signals[${index}].duration_ns`); + } + } +} + +function buildIdentities(scenario, usedPids) { + const processes = {}; + const threads = {}; + const processDefinitions = new Map(); + const threadDefinitions = new Map(); + + for (const actor of scenario.actors.processes) { + const id = nonEmptyString(actor.id, 'process.id'); + if (processDefinitions.has(id)) throw new Error(`duplicate process actor: ${id}`); + const pid = allocatePid(usedPids); + processes[id] = pid; + processDefinitions.set(id, {...actor, pid}); + } + for (const actor of scenario.actors.threads) { + const id = nonEmptyString(actor.id, 'thread.id'); + if (threadDefinitions.has(id)) throw new Error(`duplicate thread actor: ${id}`); + const process = processDefinitions.get(actor.process); + if (!process) throw new Error(`thread ${id} references unknown process ${actor.process}`); + const existingMain = [...threadDefinitions.values()].some( + (thread) => thread.tgid === process.pid && thread.tid === process.pid, + ); + if (actor.is_main && existingMain) throw new Error(`process ${actor.process} has multiple main threads`); + const tid = actor.is_main ? process.pid : allocatePid(usedPids); + threads[id] = tid; + threadDefinitions.set(id, {...actor, tid, tgid: process.pid}); + } + return {processes, threads, processDefinitions, threadDefinitions}; +} + +function absoluteTimestamp(anchor, relative, field) { + return (BigInt(anchor) + BigInt(decimalString(relative, field))).toString(); +} + +function actorForSignal(signal, identities) { + const process = identities.processDefinitions.get(signal.process); + const thread = identities.threadDefinitions.get(signal.thread); + if (!thread) throw new Error(`signal references unknown thread ${signal.thread}`); + const resolvedProcess = process ?? [...identities.processDefinitions.values()].find((item) => item.pid === thread.tgid); + if (!resolvedProcess) throw new Error(`signal references unknown process ${signal.process}`); + if (thread.tgid !== resolvedProcess.pid) { + throw new Error(`thread ${signal.thread} does not belong to process ${signal.process}`); + } + return {process: resolvedProcess, thread}; +} + +function printEvent(timestamp, tid, buf) { + return {timestamp, pid: tid, print: {buf}}; +} + +function schedSwitchEvent(timestamp, prev, next, prevState) { + return { + timestamp, + pid: 0, + schedSwitch: { + prevComm: prev.name, + prevPid: prev.tid, + prevPrio: 120, + prevState, + nextComm: next.name, + nextPid: next.tid, + nextPrio: 120, + }, + }; +} + +function encodeScenarioOverlay(repoRoot, scenario, options) { + validateScenario(scenario); + const anchorNs = decimalString(options?.anchorNs, 'options.anchorNs'); + if (!Number.isInteger(options?.sequenceId) || options.sequenceId <= 0) { + throw new Error('options.sequenceId must be a positive integer'); + } + const usedPids = new Set(options.usedPids ?? []); + const identities = buildIdentities(scenario, usedPids); + const ftraceByCpu = new Map(); + const dataPackets = []; + + function eventsForCpu(cpu) { + if (!Number.isInteger(cpu) || cpu < 0) throw new Error(`invalid cpu: ${cpu}`); + if (!ftraceByCpu.has(cpu)) ftraceByCpu.set(cpu, []); + return ftraceByCpu.get(cpu); + } + + for (const [index, signal] of scenario.signals.entries()) { + const timestamp = absoluteTimestamp(anchorNs, signal.at_ns, `scenario.signals[${index}].at_ns`); + if (signal.type === 'atrace-slice') { + const {process, thread} = actorForSignal(signal, identities); + const end = absoluteTimestamp(timestamp, signal.duration_ns, `scenario.signals[${index}].duration_ns`); + const events = eventsForCpu(signal.cpu ?? 0); + events.push(printEvent(timestamp, thread.tid, `B|${process.pid}|${nonEmptyString(signal.name, 'signal.name')}`)); + events.push(printEvent(end, thread.tid, `E|${process.pid}`)); + } else if (signal.type === 'atrace-counter') { + const {process, thread} = actorForSignal(signal, identities); + if (typeof signal.value !== 'number' || !Number.isFinite(signal.value)) { + throw new Error('atrace-counter value must be a finite number'); + } + eventsForCpu(signal.cpu ?? 0).push( + printEvent(timestamp, thread.tid, `C|${process.pid}|${nonEmptyString(signal.name, 'signal.name')}|${signal.value}`), + ); + } else if (signal.type === 'atrace-async-slice') { + const {process, thread} = actorForSignal(signal, identities); + const cookie = nonNegativeInteger(signal.cookie, 'atrace-async-slice cookie'); + const name = nonEmptyString(signal.name, 'signal.name'); + const end = absoluteTimestamp(timestamp, signal.duration_ns, `scenario.signals[${index}].duration_ns`); + const events = eventsForCpu(signal.cpu ?? 0); + events.push(printEvent(timestamp, thread.tid, `S|${process.pid}|${name}|${cookie}`)); + events.push(printEvent(end, thread.tid, `F|${process.pid}|${name}|${cookie}`)); + } else if (signal.type === 'atrace-async-track-slice') { + const {process, thread} = actorForSignal(signal, identities); + const cookie = nonNegativeInteger(signal.cookie, 'atrace-async-track-slice cookie'); + const trackName = nonEmptyString(signal.track_name, 'signal.track_name'); + const name = nonEmptyString(signal.name, 'signal.name'); + const end = absoluteTimestamp(timestamp, signal.duration_ns, `scenario.signals[${index}].duration_ns`); + const events = eventsForCpu(signal.cpu ?? 0); + events.push(printEvent(timestamp, thread.tid, `G|${process.pid}|${trackName}|${name}|${cookie}`)); + events.push(printEvent(end, thread.tid, `H|${process.pid}|${trackName}|${name}|${cookie}`)); + } else if (signal.type === 'sched-running') { + const thread = identities.threadDefinitions.get(signal.thread); + if (!thread) throw new Error(`signal references unknown thread ${signal.thread}`); + const endState = END_STATES.get(signal.end_state); + if (endState === undefined) throw new Error(`unsupported sched end_state: ${signal.end_state}`); + const end = absoluteTimestamp(timestamp, signal.duration_ns, `scenario.signals[${index}].duration_ns`); + const idle = {tid: 0, name: 'swapper'}; + const task = {tid: thread.tid, name: thread.name}; + const events = eventsForCpu(signal.cpu); + events.push(schedSwitchEvent(timestamp, idle, task, '0')); + events.push(schedSwitchEvent(end, task, idle, endState)); + } else if (signal.type === 'process-stats') { + const process = identities.processDefinitions.get(signal.process); + if (!process) throw new Error(`signal references unknown process ${signal.process}`); + const stats = {pid: process.pid}; + for (const [field, protoField] of [ + ['vm_rss_kb', 'vmRssKb'], + ['rss_anon_kb', 'rssAnonKb'], + ['rss_file_kb', 'rssFileKb'], + ['rss_shmem_kb', 'rssShmemKb'], + ['vm_swap_kb', 'vmSwapKb'], + ['vm_hwm_kb', 'vmHwmKb'], + ]) { + if (signal[field] !== undefined) stats[protoField] = nonNegativeInteger(signal[field], `process-stats ${field}`); + } + if (signal.oom_score_adj !== undefined) { + if (!Number.isInteger(signal.oom_score_adj)) throw new Error('process-stats oom_score_adj must be an integer'); + stats.oomScoreAdj = signal.oom_score_adj; + } + dataPackets.push({timestamp, processStats: {processes: [stats], collectionEndTimestamp: timestamp}}); + } else if (signal.type === 'battery-counters') { + const battery = {}; + if (signal.capacity_percent !== undefined) battery.capacityPercent = finiteNumber(signal.capacity_percent, 'battery capacity_percent'); + for (const [field, protoField] of [ + ['charge_counter_uah', 'chargeCounterUah'], + ['current_ua', 'currentUa'], + ['current_avg_ua', 'currentAvgUa'], + ['energy_counter_uwh', 'energyCounterUwh'], + ['voltage_uv', 'voltageUv'], + ]) { + if (signal[field] !== undefined) battery[protoField] = signedDecimalString(signal[field], `battery ${field}`); + } + dataPackets.push({timestamp, battery}); + } else if (signal.type === 'power-rail') { + const duration = decimalString(signal.duration_ns, `scenario.signals[${index}].duration_ns`); + const end = absoluteTimestamp(timestamp, duration, `scenario.signals[${index}].duration_ns`); + const railIndex = index + 1; + dataPackets.push({ + timestamp, + powerRails: { + sessionUuid: String(options.sequenceId), + railDescriptor: [{ + index: railIndex, + railName: nonEmptyString(signal.name, 'power-rail name'), + subsysName: nonEmptyString(signal.subsystem, 'power-rail subsystem'), + samplingRate: 1000, + }], + energyData: [ + {index: railIndex, timestampMs: (BigInt(timestamp) / 1000000n).toString(), energy: decimalString(signal.start_energy_uws, 'power-rail start_energy_uws')}, + {index: railIndex, timestampMs: (BigInt(end) / 1000000n).toString(), energy: decimalString(signal.end_energy_uws, 'power-rail end_energy_uws')}, + ], + }, + }); + } else if (signal.type === 'gpu-work-period') { + const end = absoluteTimestamp(timestamp, signal.duration_ns, `scenario.signals[${index}].duration_ns`); + eventsForCpu(signal.cpu ?? 0).push({ + timestamp, + pid: 0, + gpuWorkPeriod: { + gpuId: nonNegativeInteger(signal.gpu_id, 'gpu-work-period gpu_id'), + uid: nonNegativeInteger(signal.uid, 'gpu-work-period uid'), + startTimeNs: timestamp, + endTimeNs: end, + totalActiveDurationNs: decimalString(signal.active_duration_ns, 'gpu-work-period active_duration_ns'), + }, + }); + } else if (signal.type === 'gpu-frequency') { + eventsForCpu(signal.cpu ?? 0).push({ + timestamp, + pid: 0, + gpuFrequency: { + gpuId: nonNegativeInteger(signal.gpu_id, 'gpu-frequency gpu_id'), + state: nonNegativeInteger(signal.value, 'gpu-frequency value'), + }, + }); + } else if (signal.type === 'cpu-frequency') { + eventsForCpu(signal.cpu ?? signal.cpu_id).push({ + timestamp, + pid: 0, + cpuFrequency: { + cpuId: nonNegativeInteger(signal.cpu_id, 'cpu-frequency cpu_id'), + state: nonNegativeInteger(signal.value, 'cpu-frequency value'), + }, + }); + } else if (signal.type === 'irq-span') { + const end = absoluteTimestamp(timestamp, signal.duration_ns, `scenario.signals[${index}].duration_ns`); + const irq = nonNegativeInteger(signal.irq, 'irq-span irq'); + const events = eventsForCpu(signal.cpu ?? 0); + events.push({timestamp, pid: 0, irqHandlerEntry: {irq, name: nonEmptyString(signal.name, 'irq-span name')}}); + events.push({timestamp: end, pid: 0, irqHandlerExit: {irq, ret: 1}}); + } else if (signal.type === 'frame-timeline') { + const process = identities.processDefinitions.get(signal.process); + if (!process) throw new Error(`signal references unknown process ${signal.process}`); + const end = absoluteTimestamp(timestamp, signal.duration_ns, `scenario.signals[${index}].duration_ns`); + const cookie = nonNegativeInteger(signal.cookie, 'frame-timeline cookie'); + dataPackets.push({ + timestamp, + frameTimelineEvent: { + actualSurfaceFrameStart: { + cookie, + token: nonNegativeInteger(signal.token, 'frame-timeline token'), + displayFrameToken: nonNegativeInteger(signal.display_frame_token, 'frame-timeline display_frame_token'), + pid: process.pid, + layerName: nonEmptyString(signal.layer_name, 'frame-timeline layer_name'), + presentType: signal.jank_type ? 2 : 1, + onTimeFinish: !signal.jank_type, + gpuComposition: false, + jankType: nonNegativeInteger(signal.jank_type ?? 1, 'frame-timeline jank_type'), + predictionType: 1, + jankSeverityType: signal.jank_type ? 3 : 1, + }, + }, + }); + dataPackets.push({timestamp: end, frameTimelineEvent: {frameEnd: {cookie}}}); + } else if (signal.type === 'gpu-power-state') { + eventsForCpu(signal.cpu ?? 0).push({ + timestamp, + pid: 0, + maliGpuPowerState: { + changeNs: timestamp, + fromState: nonNegativeInteger(signal.old_state, 'gpu-power-state old_state'), + toState: nonNegativeInteger(signal.new_state, 'gpu-power-state new_state'), + }, + }); + } else if (signal.type === 'cpu-idle') { + eventsForCpu(signal.cpu ?? signal.cpu_id).push({ + timestamp, + pid: 0, + cpuIdle: { + cpuId: nonNegativeInteger(signal.cpu_id, 'cpu-idle cpu_id'), + state: nonNegativeInteger(signal.state, 'cpu-idle state'), + }, + }); + } else if (signal.type === 'lmk-kill') { + const {process, thread} = actorForSignal(signal, identities); + const end = absoluteTimestamp(timestamp, signal.duration_ns, `scenario.signals[${index}].duration_ns`); + const killReason = nonNegativeInteger(signal.kill_reason, 'lmk-kill kill_reason'); + if (!Number.isInteger(signal.oom_score_adj)) throw new Error('lmk-kill oom_score_adj must be an integer'); + const events = eventsForCpu(signal.cpu ?? 0); + events.push(printEvent(timestamp, thread.tid, `B|${process.pid}|lmk,${process.pid},${killReason},${signal.oom_score_adj}`)); + events.push(printEvent(end, thread.tid, `E|${process.pid}`)); + } else { + throw new Error(`unsupported signal type: ${signal.type}`); + } + } + + const processTree = { + processes: [...identities.processDefinitions.values()].map((actor) => ({ + pid: actor.pid, + ppid: 1, + cmdline: [actor.name], + uid: actor.uid ?? 10999, + })), + threads: [...identities.threadDefinitions.values()].map((actor) => ({ + tid: actor.tid, + tgid: actor.tgid, + name: actor.name, + })), + collectionEndTimestamp: anchorNs, + }; + const packets = [ + { + trustedPacketSequenceId: options.sequenceId, + clockSnapshot: { + clocks: [ + {clockId: 5, timestamp: anchorNs}, + {clockId: 6, timestamp: anchorNs}, + {clockId: 11, timestamp: anchorNs}, + ], + primaryTraceClock: 6, + }, + }, + { + timestamp: anchorNs, + trustedPacketSequenceId: options.sequenceId, + incrementalStateCleared: true, + processTree, + }, + ...dataPackets.map((packet) => ({ + ...packet, + trustedPacketSequenceId: options.sequenceId, + })), + ...[...ftraceByCpu.entries()] + .sort(([left], [right]) => left - right) + .map(([cpu, event]) => ({ + timestamp: anchorNs, + trustedPacketSequenceId: options.sequenceId, + ftraceEvents: {cpu, event}, + })), + ]; + const buffer = encodeTrace(repoRoot, packets); + return { + buffer, + identities: {processes: identities.processes, threads: identities.threads}, + provenance: { + anchor_ns: anchorNs, + sequence_id: options.sequenceId, + overlay_sha256: sha256Buffer(buffer), + }, + }; +} + +function materializeTrace(base, overlay, outputPath) { + if (!Buffer.isBuffer(base) || !Buffer.isBuffer(overlay)) { + throw new Error('base and overlay must be Buffers'); + } + const output = Buffer.concat([base, overlay]); + fs.mkdirSync(path.dirname(outputPath), {recursive: true}); + fs.writeFileSync(outputPath, output); + return { + base_bytes: base.length, + overlay_bytes: overlay.length, + output_bytes: output.length, + base_sha256: sha256Buffer(base), + overlay_sha256: sha256Buffer(overlay), + output_sha256: sha256Buffer(output), + }; +} + +function resolveTraceProcessor(repoRoot) { + const key = `${process.platform}-${process.arch}`; + const relative = { + 'darwin-arm64': 'backend/prebuilts/trace_processor/darwin-arm64/trace_processor_shell', + 'linux-x64': 'backend/prebuilts/trace_processor/linux-x64/trace_processor_shell', + 'win32-x64': 'backend/prebuilts/trace_processor/win32-x64/trace_processor_shell.exe', + }[key]; + if (!relative) throw new Error(`No checked-in trace processor for ${key}`); + const executable = path.join(repoRoot, relative); + if (!fs.existsSync(executable)) throw new Error(`Missing trace processor: ${executable}`); + return executable; +} + +function parseProbeCsv(output) { + const lines = output.trim().split(/\r?\n/).filter(Boolean); + if (lines.length < 2) throw new Error(`Trace probe returned no rows: ${output}`); + const unquote = (value) => value.replace(/^"|"$/g, '').replace(/""/g, '"'); + return lines.slice(1).map((line) => line.split(',').map(unquote)); +} + +function probeTrace(repoRoot, tracePath) { + if (!fs.existsSync(tracePath)) throw new Error(`Missing trace: ${tracePath}`); + const sql = ` + SELECT 'bounds' AS kind, printf('%d', trace_start()) AS value_1, + printf('%d', trace_end()) AS value_2 + UNION ALL + SELECT 'pid', CAST(pid AS TEXT), '' FROM process WHERE pid IS NOT NULL + UNION ALL + SELECT 'cpu', CAST(cpu AS TEXT), '' FROM (SELECT DISTINCT cpu FROM sched) + ORDER BY kind, value_1 + `; + const result = spawnSync(resolveTraceProcessor(repoRoot), ['-Q', sql, tracePath], { + cwd: repoRoot, + encoding: 'utf8', + maxBuffer: 16 * 1024 * 1024, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`trace_processor_shell probe failed (${result.status}): ${result.stderr}`); + } + const rows = parseProbeCsv(result.stdout); + const bounds = rows.find(([kind]) => kind === 'bounds'); + if (!bounds) throw new Error('Trace probe did not return bounds'); + decimalString(bounds[1], 'trace start'); + decimalString(bounds[2], 'trace end'); + return { + start_ns: bounds[1], + end_ns: bounds[2], + used_pids: new Set( + rows + .filter(([kind]) => kind === 'pid') + .map(([, pid]) => Number.parseInt(pid, 10)) + .filter(Number.isInteger), + ), + used_cpus: new Set( + rows + .filter(([kind]) => kind === 'cpu') + .map(([, cpu]) => Number.parseInt(cpu, 10)) + .filter(Number.isInteger), + ), + }; +} + +function chooseAnchor(bounds, clock) { + const start = BigInt(bounds.start_ns); + const end = BigInt(bounds.end_ns); + const duration = BigInt(decimalString(clock.duration_ns, 'scenario.clock.duration_ns')); + const margin = 1000000n; + const first = start + margin; + const last = end - margin - duration; + if (last < first) { + throw new Error(`Scenario duration ${duration}ns does not fit trace bounds ${start}-${end}`); + } + if (clock.anchor === 'trace-start') return first.toString(); + if (clock.anchor === 'trace-middle') return (first + (last - first) / 2n).toString(); + if (clock.anchor === 'trace-end') return last.toString(); + throw new Error(`Unsupported scenario clock anchor: ${clock.anchor}`); +} + +function allocateSequenceId(caseId, usedSequenceIds) { + const hash = sha256Buffer(Buffer.from(caseId)); + let candidate = (0x70000000 | (Number.parseInt(hash.slice(0, 8), 16) & 0x0fffffff)) >>> 0; + while (usedSequenceIds.has(candidate) || candidate === 0) candidate = (candidate + 1) >>> 0; + return candidate; +} + +function isolateScenarioCpus(scenario, usedCpus) { + const requested = [...new Set( + scenario.signals + .map((signal) => signal.cpu) + .filter((cpu) => Number.isInteger(cpu)), + )].sort((left, right) => left - right); + let nextCpu = usedCpus.size > 0 ? Math.max(...usedCpus) + 1 : 0; + const cpuMap = {}; + for (const cpu of requested) { + while (usedCpus.has(nextCpu)) nextCpu += 1; + cpuMap[cpu] = nextCpu; + usedCpus.add(nextCpu); + nextCpu += 1; + } + return { + cpuMap, + scenario: { + ...scenario, + signals: scenario.signals.map((signal) => ({ + ...signal, + ...(Number.isInteger(signal.cpu) ? {cpu: cpuMap[signal.cpu]} : {}), + })), + }, + }; +} + +function buildConstructedTrace(repoRoot, options) { + const base = fs.readFileSync(options.basePath); + const scenario = JSON.parse(fs.readFileSync(options.scenarioPath, 'utf8')); + validateScenario(scenario); + const probe = probeTrace(repoRoot, options.basePath); + const anchorNs = chooseAnchor(probe, scenario.clock); + const usedSequenceIds = collectPacketSequenceIds(repoRoot, base); + const sequenceId = allocateSequenceId(options.caseId, usedSequenceIds); + const isolated = isolateScenarioCpus(scenario, new Set(probe.used_cpus)); + const overlay = encodeScenarioOverlay(repoRoot, isolated.scenario, { + anchorNs, + usedPids: probe.used_pids, + sequenceId, + }); + fs.mkdirSync(path.dirname(options.overlayPath), {recursive: true}); + fs.writeFileSync(options.overlayPath, overlay.buffer); + const materialization = materializeTrace(base, overlay.buffer, options.outputPath); + const outputProbe = probeTrace(repoRoot, options.outputPath); + return { + overlay, + output_probe: { + start_ns: outputProbe.start_ns, + end_ns: outputProbe.end_ns, + used_pid_count: outputProbe.used_pids.size, + }, + provenance: { + case_id: options.caseId, + anchor_ns: anchorNs, + sequence_id: sequenceId, + cpu_map: isolated.cpuMap, + base_sha256: materialization.base_sha256, + overlay_sha256: materialization.overlay_sha256, + output_sha256: materialization.output_sha256, + base_bytes: materialization.base_bytes, + overlay_bytes: materialization.overlay_bytes, + output_bytes: materialization.output_bytes, + }, + }; +} + +module.exports = { + buildConstructedTrace, + encodeScenarioOverlay, + materializeTrace, + probeTrace, + resolveTraceProcessor, +}; diff --git a/Trace/tools/lib/hash.cjs b/Trace/tools/lib/hash.cjs new file mode 100644 index 000000000..0dd7c5348 --- /dev/null +++ b/Trace/tools/lib/hash.cjs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const crypto = require('node:crypto'); +const fs = require('node:fs'); + +function sha256Buffer(buffer) { + return crypto.createHash('sha256').update(buffer).digest('hex'); +} + +function sha256File(filePath) { + return sha256Buffer(fs.readFileSync(filePath)); +} + +module.exports = {sha256Buffer, sha256File}; diff --git a/Trace/tools/lib/import-real.cjs b/Trace/tools/lib/import-real.cjs new file mode 100644 index 000000000..ede68cd49 --- /dev/null +++ b/Trace/tools/lib/import-real.cjs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const fs = require('node:fs'); +const path = require('node:path'); + +const {probeTrace: defaultProbeTrace} = require('./generator.cjs'); +const {sha256File} = require('./hash.cjs'); +const {validateCatalog: defaultValidateCatalog} = require('./catalog.cjs'); +const {writeIndexes: defaultWriteIndexes} = require('./indexer.cjs'); + +const CASE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + +function requireFile(filePath, label) { + if (!filePath || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + throw new Error(`${label} does not exist: ${filePath}`); + } +} + +function copyEvidence(paths, stagingDir, category) { + const targetDir = path.join(stagingDir, 'analysis', category); + const seen = new Set(); + return paths.map((sourcePath) => { + requireFile(sourcePath, `${category} file`); + const name = path.basename(sourcePath); + if (seen.has(name)) throw new Error(`duplicate ${category} filename: ${name}`); + seen.add(name); + fs.mkdirSync(targetDir, {recursive: true}); + fs.copyFileSync(sourcePath, path.join(targetDir, name), fs.constants.COPYFILE_EXCL); + return `analysis/${category}/${name}`; + }); +} + +function importRealCase(repoRoot, options) { + if (!CASE_ID_PATTERN.test(String(options.id ?? ''))) { + throw new Error('case id must be lowercase kebab-case'); + } + for (const field of ['title', 'description', 'scene', 'origin']) { + if (typeof options[field] !== 'string' || options[field].trim() === '') { + throw new Error(`${field} must be a non-empty string`); + } + } + requireFile(options.tracePath, 'trace'); + const privateRoot = path.join(repoRoot, 'Trace', 'real', '.private'); + const finalDir = path.join(privateRoot, options.id); + if (fs.existsSync(finalDir)) throw new Error(`case already exists: ${options.id}`); + fs.mkdirSync(privateRoot, {recursive: true}); + const stagingDir = fs.mkdtempSync(path.join(privateRoot, `.${options.id}.tmp-`)); + + try { + const tracePath = path.join(stagingDir, 'trace.pftrace'); + fs.copyFileSync(options.tracePath, tracePath, fs.constants.COPYFILE_EXCL); + const probe = (options.probeTrace ?? defaultProbeTrace)(repoRoot, tracePath); + const results = copyEvidence(options.resultPaths ?? [], stagingDir, 'results'); + const logs = copyEvidence(options.logPaths ?? [], stagingDir, 'logs'); + const apiLevel = options.android?.api_level ?? null; + const manifest = { + schema_version: 1, + id: options.id, + kind: 'real', + title: options.title, + description: options.description, + scene: options.scene, + tags: [...new Set(options.tags ?? [options.scene])].sort(), + aliases: [], + trace: { + file: 'trace.pftrace', + format: 'perfetto-protobuf', + sha256: sha256File(tracePath), + materialization: 'committed', + }, + android: { + release: options.android?.release ?? null, + api_level: apiLevel, + device: options.android?.device ?? null, + build_fingerprint: options.android?.build_fingerprint ?? null, + compatibility: {min_api: apiLevel, max_api: apiLevel}, + }, + source: { + origin: options.origin, + captured_at: options.capturedAt ?? null, + imported_at: options.now ?? new Date().toISOString(), + license: null, + consent: null, + privacy_review: 'pending', + sanitization_review: 'pending', + publication: 'private', + }, + analysis: {results, logs}, + trace_probe: { + start_ns: probe.start_ns, + end_ns: probe.end_ns, + process_count_hint: probe.used_pids.size, + }, + coverage: {skills: [], strategies: [], expectations: []}, + }; + fs.writeFileSync(path.join(stagingDir, 'case.json'), `${JSON.stringify(manifest, null, 2)}\n`); + fs.renameSync(stagingDir, finalDir); + return {caseDir: finalDir, manifest}; + } catch (error) { + fs.rmSync(stagingDir, {recursive: true, force: true}); + throw error; + } +} + +function promoteRealCase(repoRoot, options) { + if (!CASE_ID_PATTERN.test(String(options.id ?? ''))) { + throw new Error('case id must be lowercase kebab-case'); + } + for (const field of ['license', 'consent']) { + if (typeof options[field] !== 'string' || options[field].trim() === '') { + throw new Error(`${field} must be a non-empty string`); + } + } + for (const [field, value] of [ + ['privacyReview', options.privacyReview], + ['sanitizationReview', options.sanitizationReview], + ]) { + if (!['approved', 'not-applicable'].includes(value)) { + throw new Error(`${field} must be approved or not-applicable`); + } + } + + const privateDir = path.join(repoRoot, 'Trace', 'real', '.private', options.id); + const publicDir = path.join(repoRoot, 'Trace', 'real', options.id); + const manifestPath = path.join(privateDir, 'case.json'); + requireFile(manifestPath, 'private case manifest'); + if (fs.existsSync(publicDir)) throw new Error(`public case already exists: ${options.id}`); + + const originalManifestText = fs.readFileSync(manifestPath, 'utf8'); + const manifest = JSON.parse(originalManifestText); + if (manifest.id !== options.id || manifest.kind !== 'real' || manifest.source?.publication !== 'private') { + throw new Error(`case ${options.id} is not a private real-case draft`); + } + manifest.source = { + ...manifest.source, + license: options.license, + consent: options.consent, + privacy_review: options.privacyReview, + sanitization_review: options.sanitizationReview, + publication: 'public', + }; + + const validateCatalog = options.validateCatalog ?? defaultValidateCatalog; + const writeIndexes = options.writeIndexes ?? defaultWriteIndexes; + fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + fs.renameSync(privateDir, publicDir); + try { + const validation = validateCatalog(repoRoot); + if (!validation.ok) { + throw new Error(validation.issues.map((item) => item.message).join('; ') || 'catalog validation failed'); + } + writeIndexes(repoRoot); + return {caseDir: publicDir, manifest}; + } catch (error) { + fs.renameSync(publicDir, privateDir); + fs.writeFileSync(path.join(privateDir, 'case.json'), originalManifestText); + throw error; + } +} + +module.exports = {importRealCase, promoteRealCase}; diff --git a/Trace/tools/lib/indexer.cjs b/Trace/tools/lib/indexer.cjs new file mode 100644 index 000000000..9b2847b51 --- /dev/null +++ b/Trace/tools/lib/indexer.cjs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const fs = require('node:fs'); +const path = require('node:path'); + +const {loadCatalog, validateCatalog} = require('./catalog.cjs'); + +const GENERATED_BANNER = ''; + +function relativeCase(repoRoot, entry) { + const clone = JSON.parse(JSON.stringify(entry)); + clone.case_dir = path.relative(repoRoot, entry.case_dir).split(path.sep).join('/'); + clone.manifest_path = path.relative(repoRoot, entry.manifest_path).split(path.sep).join('/'); + return clone; +} + +function androidLabel(entry) { + const release = entry.android?.release ? `Android ${entry.android.release}` : 'Android unknown'; + const api = entry.android?.api_level ? `API ${entry.android.api_level}` : 'API unknown'; + const range = entry.android?.compatibility; + if (range?.min_api && range?.max_api && range.min_api !== range.max_api) { + return `${release} / ${api} (tested API ${range.min_api}-${range.max_api})`; + } + return `${release} / ${api}`; +} + +function markdownTable(cases, kind, directoryPrefix = '') { + if (cases.length === 0) return '_No cases yet._'; + const header = kind === 'real' + ? '| Case | Scene | Android | Publication | Analysis |\n| --- | --- | --- | --- | --- |' + : '| Case | Scene | Android | Base | Coverage |\n| --- | --- | --- | --- | --- | --- |'; + const rows = cases.map((entry) => { + const link = `./${directoryPrefix}${entry.id}/`; + if (kind === 'real') { + const analysisCount = (entry.analysis?.results?.length ?? 0) + (entry.analysis?.logs?.length ?? 0); + return `| [${entry.title}](${link}) | ${entry.scene} | ${androidLabel(entry)} | ${entry.source?.publication ?? 'unknown'} | ${analysisCount} file(s) |`; + } + const coverageCount = (entry.coverage?.skills?.length ?? 0) + (entry.coverage?.strategies?.length ?? 0); + return `| [${entry.title}](${link}) | ${entry.scene} | ${androidLabel(entry)} | ${entry.construction?.base_case_id ?? '-'} | ${coverageCount} target(s) |`; + }); + return [header, ...rows].join('\n'); +} + +function renderRootReadme(catalog) { + const real = catalog.cases.filter((entry) => entry.kind === 'real'); + const constructed = catalog.cases.filter((entry) => entry.kind === 'constructed'); + const modes = constructed + .flatMap((entry) => entry.coverage?.expectations ?? []) + .filter((expectation) => expectation.type === 'skill') + .reduce((counts, expectation) => { + counts[expectation.mode] = (counts[expectation.mode] ?? 0) + 1; + return counts; + }, {}); + return `${GENERATED_BANNER} +# SmartPerfetto Trace Corpus + +This directory is the source-controlled trace test and reference corpus. + +- [Real trace cases](./real/README.md): ${real.length} +- [Constructed trace cases](./constructed/README.md): ${constructed.length} +- [Machine-readable catalog](./catalog.json) +- [Skill and Strategy coverage](./coverage.json) + +Skill execution quality: ${modes.semantic ?? 0} semantic, ${modes.graceful_empty ?? 0} explicit graceful-empty, ${modes.unavailable ?? 0} explicit unavailable prerequisite, ${modes.definition ?? 0} definition-only. + +## Commands + +\`npm run trace:validate\` checks manifests, hashes, generated indexes, publication gates, legacy path coupling, and exact current Skill/Strategy inventory coverage. + +\`npm run trace:build\` deterministically materializes every base-plus-overlay case under ignored \`Trace/.generated/\` and reparses it with the pinned trace processor. + +\`npm run trace:regression\` validates, builds, and executes the complete deterministic corpus. Per-case evidence is written below \`Trace/.generated/constructed//\`. + +## Add a real case + +Stage a capture privately first; repeat \`--result\`, \`--log\`, and \`--tag\` as needed: + +\`node Trace/tools/trace-corpus.cjs import-real --id typical-startup --title "Typical startup" --description "Representative cold start" --scene startup --trace /path/to/trace.pftrace --origin "maintainer capture" --result /path/to/result.json --log /path/to/session.log --android-release 16 --api-level 36 --device "Pixel"\` + +The command copies evidence atomically into ignored \`Trace/real/.private//\`. Review the trace and manifest before publication. Promotion requires explicit approvals and rolls back if catalog validation fails: + +\`node Trace/tools/trace-corpus.cjs promote-real typical-startup --license CC-BY-4.0 --consent owner-approved --privacy-review approved --sanitization-review approved\` + +## Extend constructed cases + +Constructed cases keep source scenarios and overlay protobufs in Git; combined traces are generated. Copy [the scenario template](./constructed/_templates/scenario.example.json), declare Android compatibility in \`case.json\`, assign each target explicitly, then run \`npm run trace:regression\`. Adding a Skill or Strategy without a current expectation fails validation. Use separate case ids with non-overlapping API ranges when Android behavior differs. + +## Real cases + +${markdownTable(real, 'real', 'real/')} + +## Constructed cases + +${markdownTable(constructed, 'constructed', 'constructed/')} +`; +} + +function renderKindReadme(catalog, kind) { + const cases = catalog.cases.filter((entry) => entry.kind === kind); + const title = kind === 'real' ? 'Real Trace Cases' : 'Constructed Trace Cases'; + const note = kind === 'real' + ? 'New captures are first imported under ignored `.private/` staging and require explicit review before promotion.' + : 'Run `npm run trace:build` to materialize base-plus-overlay traces under `Trace/.generated/`.'; + return `${GENERATED_BANNER} +# ${title} + +${note} + +${markdownTable(cases, kind)} +`; +} + +function generatedFiles(repoRoot) { + const catalog = loadCatalog(repoRoot); + const validation = validateCatalog(repoRoot); + const serializableCatalog = { + schema_version: 1, + cases: catalog.cases.map((entry) => relativeCase(repoRoot, entry)), + }; + return new Map([ + [path.join(repoRoot, 'Trace/README.md'), renderRootReadme(catalog)], + [path.join(repoRoot, 'Trace/real/README.md'), renderKindReadme(catalog, 'real')], + [path.join(repoRoot, 'Trace/constructed/README.md'), renderKindReadme(catalog, 'constructed')], + [path.join(repoRoot, 'Trace/catalog.json'), `${JSON.stringify(serializableCatalog, null, 2)}\n`], + [path.join(repoRoot, 'Trace/coverage.json'), `${JSON.stringify({schema_version: 1, ...validation.coverage}, null, 2)}\n`], + ]); +} + +function writeIndexes(repoRoot, options = {}) { + const files = generatedFiles(repoRoot); + const changed = []; + for (const [filePath, content] of files) { + const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null; + if (current === content) continue; + if (options.check) { + throw new Error(`stale generated file: ${path.relative(repoRoot, filePath)}`); + } + fs.mkdirSync(path.dirname(filePath), {recursive: true}); + fs.writeFileSync(filePath, content); + changed.push(path.relative(repoRoot, filePath).split(path.sep).join('/')); + } + return {changed}; +} + +module.exports = {generatedFiles, writeIndexes}; diff --git a/Trace/tools/lib/perfetto-proto.cjs b/Trace/tools/lib/perfetto-proto.cjs new file mode 100644 index 000000000..e5f789df3 --- /dev/null +++ b/Trace/tools/lib/perfetto-proto.cjs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const path = require('node:path'); + +const cache = new Map(); + +function loadTraceType(repoRoot) { + const normalizedRoot = path.resolve(repoRoot); + if (cache.has(normalizedRoot)) return cache.get(normalizedRoot); + + const protobufPath = require.resolve('protobufjs', { + paths: [path.join(normalizedRoot, 'backend')], + }); + const protobuf = require(protobufPath); + const perfettoRoot = path.join(normalizedRoot, 'perfetto'); + const root = new protobuf.Root(); + root.resolvePath = (origin, target) => { + if (target.startsWith('protos/')) return path.join(perfettoRoot, target); + return protobuf.util.path.resolve(origin, target); + }; + root.loadSync(path.join(perfettoRoot, 'protos/perfetto/trace/trace.proto')); + root.resolveAll(); + const traceType = root.lookupType('perfetto.protos.Trace'); + cache.set(normalizedRoot, traceType); + return traceType; +} + +function encodeTrace(repoRoot, packets) { + const traceType = loadTraceType(repoRoot); + const message = traceType.fromObject({packet: packets}); + const validationError = traceType.verify(message); + if (validationError) throw new Error(`Invalid Perfetto Trace protobuf: ${validationError}`); + return Buffer.from(traceType.encode(message).finish()); +} + +function collectPacketSequenceIds(repoRoot, traceBuffer) { + const traceType = loadTraceType(repoRoot); + const trace = traceType.decode(traceBuffer); + return new Set( + trace.packet + .map((packet) => packet.trustedPacketSequenceId) + .filter((value) => Number.isInteger(value) && value > 0), + ); +} + +module.exports = {collectPacketSequenceIds, encodeTrace, loadTraceType}; diff --git a/Trace/tools/trace-corpus.cjs b/Trace/tools/trace-corpus.cjs new file mode 100644 index 000000000..00d3677e4 --- /dev/null +++ b/Trace/tools/trace-corpus.cjs @@ -0,0 +1,195 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +const path = require('node:path'); + +const { + discoverCoverageTargets, + resolveCaseTrace, + validateCatalog, +} = require('./lib/catalog.cjs'); +const {writeIndexes} = require('./lib/indexer.cjs'); +const {buildCatalogCases} = require('./lib/builder.cjs'); +const {importRealCase, promoteRealCase} = require('./lib/import-real.cjs'); + +function parseArgs(argv) { + const args = [...argv]; + let repoRoot = path.resolve(__dirname, '../..'); + const repoIndex = args.indexOf('--repo'); + if (repoIndex !== -1) { + const value = args[repoIndex + 1]; + if (!value) throw new Error('--repo requires a path'); + repoRoot = path.resolve(value); + args.splice(repoIndex, 2); + } + const command = args.shift(); + const positional = []; + const flags = new Set(); + const values = new Map(); + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (!token.startsWith('--')) { + positional.push(token); + continue; + } + flags.add(token); + const next = args[index + 1]; + if (next !== undefined && !next.startsWith('--')) { + if (!values.has(token)) values.set(token, []); + values.get(token).push(next); + index += 1; + } + } + return { + command, + positional, + flags, + value: (name) => values.get(name)?.at(-1), + values: (name) => values.get(name) ?? [], + repoRoot, + }; +} + +function requiredValue(parsed, name) { + const value = parsed.value(name); + if (!value) throw new Error(`${name} requires a value`); + return value; +} + +function printIssues(validation) { + for (const item of validation.issues) { + const file = item.file ? ` (${item.file})` : ''; + console.error(`[${item.code}] ${item.message}${file}`); + } +} + +function usage() { + console.log(`Usage: node Trace/tools/trace-corpus.cjs [options] + +Commands: + index [--check] Generate or check README/catalog indexes + validate [--check-generated] Validate manifests, hashes, and coverage + coverage Print exact Skill and Strategy coverage + build [--check] [--case ] Materialize constructed trace cases + resolve Print the committed trace path + import-real [options] Stage a captured trace under ignored .private/ + promote-real [options] Publish a reviewed private draft atomically + +Import options: + --id --title --description --scene --trace --origin + [--result ]... [--log ]... [--tag ]... + [--android-release ] [--api-level ] [--device ] + [--build-fingerprint ] [--captured-at ] + +Promotion options: + --license --consent + --privacy-review + --sanitization-review + +Global options: + --repo Override repository root for tooling/tests`); +} + +function main(argv) { + const parsed = parseArgs(argv); + if (parsed.command === 'index') { + const result = writeIndexes(parsed.repoRoot, {check: parsed.flags.has('--check')}); + console.log(parsed.flags.has('--check') + ? 'PASS generated indexes are current' + : `generated ${result.changed.length} file(s)`); + return 0; + } + if (parsed.command === 'validate') { + if (parsed.flags.has('--check-generated')) writeIndexes(parsed.repoRoot, {check: true}); + const validation = validateCatalog(parsed.repoRoot); + if (!validation.ok) { + printIssues(validation); + return 1; + } + console.log(`PASS ${validation.catalog.cases.length} trace case(s) validated`); + return 0; + } + if (parsed.command === 'coverage') { + const targets = discoverCoverageTargets(parsed.repoRoot); + const validation = validateCatalog(parsed.repoRoot); + console.log(`Skills: ${validation.coverage.covered.skills.length}/${targets.skills.length}`); + console.log(`Strategies: ${validation.coverage.covered.strategies.length}/${targets.strategies.length}`); + console.log(`Skill quality: semantic=${validation.coverage.quality.semantic.length}, graceful-empty=${validation.coverage.quality.graceful_empty.length}, unavailable=${validation.coverage.quality.unavailable.length}, definition=${validation.coverage.quality.definition.length}`); + if (!validation.ok) { + printIssues(validation); + return 1; + } + return 0; + } + if (parsed.command === 'build') { + const caseId = parsed.value('--case'); + const caseIds = caseId ? [caseId] : undefined; + if (parsed.flags.has('--case') && !caseId) throw new Error('--case requires a case id'); + const result = buildCatalogCases(parsed.repoRoot, { + caseIds, + check: parsed.flags.has('--check'), + }); + console.log(`built ${result.length} constructed case(s)`); + return 0; + } + if (parsed.command === 'resolve') { + const selector = parsed.positional[0]; + if (!selector) throw new Error('resolve requires a case id or alias'); + console.log(resolveCaseTrace(parsed.repoRoot, selector)); + return 0; + } + if (parsed.command === 'import-real') { + const apiLevelValue = parsed.value('--api-level'); + const apiLevel = apiLevelValue === undefined ? null : Number.parseInt(apiLevelValue, 10); + if (apiLevelValue !== undefined && (!Number.isInteger(apiLevel) || apiLevel < 1)) { + throw new Error('--api-level must be a positive integer'); + } + const imported = importRealCase(parsed.repoRoot, { + id: requiredValue(parsed, '--id'), + title: requiredValue(parsed, '--title'), + description: requiredValue(parsed, '--description'), + scene: requiredValue(parsed, '--scene'), + tracePath: path.resolve(requiredValue(parsed, '--trace')), + origin: requiredValue(parsed, '--origin'), + resultPaths: parsed.values('--result').map((file) => path.resolve(file)), + logPaths: parsed.values('--log').map((file) => path.resolve(file)), + tags: parsed.values('--tag').length > 0 ? parsed.values('--tag') : undefined, + capturedAt: parsed.value('--captured-at') ?? null, + android: { + release: parsed.value('--android-release') ?? null, + api_level: apiLevel, + device: parsed.value('--device') ?? null, + build_fingerprint: parsed.value('--build-fingerprint') ?? null, + }, + }); + console.log(`staged private real case: ${imported.caseDir}`); + return 0; + } + if (parsed.command === 'promote-real') { + const id = parsed.positional[0] ?? parsed.value('--id'); + if (!id) throw new Error('promote-real requires a case id'); + const promoted = promoteRealCase(parsed.repoRoot, { + id, + license: requiredValue(parsed, '--license'), + consent: requiredValue(parsed, '--consent'), + privacyReview: requiredValue(parsed, '--privacy-review'), + sanitizationReview: requiredValue(parsed, '--sanitization-review'), + }); + console.log(`promoted public real case: ${promoted.caseDir}`); + return 0; + } + usage(); + return parsed.command ? 1 : 0; +} + +if (require.main === module) { + try { + process.exitCode = main(process.argv.slice(2)); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} + +module.exports = {main, parseArgs}; diff --git a/backend/package.json b/backend/package.json index 1a7d4d861..71494fd30 100644 --- a/backend/package.json +++ b/backend/package.json @@ -64,6 +64,10 @@ "test:skill-eval": "jest --testPathPatterns=tests/skill-eval --runInBand --testTimeout=120000 --forceExit", "test:skill-eval:real-traces": "jest --runInBand --testTimeout=180000 --forceExit tests/skill-eval/scrolling_analysis.eval.ts tests/skill-eval/startup_analysis.eval.ts", "test:scene-trace-regression": "TP_PORT_MIN=9800 TP_PORT_MAX=9899 tsx tests/skill-eval/scene_trace_regression.ts", + "trace:validate": "node ../Trace/tools/trace-corpus.cjs validate --check-generated", + "trace:build": "node ../Trace/tools/trace-corpus.cjs build --check", + "trace:test": "TP_PORT_MIN=9900 TP_PORT_MAX=9999 tsx tests/trace-corpus/trace_corpus_regression.ts", + "trace:regression": "npm run trace:validate && npm run trace:build && npm run trace:test", "test:state-timeline-regression": "TP_PORT_MIN=9800 TP_PORT_MAX=9899 tsx tests/skill-eval/state_timeline_regression.ts", "test:integration": "jest --testPathPatterns=tests/integration --runInBand --forceExit", "test:agent-eval": "tsx tests/agent-eval/evaluationRunner.ts", @@ -75,7 +79,7 @@ "benchmark:trace-rss:audit": "tsx src/scripts/auditTraceProcessorRssMatrix.ts", "benchmark:enterprise-load": "tsx src/scripts/enterpriseAcceptanceLoadTest.ts", "enterprise:readiness-audit": "tsx src/scripts/enterpriseReadinessAudit.ts", - "verify:pr": "npm run validate:skills && npm run validate:strategies && npm run validate:cases && npm run typecheck && npm run build && node scripts/check-cli-pack.cjs && npm run test:core && npm run trace-processor:ensure && npm run test:scene-trace-regression", + "verify:pr": "npm run validate:skills && npm run validate:strategies && npm run validate:cases && npm run trace:validate && npm run typecheck && npm run build && node scripts/check-cli-pack.cjs && npm run test:core && npm run trace-processor:ensure && npm run test:scene-trace-regression", "enterprise:migration": "tsx src/scripts/enterpriseMigrationSnapshot.ts", "skill:validate": "tsx src/cli/index.ts validate", "validate:skills": "tsx src/cli/index.ts validate --contracts --all", diff --git a/backend/scripts/run-codebase-aware-e2e.cjs b/backend/scripts/run-codebase-aware-e2e.cjs index b466a2cf7..a8b0007ec 100644 --- a/backend/scripts/run-codebase-aware-e2e.cjs +++ b/backend/scripts/run-codebase-aware-e2e.cjs @@ -8,6 +8,7 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); const {spawnSync} = require('child_process'); +const {resolveCaseTrace} = require('../../Trace/tools/lib/catalog.cjs'); const backendRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(backendRoot, '..'); @@ -364,12 +365,12 @@ function resolveCli(selectedMode) { function resolveHeavyTrace() { return process.env.SMARTPERFETTO_E2E_HEAVY_TRACE || - path.join(repoRoot, 'test-traces', 'lacunh_heavy.pftrace'); + resolveCaseTrace(repoRoot, 'lacunh_heavy.pftrace'); } function resolveLightTrace() { return process.env.SMARTPERFETTO_E2E_LIGHT_TRACE || - path.join(repoRoot, 'test-traces', 'launch_light.pftrace'); + resolveCaseTrace(repoRoot, 'launch_light.pftrace'); } function resolveHighPerformanceRoot() { diff --git a/backend/scripts/run-deepseek-agent-e2e.cjs b/backend/scripts/run-deepseek-agent-e2e.cjs index 134755b68..be51dd3cf 100644 --- a/backend/scripts/run-deepseek-agent-e2e.cjs +++ b/backend/scripts/run-deepseek-agent-e2e.cjs @@ -30,7 +30,7 @@ const suites = { '--provider-id', 'env', '--trace', - '../test-traces/lacunh_heavy.pftrace', + '../Trace/real/android-startup-heavy/trace.pftrace', '--query', '分析启动性能', '--output', @@ -68,7 +68,7 @@ const suites = { '--provider-id', 'env', '--trace', - '../test-traces/scroll-demo-customer-scroll.pftrace', + '../Trace/real/android-scroll-customer/trace.pftrace', '--query', '分析滑动性能', '--output', @@ -98,9 +98,9 @@ const suites = { '--provider-id', 'env', '--trace', - '../test-traces/lacunh_heavy.pftrace', + '../Trace/real/android-startup-heavy/trace.pftrace', '--reference-trace', - '../test-traces/launch_light.pftrace', + '../Trace/real/android-startup-light/trace.pftrace', '--query', '对比左右两个 Trace 的启动速度差异。请先读取窗口映射,然后用 compare_skill 跑 startup_analysis 对比冷启动阶段,最后用证据说明哪边更慢。', '--output', diff --git a/backend/scripts/run-quick-agent-e2e.cjs b/backend/scripts/run-quick-agent-e2e.cjs index 0677e5ee7..44ba2c537 100644 --- a/backend/scripts/run-quick-agent-e2e.cjs +++ b/backend/scripts/run-quick-agent-e2e.cjs @@ -18,7 +18,7 @@ const QUICK_RUNTIME_KINDS = [ 'pi-agent-core', 'opencode', ]; -const DEFAULT_TRACE = '../test-traces/scroll-demo-customer-scroll.pftrace'; +const DEFAULT_TRACE = '../Trace/real/android-scroll-customer/trace.pftrace'; const DEFAULT_TIMEOUT_MS = '300000'; const QUICK_FULL_REPORT_FALLBACK = 'quick_full_report_shape'; const QUICK_REFERENCE_HEADING = '## 逐句数据引用(结构化来源)'; diff --git a/backend/skills/atomic/android_gpu_work_period_track.skill.yaml b/backend/skills/atomic/android_gpu_work_period_track.skill.yaml index fe4f44c77..7f32862d7 100644 --- a/backend/skills/atomic/android_gpu_work_period_track.skill.yaml +++ b/backend/skills/atomic/android_gpu_work_period_track.skill.yaml @@ -47,9 +47,12 @@ steps: type: number sql: | SELECT - ts, - ROUND(dur / 1e6, 2) AS dur_ms, - uid - FROM android_gpu_work_period_track - ORDER BY ts ASC + s.ts, + ROUND(s.dur / 1e6, 2) AS dur_ms, + t.uid, + t.gpu_id + FROM android_gpu_work_period_track t + JOIN slice s ON s.track_id = t.id + WHERE s.dur > 0 + ORDER BY s.ts ASC LIMIT 100 diff --git a/backend/skills/atomic/app_process_starts_summary.skill.yaml b/backend/skills/atomic/app_process_starts_summary.skill.yaml index 62887cf68..41b9dd01f 100644 --- a/backend/skills/atomic/app_process_starts_summary.skill.yaml +++ b/backend/skills/atomic/app_process_starts_summary.skill.yaml @@ -65,11 +65,11 @@ steps: sql: | SELECT process_name, - process_start_ts, + proc_start_ts AS process_start_ts, ROUND(total_dur / 1e6, 1) AS total_dur_ms, reason FROM android_app_process_starts WHERE (process_name GLOB '${package}*' OR '${package}' = '') - AND (${start_ts} IS NULL OR process_start_ts >= ${start_ts}) - AND (${end_ts} IS NULL OR process_start_ts < ${end_ts}) - ORDER BY process_start_ts ASC + AND (${start_ts} IS NULL OR proc_start_ts >= ${start_ts}) + AND (${end_ts} IS NULL OR proc_start_ts < ${end_ts}) + ORDER BY proc_start_ts ASC diff --git a/backend/skills/atomic/cpu_process_utilization_period.skill.yaml b/backend/skills/atomic/cpu_process_utilization_period.skill.yaml index 2817d2fb1..d9cb976f9 100644 --- a/backend/skills/atomic/cpu_process_utilization_period.skill.yaml +++ b/backend/skills/atomic/cpu_process_utilization_period.skill.yaml @@ -43,9 +43,19 @@ steps: type: number format: compact sql: | - SELECT - process_name, - ROUND(utilization, 4) AS utilization - FROM cpu_process_utilization_per_period + WITH target_processes AS ( + SELECT upid, name AS process_name + FROM process + WHERE upid IS NOT NULL AND name IS NOT NULL + ), samples AS ( + SELECT + p.process_name, + u.ts, + u.utilization + FROM target_processes p + CROSS JOIN cpu_process_utilization_per_period(time_from_ms(100), p.upid) u + ) + SELECT process_name, ts, ROUND(utilization, 4) AS utilization + FROM samples ORDER BY utilization DESC LIMIT 30 diff --git a/backend/skills/atomic/cpu_thread_utilization_period.skill.yaml b/backend/skills/atomic/cpu_thread_utilization_period.skill.yaml index a6301b18e..9b7fafb88 100644 --- a/backend/skills/atomic/cpu_thread_utilization_period.skill.yaml +++ b/backend/skills/atomic/cpu_thread_utilization_period.skill.yaml @@ -58,11 +58,22 @@ steps: type: number format: compact sql: | - SELECT - process_name, - thread_name, - ROUND(utilization, 4) AS utilization - FROM cpu_thread_utilization_per_period - WHERE (process_name GLOB '${process_name}*' OR '${process_name}' = '') + WITH target_threads AS ( + SELECT t.utid, t.name AS thread_name, p.name AS process_name + FROM thread t + LEFT JOIN process p USING (upid) + WHERE t.utid IS NOT NULL + AND (p.name GLOB '${process_name}*' OR '${process_name}' = '') + ), samples AS ( + SELECT + t.process_name, + t.thread_name, + u.ts, + u.utilization + FROM target_threads t + CROSS JOIN cpu_thread_utilization_per_period(time_from_ms(100), t.utid) u + ) + SELECT process_name, thread_name, ts, ROUND(utilization, 4) AS utilization + FROM samples ORDER BY utilization DESC LIMIT ${top_n|30} diff --git a/backend/skills/atomic/cpu_utilization_per_period.skill.yaml b/backend/skills/atomic/cpu_utilization_per_period.skill.yaml index f0c793c1b..28d521eda 100644 --- a/backend/skills/atomic/cpu_utilization_per_period.skill.yaml +++ b/backend/skills/atomic/cpu_utilization_per_period.skill.yaml @@ -49,7 +49,7 @@ steps: sql: | SELECT ts, - ROUND(dur / 1e6, 1) AS dur_ms, + 100.0 AS dur_ms, ROUND(utilization, 4) AS utilization - FROM cpu_utilization_per_period + FROM cpu_utilization_per_period(time_from_ms(100)) ORDER BY ts ASC diff --git a/backend/skills/atomic/frame_overrun_summary.skill.yaml b/backend/skills/atomic/frame_overrun_summary.skill.yaml index 5ab330bf5..eea1148cc 100644 --- a/backend/skills/atomic/frame_overrun_summary.skill.yaml +++ b/backend/skills/atomic/frame_overrun_summary.skill.yaml @@ -59,10 +59,13 @@ steps: format: duration_ms sql: | SELECT - frame_id, - ts, - ROUND(dur / 1e6, 2) AS dur_ms, - ROUND(overrun / 1e6, 2) AS overrun_ms - FROM android_frames_overrun - ORDER BY overrun DESC + o.frame_id, + MIN(a.ts) AS ts, + ROUND(MAX(a.dur) / 1e6, 2) AS dur_ms, + ROUND(o.overrun / 1e6, 2) AS overrun_ms + FROM android_frames_overrun o + LEFT JOIN actual_frame_timeline_slice a + ON CAST(a.name AS INTEGER) = o.frame_id + GROUP BY o.frame_id, o.overrun + ORDER BY o.overrun DESC LIMIT 100 diff --git a/backend/skills/atomic/memory_pressure_in_range.skill.yaml b/backend/skills/atomic/memory_pressure_in_range.skill.yaml index f45b604c9..a3fb3d9cd 100644 --- a/backend/skills/atomic/memory_pressure_in_range.skill.yaml +++ b/backend/skills/atomic/memory_pressure_in_range.skill.yaml @@ -52,6 +52,7 @@ inputs: steps: - id: memory_pressure_analysis + type: atomic description: Analyze memory pressure indicators in the given time range optional: true sql: | @@ -379,4 +380,4 @@ diagnostics: 建议: 1. 减少内存分配频率 2. 避免在关键路径上分配大对象 - 3. 使用对象池复用对象 \ No newline at end of file + 3. 使用对象池复用对象 diff --git a/backend/skills/atomic/memory_rss_high_watermark.skill.yaml b/backend/skills/atomic/memory_rss_high_watermark.skill.yaml index b05b894e4..d8c6981b1 100644 --- a/backend/skills/atomic/memory_rss_high_watermark.skill.yaml +++ b/backend/skills/atomic/memory_rss_high_watermark.skill.yaml @@ -55,8 +55,8 @@ steps: sql: | SELECT process_name, - rss_high_watermark_kb + ROUND(rss_high_watermark / 1024.0, 2) AS rss_high_watermark_kb FROM memory_rss_high_watermark_per_process WHERE (process_name GLOB '${process_name}*' OR '${process_name}' = '') - ORDER BY rss_high_watermark_kb DESC + ORDER BY rss_high_watermark DESC LIMIT ${top_n|30} diff --git a/backend/skills/atomic/pipeline_4feature_scoring.skill.yaml b/backend/skills/atomic/pipeline_4feature_scoring.skill.yaml index 7229b1671..175e3e3e9 100644 --- a/backend/skills/atomic/pipeline_4feature_scoring.skill.yaml +++ b/backend/skills/atomic/pipeline_4feature_scoring.skill.yaml @@ -272,7 +272,7 @@ steps: WHEN (SELECT cnt FROM swappy) > 0 THEN 'swappy_pacing' WHEN (SELECT cnt FROM achoreographer) > 0 THEN 'achoreographer' WHEN (SELECT cnt FROM engine_loop) > 0 THEN 'engine_main_loop' - WHEN (SELECT cnt FROM camera) > 0 THEN 'camera_sensor_trigger' + WHEN (SELECT cnt FROM camera) > 0 THEN 'camera_request_activity' WHEN (SELECT cnt FROM codec) > 0 THEN 'video_codec_pacing' WHEN (SELECT cnt FROM set_frame_rate) > 0 THEN 'set_frame_rate_voted' ELSE 'vsync_app_only' diff --git a/backend/skills/atomic/vsync_period_detection.skill.yaml b/backend/skills/atomic/vsync_period_detection.skill.yaml index f2f002b49..a369baa80 100644 --- a/backend/skills/atomic/vsync_period_detection.skill.yaml +++ b/backend/skills/atomic/vsync_period_detection.skill.yaml @@ -46,6 +46,7 @@ inputs: steps: - id: detect_vsync_period + type: atomic description: Detect VSync period using multiple data sources sql: | -- VSync Period Detection with Multiple Sources @@ -175,4 +176,4 @@ steps: description: Number of samples used for detection - name: theoretical_fps type: number - description: Theoretical max FPS based on refresh rate \ No newline at end of file + description: Theoretical max FPS based on refresh rate diff --git a/backend/skills/atomic/wattson_rails_power_breakdown.skill.yaml b/backend/skills/atomic/wattson_rails_power_breakdown.skill.yaml index 9356c700a..aaaca33eb 100644 --- a/backend/skills/atomic/wattson_rails_power_breakdown.skill.yaml +++ b/backend/skills/atomic/wattson_rails_power_breakdown.skill.yaml @@ -77,12 +77,6 @@ steps: label: "数据来源" type: string sql: | - WITH window AS ( - SELECT - COALESCE(${start_ts}, trace_start()) AS ts, - MAX(COALESCE(${end_ts}, trace_end()) - COALESCE(${start_ts}, trace_start()), 0) AS dur, - 0 AS period_id - ) SELECT subsystem, breakdown_type, @@ -91,7 +85,12 @@ steps: ROUND(SUM(estimated_mws) / 3600.0, 6) AS energy_mwh, ROUND(SUM(estimated_mws) * 1e9 / NULLIF(SUM(period_dur), 0), 2) AS avg_power_mw, 'wattson_estimate' AS source_level - FROM wattson_rails_aggregation!(window) + FROM wattson_rails_aggregation!(( + SELECT + COALESCE(${start_ts}, trace_start()) AS ts, + MAX(COALESCE(${end_ts}, trace_end()) - COALESCE(${start_ts}, trace_start()), 0) AS dur, + 0 AS period_id + )) GROUP BY subsystem, breakdown_type, component_id ORDER BY total_energy_mws DESC LIMIT ${top_n|20} diff --git a/backend/skills/atomic/wattson_thread_power_attribution.skill.yaml b/backend/skills/atomic/wattson_thread_power_attribution.skill.yaml index a7c9561b0..591a23136 100644 --- a/backend/skills/atomic/wattson_thread_power_attribution.skill.yaml +++ b/backend/skills/atomic/wattson_thread_power_attribution.skill.yaml @@ -82,12 +82,6 @@ steps: label: "数据来源" type: string sql: | - WITH window AS ( - SELECT - COALESCE(${start_ts}, trace_start()) AS ts, - MAX(COALESCE(${end_ts}, trace_end()) - COALESCE(${start_ts}, trace_start()), 0) AS dur, - 0 AS period_id - ) SELECT process_name, thread_name, @@ -95,7 +89,12 @@ steps: ROUND(SUM(total_mws) / 3600.0, 6) AS energy_mwh, ROUND(SUM(total_mws) * 1e9 / NULLIF(SUM(period_dur), 0), 2) AS avg_cpu_mw, 'wattson_estimate' AS source_level - FROM wattson_threads_aggregation!(window) + FROM wattson_threads_aggregation!(( + SELECT + COALESCE(${start_ts}, trace_start()) AS ts, + MAX(COALESCE(${end_ts}, trace_end()) - COALESCE(${start_ts}, trace_start()), 0) AS dur, + 0 AS period_id + )) WHERE ( ('${process_name}' != '' AND process_name GLOB '${process_name}*') OR ('${package}' != '' AND process_name GLOB '${package}*') diff --git a/backend/skills/composite/camera_trace_evidence.skill.yaml b/backend/skills/composite/camera_trace_evidence.skill.yaml new file mode 100644 index 000000000..47539dc5b --- /dev/null +++ b/backend/skills/composite/camera_trace_evidence.skill.yaml @@ -0,0 +1,787 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2024-2026 Gracker (Chris) +# This file is part of SmartPerfetto. See LICENSE for details. + +name: camera_trace_evidence +version: "1.0" +type: composite +category: rendering +tier: B + +meta: + display_name: "Camera Trace 证据" + description: "枚举 Camera 相关 identity、slice、Binder、系统上下文、DMA-BUF/legacy ION 与可选 Pixel stage 证据" + icon: "photo_camera" + tags: [camera, rendering, evidence, binder, dmabuf, ion, pixel] + +triggers: + keywords: + zh: [Camera, 相机, 打开相机, 相机内存] + en: [camera, camera open, camera memory] + patterns: + - ".*(camera|Camera|相机).*(open|preview|capture|打开|预览|拍照|DMA|dmabuf).*" + - ".*(open|preview|capture|打开|预览|拍照).*(camera|Camera|相机).*" + +prerequisites: + required_tables: [slice, thread, process] + modules: + - slices.with_context + - android.binder + - android.frames.timeline + - android.memory.dmabuf + - linux.cpu.frequency + - pixel.camera + +inputs: + - name: start_ts + type: timestamp + required: false + description: "分析起始时间戳(ns),默认 trace_start()" + - name: end_ts + type: timestamp + required: false + description: "分析结束时间戳(ns),默认 trace_end()" + - name: max_rows + type: integer + required: false + default: 20 + description: "每个明细步骤最大返回行数,限制为 1-100" + +steps: + - id: evidence_coverage + type: atomic + name: "Camera 证据覆盖" + display: + layer: overview + level: summary + title: "Camera 证据覆盖" + columns: + - name: evidence_family + label: "证据族" + type: string + - name: status + label: "状态" + type: string + - name: row_count + label: "行数" + type: number + - name: source + label: "来源" + type: string + - name: limitation + label: "限制" + type: string + format: truncate + sql: | + WITH + raw_input AS ( + SELECT + COALESCE(${start_ts}, trace_start()) AS raw_start_ts, + COALESCE(${end_ts}, trace_end()) AS raw_end_ts, + MIN(MAX(COALESCE(${max_rows|20}, 20), 1), 100) AS max_rows + ), + input AS ( + SELECT + MIN(raw_start_ts, raw_end_ts) AS start_ts, + MAX(raw_start_ts, raw_end_ts) AS end_ts, + max_rows + FROM raw_input + ), + identity_count AS ( + SELECT + (SELECT COUNT(*) + FROM process + WHERE lower(COALESCE(name, '')) GLOB '*camera*' + OR lower(COALESCE(name, '')) GLOB '*camx*' + OR lower(COALESCE(name, '')) GLOB '*mtkcam*') + + + (SELECT COUNT(*) + FROM thread + WHERE lower(COALESCE(name, '')) GLOB '*camera*' + OR lower(COALESCE(name, '')) GLOB '*camx*' + OR lower(COALESCE(name, '')) GLOB '*mtkcam*') AS candidate_count + ), + slice_count AS ( + SELECT COUNT(*) AS candidate_count + FROM thread_slice, input + WHERE ts < input.end_ts + AND ts + IIF(dur = -1, trace_end() - ts, dur) > input.start_ts + AND ( + lower(COALESCE(name, '')) GLOB '*camera*' + OR lower(COALESCE(name, '')) GLOB '*camx*' + OR lower(COALESCE(name, '')) GLOB '*mtkcam*' + OR lower(COALESCE(name, '')) GLOB '*capture*' + OR lower(COALESCE(name, '')) GLOB '*preview*' + ) + ), + binder_count AS ( + SELECT COUNT(*) AS candidate_count + FROM android_binder_txns, input + WHERE client_ts >= input.start_ts + AND client_ts < input.end_ts + AND ( + lower(COALESCE(client_process, '')) GLOB '*camera*' + OR lower(COALESCE(server_process, '')) GLOB '*camera*' + OR lower(COALESCE(client_process, '')) GLOB '*camx*' + OR lower(COALESCE(server_process, '')) GLOB '*camx*' + OR lower(COALESCE(client_process, '')) GLOB '*mtkcam*' + OR lower(COALESCE(server_process, '')) GLOB '*mtkcam*' + ) + ), + scheduler_count AS ( + SELECT COUNT(*) AS candidate_count + FROM sched_slice, input + WHERE ts < input.end_ts + AND ts + IIF(dur = -1, trace_end() - ts, dur) > input.start_ts + ), + frequency_count AS ( + SELECT COUNT(*) AS candidate_count + FROM cpu_frequency_counters, input + WHERE ts < input.end_ts + AND ts + IIF(dur = -1, trace_end() - ts, dur) > input.start_ts + ), + frame_count AS ( + SELECT COUNT(*) AS candidate_count + FROM actual_frame_timeline_slice, input + WHERE ts < input.end_ts + AND ts + IIF(dur = -1, trace_end() - ts, dur) > input.start_ts + ), + buffer_memory_count AS ( + SELECT + (SELECT COUNT(*) + FROM android_dmabuf_allocs, input + WHERE ts >= input.start_ts + AND ts < input.end_ts) + + + (SELECT COUNT(*) + FROM counter AS c + JOIN thread_counter_track AS tct ON c.track_id = tct.id + CROSS JOIN input + WHERE tct.name GLOB 'mem.ion_change*' + AND c.ts >= input.start_ts + AND c.ts < input.end_ts) AS candidate_count + ), + pixel_count AS ( + SELECT COUNT(*) AS candidate_count + FROM pixel_camera_frames, input + WHERE ts >= input.start_ts AND ts < input.end_ts + ) + SELECT + 'camera_process_thread_identity' AS evidence_family, + CASE WHEN candidate_count > 0 THEN 'available' ELSE 'missing' END AS status, + candidate_count AS row_count, + 'process,thread' AS source, + 'Name-based identity candidates are not proof of a specific Camera milestone.' AS limitation + FROM identity_count + UNION ALL + SELECT + 'camera_slice_candidates', + CASE WHEN candidate_count > 0 THEN 'vendor_specific' ELSE 'missing' END, + candidate_count, + 'thread_slice', + 'Slice names are implementation-specific candidates and require identity/anchor verification.' + FROM slice_count + UNION ALL + SELECT + 'binder_transactions', + CASE WHEN candidate_count > 0 THEN 'available' ELSE 'missing' END, + candidate_count, + 'android_binder_txns', + 'Binder rows provide cross-process correlation; interface names and endpoints must still be verified.' + FROM binder_count + UNION ALL + SELECT + 'scheduler_context', + CASE WHEN candidate_count > 0 THEN 'available' ELSE 'missing' END, + candidate_count, + 'sched_slice', + 'Scheduler rows provide execution context only after Camera thread identity is established.' + FROM scheduler_count + UNION ALL + SELECT + 'cpu_frequency_context', + CASE WHEN candidate_count > 0 THEN 'available' ELSE 'missing' END, + candidate_count, + 'cpu_frequency_counters', + 'CPU-frequency rows provide system context and do not by themselves attribute Camera latency.' + FROM frequency_count + UNION ALL + SELECT + 'frame_timeline', + CASE WHEN candidate_count > 0 THEN 'available' ELSE 'missing' END, + candidate_count, + 'actual_frame_timeline_slice', + 'FrameTimeline can support presentation correlation only after preview surface identity is established.' + FROM frame_count + UNION ALL + SELECT + 'dmabuf_allocations', + CASE WHEN candidate_count > 0 THEN 'available' ELSE 'missing' END, + candidate_count, + 'android_dmabuf_allocs; counter + thread_counter_track (mem.ion_change*)', + 'DMA-BUF and legacy ION signed deltas cover only observed events; they are not retained memory or leak proof.' + FROM buffer_memory_count + UNION ALL + SELECT + 'pixel_camera_frames', + CASE WHEN candidate_count > 0 THEN 'vendor_specific' ELSE 'missing' END, + candidate_count, + 'pixel_camera_frames', + 'pixel.camera is an optional Pixel slice parser, not a portable Android Camera contract.' + FROM pixel_count + save_as: evidence_coverage + synthesize: + role: overview + fields: + - key: evidence_family + label: 证据族 + - key: status + label: 可用性 + - key: row_count + label: 行数 + - key: limitation + label: 限制 + + - id: camera_process_candidates + type: atomic + name: "Camera 进程/线程候选" + display: + layer: list + level: detail + title: "Camera identity 候选" + columns: + - name: identity_kind + label: "类型" + type: string + - name: process_name + label: "进程" + type: string + - name: thread_name + label: "线程" + type: string + - name: upid + label: "UPID" + type: number + - name: pid + label: "PID" + type: number + - name: utid + label: "UTID" + type: number + - name: tid + label: "TID" + type: number + - name: source + label: "来源" + type: string + - name: limitation + label: "限制" + type: string + format: truncate + sql: | + WITH + raw_input AS ( + SELECT + COALESCE(${start_ts}, trace_start()) AS raw_start_ts, + COALESCE(${end_ts}, trace_end()) AS raw_end_ts, + MIN(MAX(COALESCE(${max_rows|20}, 20), 1), 100) AS max_rows + ), + input AS ( + SELECT + MIN(raw_start_ts, raw_end_ts) AS start_ts, + MAX(raw_start_ts, raw_end_ts) AS end_ts, + max_rows + FROM raw_input + ), + candidates AS ( + SELECT + 'process' AS identity_kind, + COALESCE(p.name, '') AS process_name, + CAST(NULL AS TEXT) AS thread_name, + p.upid, + p.pid, + CAST(NULL AS INTEGER) AS utid, + CAST(NULL AS INTEGER) AS tid, + 'process' AS source + FROM process p + WHERE lower(COALESCE(p.name, '')) GLOB '*camera*' + OR lower(COALESCE(p.name, '')) GLOB '*camx*' + OR lower(COALESCE(p.name, '')) GLOB '*mtkcam*' + UNION ALL + SELECT + 'thread', + COALESCE(p.name, ''), + COALESCE(t.name, ''), + t.upid, + p.pid, + t.utid, + t.tid, + 'thread' + FROM thread t + LEFT JOIN process p ON p.upid = t.upid + WHERE lower(COALESCE(t.name, '')) GLOB '*camera*' + OR lower(COALESCE(t.name, '')) GLOB '*camx*' + OR lower(COALESCE(t.name, '')) GLOB '*mtkcam*' + ) + SELECT + identity_kind, + process_name, + thread_name, + upid, + pid, + utid, + tid, + source, + 'Name matches are identity candidates; verify process/thread ownership before attributing Camera work.' AS limitation + FROM candidates, input + ORDER BY identity_kind, process_name, thread_name + LIMIT (SELECT max_rows FROM input) + save_as: camera_process_candidates + synthesize: + role: list + fields: + - key: identity_kind + label: Identity 类型 + - key: process_name + label: 进程 + - key: thread_name + label: 线程 + - key: source + label: 来源 + + - id: camera_slice_candidates + type: atomic + name: "Camera Slice 候选" + display: + layer: list + level: detail + title: "Camera slice 候选" + columns: + - name: ts + label: "开始" + type: timestamp + unit: ns + clickAction: navigate_range + durationColumn: dur_ns + - name: dur_ns + label: "时长" + type: duration + unit: ns + format: duration_ms + - name: slice_name + label: "Slice" + type: string + format: truncate + - name: process_name + label: "进程" + type: string + - name: thread_name + label: "线程" + type: string + - name: upid + label: "UPID" + type: number + - name: utid + label: "UTID" + type: number + - name: source + label: "来源" + type: string + - name: limitation + label: "限制" + type: string + format: truncate + sql: | + WITH + raw_input AS ( + SELECT + COALESCE(${start_ts}, trace_start()) AS raw_start_ts, + COALESCE(${end_ts}, trace_end()) AS raw_end_ts, + MIN(MAX(COALESCE(${max_rows|20}, 20), 1), 100) AS max_rows + ), + input AS ( + SELECT + MIN(raw_start_ts, raw_end_ts) AS start_ts, + MAX(raw_start_ts, raw_end_ts) AS end_ts, + max_rows + FROM raw_input + ), + candidates AS ( + SELECT + MAX(ts, input.start_ts) AS overlap_ts, + MIN(ts + IIF(dur = -1, trace_end() - ts, dur), input.end_ts) + - MAX(ts, input.start_ts) AS overlap_dur, + COALESCE(name, '') AS slice_name, + COALESCE(process_name, '') AS process_name, + COALESCE(thread_name, '') AS thread_name, + upid, + utid + FROM thread_slice, input + WHERE ts < input.end_ts + AND ts + IIF(dur = -1, trace_end() - ts, dur) > input.start_ts + AND ( + lower(COALESCE(name, '')) GLOB '*camera*' + OR lower(COALESCE(name, '')) GLOB '*camx*' + OR lower(COALESCE(name, '')) GLOB '*mtkcam*' + OR lower(COALESCE(name, '')) GLOB '*capture*' + OR lower(COALESCE(name, '')) GLOB '*preview*' + ) + ) + SELECT + printf('%d', overlap_ts) AS ts, + printf('%d', overlap_dur) AS dur_ns, + slice_name, + process_name, + thread_name, + upid, + utid, + 'thread_slice' AS source, + 'Slice names are implementation-specific candidates and require identity/anchor verification.' AS limitation + FROM candidates, input + WHERE overlap_dur > 0 + ORDER BY overlap_dur DESC + LIMIT (SELECT max_rows FROM input) + save_as: camera_slice_candidates + synthesize: + role: list + fields: + - key: slice_name + label: Slice 候选 + - key: process_name + label: 进程 + - key: thread_name + label: 线程 + - key: source + label: 来源 + + - id: camera_binder_summary + type: atomic + name: "Camera Binder 事务" + display: + layer: list + level: detail + title: "Camera 相关 Binder 事务" + columns: + - name: ts + label: "开始" + type: timestamp + unit: ns + clickAction: navigate_range + durationColumn: dur_ns + - name: dur_ns + label: "耗时" + type: duration + unit: ns + format: duration_ms + - name: aidl_name + label: "AIDL" + type: string + format: truncate + - name: client_process + label: "Client 进程" + type: string + - name: client_thread + label: "Client 线程" + type: string + - name: server_process + label: "Server 进程" + type: string + - name: server_thread + label: "Server 线程" + type: string + - name: client_upid + label: "Client UPID" + type: number + - name: client_utid + label: "Client UTID" + type: number + - name: server_upid + label: "Server UPID" + type: number + - name: server_utid + label: "Server UTID" + type: number + - name: source + label: "来源" + type: string + - name: limitation + label: "限制" + type: string + format: truncate + sql: | + WITH + raw_input AS ( + SELECT + COALESCE(${start_ts}, trace_start()) AS raw_start_ts, + COALESCE(${end_ts}, trace_end()) AS raw_end_ts, + MIN(MAX(COALESCE(${max_rows|20}, 20), 1), 100) AS max_rows + ), + input AS ( + SELECT + MIN(raw_start_ts, raw_end_ts) AS start_ts, + MAX(raw_start_ts, raw_end_ts) AS end_ts, + max_rows + FROM raw_input + ) + SELECT + printf('%d', client_ts) AS ts, + printf('%d', MAX(client_dur, 0)) AS dur_ns, + COALESCE(aidl_name, interface, method_name, '') AS aidl_name, + COALESCE(client_process, '') AS client_process, + COALESCE(client_thread, '') AS client_thread, + COALESCE(server_process, '') AS server_process, + COALESCE(server_thread, '') AS server_thread, + client_upid, + client_utid, + server_upid, + server_utid, + 'android_binder_txns' AS source, + 'Binder rows provide cross-process correlation; interface names and endpoints must still be verified.' AS limitation + FROM android_binder_txns, input + WHERE client_ts >= input.start_ts + AND client_ts < input.end_ts + AND ( + lower(COALESCE(client_process, '')) GLOB '*camera*' + OR lower(COALESCE(server_process, '')) GLOB '*camera*' + OR lower(COALESCE(client_process, '')) GLOB '*camx*' + OR lower(COALESCE(server_process, '')) GLOB '*camx*' + OR lower(COALESCE(client_process, '')) GLOB '*mtkcam*' + OR lower(COALESCE(server_process, '')) GLOB '*mtkcam*' + ) + ORDER BY client_dur DESC, client_ts + LIMIT (SELECT max_rows FROM input) + save_as: camera_binder_summary + synthesize: + role: list + fields: + - key: aidl_name + label: Binder 接口 + - key: client_process + label: Client + - key: server_process + label: Server + - key: source + label: 来源 + + - id: camera_dmabuf_summary + type: atomic + name: "Camera DMA-BUF/legacy ION window evidence" + display: + layer: list + level: detail + title: "Camera DMA-BUF/legacy ION window evidence" + columns: + - name: process_name + label: "进程" + type: string + - name: upid + label: "UPID" + type: number + - name: pid + label: "PID" + type: number + - name: memory_source + label: "内存来源" + type: string + - name: allocation_count + label: "分配次数" + type: number + - name: allocation_bytes + label: "分配量" + type: bytes + - name: release_bytes + label: "释放量" + type: bytes + - name: observed_net_delta_bytes + label: "窗口内观测净变化" + type: bytes + - name: peak_event_bytes + label: "最大单事件" + type: bytes + - name: source + label: "来源" + type: string + - name: limitation + label: "限制" + type: string + format: truncate + sql: | + WITH + raw_input AS ( + SELECT + COALESCE(${start_ts}, trace_start()) AS raw_start_ts, + COALESCE(${end_ts}, trace_end()) AS raw_end_ts, + MIN(MAX(COALESCE(${max_rows|20}, 20), 1), 100) AS max_rows + ), + input AS ( + SELECT + MIN(raw_start_ts, raw_end_ts) AS start_ts, + MAX(raw_start_ts, raw_end_ts) AS end_ts, + max_rows + FROM raw_input + ), + events AS ( + SELECT + alloc.ts, + alloc.buf_size AS signed_bytes, + COALESCE(alloc.process_name, '') AS process_name, + alloc.upid, + alloc.pid, + 'dmabuf' AS memory_source + FROM android_dmabuf_allocs AS alloc + CROSS JOIN input + WHERE alloc.ts >= input.start_ts + AND alloc.ts < input.end_ts + AND ( + lower(COALESCE(alloc.process_name, '')) GLOB '*camera*' + OR lower(COALESCE(alloc.process_name, '')) GLOB '*camx*' + OR lower(COALESCE(alloc.process_name, '')) GLOB '*mtkcam*' + OR lower(COALESCE(alloc.thread_name, '')) GLOB '*camera*' + OR lower(COALESCE(alloc.thread_name, '')) GLOB '*camx*' + OR lower(COALESCE(alloc.thread_name, '')) GLOB '*mtkcam*' + ) + UNION ALL + SELECT + c.ts, + CAST(c.value AS INTEGER) AS signed_bytes, + COALESCE(p.name, '') AS process_name, + p.upid, + p.pid, + 'legacy_ion' AS memory_source + FROM counter AS c + JOIN thread_counter_track AS tct ON c.track_id = tct.id + JOIN thread AS th ON th.utid = tct.utid + LEFT JOIN process AS p ON p.upid = th.upid + CROSS JOIN input + WHERE tct.name GLOB 'mem.ion_change*' + AND c.ts >= input.start_ts + AND c.ts < input.end_ts + AND ( + lower(COALESCE(p.name, '')) GLOB '*camera*' + OR lower(COALESCE(p.name, '')) GLOB '*camx*' + OR lower(COALESCE(p.name, '')) GLOB '*mtkcam*' + OR lower(COALESCE(th.name, '')) GLOB '*camera*' + OR lower(COALESCE(th.name, '')) GLOB '*camx*' + OR lower(COALESCE(th.name, '')) GLOB '*mtkcam*' + ) + ) + SELECT + process_name, + upid, + pid, + memory_source, + SUM(CASE WHEN signed_bytes > 0 THEN 1 ELSE 0 END) AS allocation_count, + SUM(CASE WHEN signed_bytes > 0 THEN signed_bytes ELSE 0 END) AS allocation_bytes, + -SUM(CASE WHEN signed_bytes < 0 THEN signed_bytes ELSE 0 END) AS release_bytes, + SUM(signed_bytes) AS observed_net_delta_bytes, + MAX(ABS(signed_bytes)) AS peak_event_bytes, + CASE memory_source + WHEN 'dmabuf' THEN 'android_dmabuf_allocs' + ELSE 'counter + thread_counter_track (mem.ion_change*)' + END AS source, + 'DMA-BUF (android_dmabuf_allocs) and legacy ION (counter + thread_counter_track mem.ion_change*) signed deltas cover only events inside this selected window; allocations may predate it and releases may occur later, so this is not retained memory or leak proof.' AS limitation + FROM events + GROUP BY process_name, upid, pid, memory_source + ORDER BY ABS(SUM(signed_bytes)) DESC, allocation_bytes DESC + LIMIT (SELECT max_rows FROM input) + save_as: camera_dmabuf_summary + synthesize: + role: list + fields: + - key: process_name + label: 进程 + - key: memory_source + label: 内存来源 + - key: allocation_bytes + label: 窗口内分配量 + - key: release_bytes + label: 窗口内释放量 + - key: observed_net_delta_bytes + label: 窗口内观测净变化 + - key: limitation + label: 限制 + + - id: pixel_camera_stage_summary + type: atomic + name: "Pixel Camera Stage 摘要" + display: + layer: list + level: detail + title: "可选 Pixel Camera stage" + columns: + - name: cam_id + label: "Camera ID" + type: number + - name: node + label: "Node" + type: string + - name: port_group + label: "Port group" + type: string + - name: frame_count + label: "帧数" + type: number + - name: avg_duration_ns + label: "平均时长" + type: duration + unit: ns + format: duration_ms + - name: max_duration_ns + label: "最大时长" + type: duration + unit: ns + format: duration_ms + - name: source + label: "来源" + type: string + - name: limitation + label: "限制" + type: string + format: truncate + sql: | + WITH + raw_input AS ( + SELECT + COALESCE(${start_ts}, trace_start()) AS raw_start_ts, + COALESCE(${end_ts}, trace_end()) AS raw_end_ts, + MIN(MAX(COALESCE(${max_rows|20}, 20), 1), 100) AS max_rows + ), + input AS ( + SELECT + MIN(raw_start_ts, raw_end_ts) AS start_ts, + MAX(raw_start_ts, raw_end_ts) AS end_ts, + max_rows + FROM raw_input + ) + SELECT + cam_id, + COALESCE(node, '') AS node, + COALESCE(port_group, '') AS port_group, + COUNT(*) AS frame_count, + CAST(ROUND(AVG(dur)) AS INTEGER) AS avg_duration_ns, + MAX(dur) AS max_duration_ns, + 'pixel_camera_frames' AS source, + 'pixel.camera is an optional Pixel slice parser; stage rows require vendor-specific interpretation and portable identity checks.' AS limitation + FROM pixel_camera_frames, input + WHERE ts >= input.start_ts + AND ts < input.end_ts + GROUP BY cam_id, node, port_group + ORDER BY max_duration_ns DESC, frame_count DESC + LIMIT (SELECT max_rows FROM input) + save_as: pixel_camera_stage_summary + synthesize: + role: list + fields: + - key: cam_id + label: Camera ID + - key: node + label: Node + - key: port_group + label: Port group + - key: frame_count + label: 帧数 + - key: source + label: 来源 + +output: + format: structured diff --git a/backend/skills/composite/irq_analysis.skill.yaml b/backend/skills/composite/irq_analysis.skill.yaml index 5e91197b9..dde0ef5af 100644 --- a/backend/skills/composite/irq_analysis.skill.yaml +++ b/backend/skills/composite/irq_analysis.skill.yaml @@ -7,13 +7,8 @@ # ============================================================================= # 系统级分析:硬中断和软中断的频率、耗时、嵌套情况 # -# 使用 Perfetto stdlib linux.cpu.irq 模块: -# - linux_cpu_irq_stats: IRQ 统计聚合视图 -# - linux_cpu_hard_irq: 硬件中断事件 -# - linux_cpu_soft_irq: 软中断事件 -# -# 回退机制: -# - 如果 stdlib 视图不可用,使用 raw slice 数据 (name LIKE 'irq/%' / 'softirq/%') +# 使用 Perfetto stdlib linux.irqs 模块的 linux_irqs 统一视图,避免依赖 +# trace processor 内部 IRQ slice 命名格式。 # # 分析维度: # - L0: IRQ 数据存在性检查 @@ -49,6 +44,7 @@ prerequisites: required_tables: - slice modules: + - linux.irqs inputs: - name: package @@ -92,11 +88,7 @@ steps: display: false sql: | SELECT - CASE WHEN EXISTS ( - SELECT 1 FROM slice - WHERE name LIKE 'irq/%' OR name LIKE 'softirq/%' - OR name LIKE 'irq_handler_%' OR name LIKE 'softirq_' - ) THEN 1 ELSE 0 END as has_irq_data + CASE WHEN EXISTS (SELECT 1 FROM linux_irqs) THEN 1 ELSE 0 END as has_irq_data save_as: data_check # ========== L1: 中断类型概览 ========== @@ -150,19 +142,13 @@ steps: format: compact sql: | SELECT - CASE - WHEN name LIKE 'irq/%' OR name LIKE 'irq_handler_%' THEN 'Hard IRQ' - WHEN name LIKE 'softirq/%' OR name LIKE 'softirq_%' THEN 'Soft IRQ' - ELSE 'Unknown' - END AS irq_type, + CASE WHEN is_soft_irq = 1 THEN 'Soft IRQ' ELSE 'Hard IRQ' END AS irq_type, COUNT(*) AS irq_count, ROUND(SUM(dur) / 1e6, 2) AS total_dur_ms, ROUND(AVG(dur) / 1e3, 2) AS avg_dur_us, ROUND(MAX(dur) / 1e3, 2) AS max_dur_us - FROM slice - WHERE (name LIKE 'irq/%' OR name LIKE 'softirq/%' - OR name LIKE 'irq_handler_%' OR name LIKE 'softirq_%') - AND (${start_ts} IS NULL OR ts >= ${start_ts}) + FROM linux_irqs + WHERE (${start_ts} IS NULL OR ts >= ${start_ts}) AND (${end_ts} IS NULL OR ts < ${end_ts}) GROUP BY irq_type ORDER BY total_dur_ms DESC @@ -223,18 +209,13 @@ steps: WITH irq_stats AS ( SELECT name as irq_name, - CASE - WHEN name LIKE 'irq/%' OR name LIKE 'irq_handler_%' THEN 'Hard IRQ' - ELSE 'Soft IRQ' - END as irq_type, + CASE WHEN is_soft_irq = 1 THEN 'Soft IRQ' ELSE 'Hard IRQ' END as irq_type, COUNT(*) as irq_count, SUM(dur) as total_dur_ns, AVG(dur) as avg_dur_ns, MAX(dur) as max_dur_ns - FROM slice - WHERE (name LIKE 'irq/%' OR name LIKE 'softirq/%' - OR name LIKE 'irq_handler_%' OR name LIKE 'softirq_%') - AND (${start_ts} IS NULL OR ts >= ${start_ts}) + FROM linux_irqs + WHERE (${start_ts} IS NULL OR ts >= ${start_ts}) AND (${end_ts} IS NULL OR ts < ${end_ts}) GROUP BY name ), @@ -301,8 +282,8 @@ steps: WHEN MAX(dur) / 1e3 > ${hard_irq_long_threshold_us|1000} / 10 THEN 'notice' ELSE 'normal' END as severity - FROM slice - WHERE (name LIKE 'irq/%' OR name LIKE 'irq_handler_%') + FROM linux_irqs + WHERE is_soft_irq = 0 AND (${start_ts} IS NULL OR ts >= ${start_ts}) AND (${end_ts} IS NULL OR ts < ${end_ts}) GROUP BY name @@ -357,8 +338,8 @@ steps: WHEN MAX(dur) / 1e3 > ${soft_irq_long_threshold_us|10000} / 10 THEN 'notice' ELSE 'normal' END as severity - FROM slice - WHERE (name LIKE 'softirq/%' OR name LIKE 'softirq_%') + FROM linux_irqs + WHERE is_soft_irq = 1 AND (${start_ts} IS NULL OR ts >= ${start_ts}) AND (${end_ts} IS NULL OR ts < ${end_ts}) GROUP BY name @@ -414,41 +395,36 @@ steps: type: string sql: | SELECT - printf('%d', s.ts) as ts, - s.name as irq_name, + printf('%d', li.ts) as ts, + li.name as irq_name, + CASE WHEN li.is_soft_irq = 1 THEN 'Soft IRQ' ELSE 'Hard IRQ' END AS irq_type, + ROUND(li.dur / 1e3, 2) AS dur_us, + extract_arg(t.dimension_arg_set_id, 'cpu') as cpu, CASE - WHEN s.name LIKE 'irq/%' OR s.name LIKE 'irq_handler_%' THEN 'Hard IRQ' - ELSE 'Soft IRQ' - END AS irq_type, - ROUND(s.dur / 1e3, 2) AS dur_us, - t.cpu as cpu, - CASE - WHEN s.name LIKE 'irq/%' OR s.name LIKE 'irq_handler_%' THEN + WHEN li.is_soft_irq = 0 THEN CASE - WHEN s.dur / 1e3 > ${hard_irq_long_threshold_us|1000} * 5 THEN 'critical' - WHEN s.dur / 1e3 > ${hard_irq_long_threshold_us|1000} THEN 'warning' + WHEN li.dur / 1e3 > ${hard_irq_long_threshold_us|1000} * 5 THEN 'critical' + WHEN li.dur / 1e3 > ${hard_irq_long_threshold_us|1000} THEN 'warning' ELSE 'notice' END ELSE CASE - WHEN s.dur / 1e3 > ${soft_irq_long_threshold_us|10000} * 5 THEN 'critical' - WHEN s.dur / 1e3 > ${soft_irq_long_threshold_us|10000} THEN 'warning' + WHEN li.dur / 1e3 > ${soft_irq_long_threshold_us|10000} * 5 THEN 'critical' + WHEN li.dur / 1e3 > ${soft_irq_long_threshold_us|10000} THEN 'warning' ELSE 'notice' END END as severity - FROM slice s - LEFT JOIN thread_track tt ON s.track_id = tt.id - LEFT JOIN thread t ON tt.utid = t.utid - WHERE (s.name LIKE 'irq/%' OR s.name LIKE 'softirq/%' - OR s.name LIKE 'irq_handler_%' OR s.name LIKE 'softirq_%') - AND (${start_ts} IS NULL OR s.ts >= ${start_ts}) - AND (${end_ts} IS NULL OR s.ts < ${end_ts}) + FROM linux_irqs li + JOIN slice s ON s.id = li.id + JOIN track t ON t.id = s.track_id + WHERE (${start_ts} IS NULL OR li.ts >= ${start_ts}) + AND (${end_ts} IS NULL OR li.ts < ${end_ts}) -- 硬中断 > threshold 或 软中断 > threshold AND ( - ((s.name LIKE 'irq/%' OR s.name LIKE 'irq_handler_%') AND s.dur > ${hard_irq_long_threshold_us|1000} * 1000) - OR ((s.name LIKE 'softirq/%' OR s.name LIKE 'softirq_%') AND s.dur > ${soft_irq_long_threshold_us|10000} * 1000) + (li.is_soft_irq = 0 AND li.dur > ${hard_irq_long_threshold_us|1000} * 1000) + OR (li.is_soft_irq = 1 AND li.dur > ${soft_irq_long_threshold_us|10000} * 1000) ) - ORDER BY s.dur DESC + ORDER BY li.dur DESC LIMIT 30 save_as: long_irq_events optional: true @@ -490,26 +466,18 @@ steps: format: compact sql: | WITH time_base AS ( - SELECT MIN(ts) as base_ts FROM slice - WHERE name LIKE 'irq/%' OR name LIKE 'softirq/%' - OR name LIKE 'irq_handler_%' OR name LIKE 'softirq_%' + SELECT MIN(ts) as base_ts FROM linux_irqs ) SELECT - CAST((s.ts - (SELECT base_ts FROM time_base)) / 1e9 AS INT) AS second, - SUM(CASE WHEN s.name LIKE 'irq/%' OR s.name LIKE 'irq_handler_%' - THEN 1 ELSE 0 END) AS hard_irq_count, - SUM(CASE WHEN s.name LIKE 'softirq/%' OR s.name LIKE 'softirq_%' - THEN 1 ELSE 0 END) AS soft_irq_count, - ROUND(SUM(CASE WHEN s.name LIKE 'irq/%' OR s.name LIKE 'irq_handler_%' - THEN s.dur ELSE 0 END) / 1e6, 2) AS hard_irq_dur_ms, - ROUND(SUM(CASE WHEN s.name LIKE 'softirq/%' OR s.name LIKE 'softirq_%' - THEN s.dur ELSE 0 END) / 1e6, 2) AS soft_irq_dur_ms, + CAST((ts - (SELECT base_ts FROM time_base)) / 1e9 AS INT) AS second, + SUM(CASE WHEN is_soft_irq = 0 THEN 1 ELSE 0 END) AS hard_irq_count, + SUM(CASE WHEN is_soft_irq = 1 THEN 1 ELSE 0 END) AS soft_irq_count, + ROUND(SUM(CASE WHEN is_soft_irq = 0 THEN dur ELSE 0 END) / 1e6, 2) AS hard_irq_dur_ms, + ROUND(SUM(CASE WHEN is_soft_irq = 1 THEN dur ELSE 0 END) / 1e6, 2) AS soft_irq_dur_ms, COUNT(*) as total_irq_count - FROM slice s - WHERE (s.name LIKE 'irq/%' OR s.name LIKE 'softirq/%' - OR s.name LIKE 'irq_handler_%' OR s.name LIKE 'softirq_%') - AND (${start_ts} IS NULL OR s.ts >= ${start_ts}) - AND (${end_ts} IS NULL OR s.ts < ${end_ts}) + FROM linux_irqs + WHERE (${start_ts} IS NULL OR ts >= ${start_ts}) + AND (${end_ts} IS NULL OR ts < ${end_ts}) GROUP BY second ORDER BY second LIMIT 120 @@ -568,35 +536,31 @@ steps: SELECT COUNT(*) as total_count, SUM(dur) / 1e6 as total_dur_ms, - SUM(CASE WHEN (name LIKE 'irq/%' OR name LIKE 'irq_handler_%') THEN 1 ELSE 0 END) as hard_count, - SUM(CASE WHEN (name LIKE 'softirq/%' OR name LIKE 'softirq_%') THEN 1 ELSE 0 END) as soft_count - FROM slice - WHERE (name LIKE 'irq/%' OR name LIKE 'softirq/%' - OR name LIKE 'irq_handler_%' OR name LIKE 'softirq_%') - AND (${start_ts} IS NULL OR ts >= ${start_ts}) + SUM(CASE WHEN is_soft_irq = 0 THEN 1 ELSE 0 END) as hard_count, + SUM(CASE WHEN is_soft_irq = 1 THEN 1 ELSE 0 END) as soft_count + FROM linux_irqs + WHERE (${start_ts} IS NULL OR ts >= ${start_ts}) AND (${end_ts} IS NULL OR ts < ${end_ts}) ), long_hard AS ( SELECT COUNT(*) as cnt - FROM slice - WHERE (name LIKE 'irq/%' OR name LIKE 'irq_handler_%') + FROM linux_irqs + WHERE is_soft_irq = 0 AND dur > ${hard_irq_long_threshold_us|1000} * 1000 AND (${start_ts} IS NULL OR ts >= ${start_ts}) AND (${end_ts} IS NULL OR ts < ${end_ts}) ), long_soft AS ( SELECT COUNT(*) as cnt - FROM slice - WHERE (name LIKE 'softirq/%' OR name LIKE 'softirq_%') + FROM linux_irqs + WHERE is_soft_irq = 1 AND dur > ${soft_irq_long_threshold_us|10000} * 1000 AND (${start_ts} IS NULL OR ts >= ${start_ts}) AND (${end_ts} IS NULL OR ts < ${end_ts}) ), trace_duration AS ( SELECT (MAX(ts) - MIN(ts)) / 1e9 as trace_sec - FROM slice - WHERE (name LIKE 'irq/%' OR name LIKE 'softirq/%' - OR name LIKE 'irq_handler_%' OR name LIKE 'softirq_%') + FROM linux_irqs ) SELECT CASE diff --git a/backend/skills/composite/state_timeline.skill.yaml b/backend/skills/composite/state_timeline.skill.yaml index 9c29999e1..f6beb46ab 100644 --- a/backend/skills/composite/state_timeline.skill.yaml +++ b/backend/skills/composite/state_timeline.skill.yaml @@ -187,6 +187,7 @@ steps: type: atomic name: "设备状态泳道(降级)" condition: timeline_bounds.data[0]?.has_screen_state !== 1 + optional: true display: level: detail layer: list @@ -723,6 +724,7 @@ steps: type: atomic name: "应用状态泳道(降级)" condition: timeline_bounds.data[0]?.has_battery_top !== 1 + optional: true display: level: detail layer: list @@ -1018,4 +1020,4 @@ steps: output: format: layered - default_expanded: [device_state_lane, device_state_lane_fallback, input_state_lane_frames, input_state_lane_fallback, app_state_lane, app_state_lane_fallback, system_state_lane] \ No newline at end of file + default_expanded: [device_state_lane, device_state_lane_fallback, input_state_lane_frames, input_state_lane_fallback, app_state_lane, app_state_lane_fallback, system_state_lane] diff --git a/backend/skills/pipelines/android_view_mixed.skill.yaml b/backend/skills/pipelines/android_view_mixed.skill.yaml index 8a1d456ce..4a95675c8 100644 --- a/backend/skills/pipelines/android_view_mixed.skill.yaml +++ b/backend/skills/pipelines/android_view_mixed.skill.yaml @@ -28,7 +28,7 @@ meta: optional_producer_threads: ["GLThread", "MediaCodec*", "*Decoder*", "EGLThread"] expected_layer_count: 2 # 宿主 + 独立 SurfaceView layer(最少);TextureView 路径仍是 1 层 bufferqueue_path: "BBQ_TRANSACTION_INPROC + INDEPENDENT_LAYER" # 多条并行 - extra_rhythm_sources: ["video_codec_pacing", "camera_sensor_trigger"] # 视情况 + extra_rhythm_sources: ["video_codec_pacing", "camera_request_activity"] # 视情况;Camera 名称仅为 request activity candidate deviation_anchors: "multi_path_anchor_4_5_6_8" # 锚点偏离:宿主走完整 ④⑤⑥;独立 Producer 也各自走 ④⑤⑥(独立节奏); # ⑧ SF latch 这一轮处理多 layer——文章 §"多 Layer 场景里,SF 会对没有新 buffer 的 Layer 沿用旧内容"。 diff --git a/backend/skills/pipelines/camera_pipeline.skill.yaml b/backend/skills/pipelines/camera_pipeline.skill.yaml index 9adca73f5..02302e3aa 100644 --- a/backend/skills/pipelines/camera_pipeline.skill.yaml +++ b/backend/skills/pipelines/camera_pipeline.skill.yaml @@ -24,13 +24,14 @@ meta: # ----- Article-grounded signature (S11) ----- s_article_ref: "S11" four_features: + # 线程 / slice 名称只是 vendor-dependent candidate inventory,不是 request/result/buffer 生命周期证明。 producer_threads: ["cameraserver", "camera HAL", "Camera*", "RequestThread", "CaptureSession"] optional_consumer_threads: ["RenderThread", "ImageReader", "media.codec"] expected_layer_count: 1 # 预览独立 layer(SurfaceView 模式)或 0(TextureView 模式回宿主) bufferqueue_path: "HAL_PRODUCED_TO_PREVIEW_SURFACE_OR_IMAGEREADER" - extra_rhythm_sources: ["sensor_exposure_capture_rhythm"] - deviation_anchors: "no_vsync_app_hardware_sensor_trigger" - # 锚点偏离:① 没有 vsync-app(sensor 采集节奏由 capture request 参数决定); + extra_rhythm_sources: ["camera_request_activity"] + deviation_anchors: "no_vsync_app_camera_request_activity_candidate" + # 锚点偏离:① 候选 request activity 不跟随 vsync-app;slice 名不能证明 sensor trigger / exposure; # ② 宿主 UI Choreographer callback 仅管控制层,不生产预览像素; # ④⑤⑥ 由 Camera HAL 完成,App 只配置 Surface。 subvariants_note: | @@ -41,9 +42,9 @@ meta: - CAMERA_VIDEORECORD_SURFACE(录像 Surface) Phase E 拆分独立 ID。当前 ID 一并覆盖。 multi_output_back_pressure: | - Camera 多路输出(preview/analysis/record)共享同一 capture pipeline—— - 任一路消费慢会 back-pressure 整段 pipeline,导致预览看似"卡"实际是被某路拖慢。 - 分析时必须分开看 3 路。 + Camera 多路输出(preview/analysis/record)可能共享硬件与 buffer 资源。 + 只有存在 buffer ownership、acquire/release 或 fence 等证据时,才能把某一路消费慢报告为 + 整体 pipeline back-pressure;仅凭 PSS/RSS 增长不能判定 ImageReader 泄漏。 detection: scoring_signals: @@ -64,9 +65,15 @@ teaching: title: "相机渲染管线" summary: | Camera2 API 和 HAL3 相机架构,支持多流同时处理(预览、拍照、录像)。 - 涉及相机硬件、ISP、编码器等多个组件的协同工作。 - 需要关注帧率稳定性和延迟。Camera trace 的可见性强依赖设备、 - OEM 和启用的数据源,不应把缺失某个 `Camera*` slice 直接等价为“没有相机压力”。 + Request 按序提交,但可有多个 request 同时 in-flight;每个 request 可产生 partial metadata + 和一个或多个输出 buffer。`onOpened` 只证明设备打开完成,不证明首个 result、buffer 或预览呈现。 + 首帧需要稳定的 session/camera identity,以及明确的 request/result/buffer/presentation 锚点; + vendor slice-name inventory 只能作为 candidate,不能单独证明首帧。首帧到达时 3A 仍可能搜索, + 有 trace 证据时应单独报告 3A 状态。`prepare()` 是 buffer 预分配取舍,可能推迟首次输出并增加内存, + 不是通用首帧修复。ZSL 行为和 buffer topology 取决于 capability、实现与 App 配置。 + ImageReader backpressure 只有在 buffer ownership/acquire-release 证据存在时才是受支持的假设; + PSS/RSS 增长本身不足以判定泄漏。Pixel `pixel.camera` 解析只是可选 vendor fast path, + 不是可移植的 Android Camera contract。Camera trace 可见性强依赖设备、OEM 和启用的数据源。 mermaid: | sequenceDiagram @@ -109,16 +116,16 @@ teaching: key_slices: - name: "Camera" thread: any - description: "相机相关操作(是否可见依设备 / tracing 配置)" + description: "相机相关操作候选(vendor 名称不能单独证明 request/result/buffer 阶段)" - name: "CaptureSession" thread: any - description: "采集会话(名称与可见性依版本 / OEM 变化)" + description: "采集会话活动候选(名称与可见性依版本 / OEM 变化)" - name: "createCaptureSession" thread: any - description: "创建采集会话(常见于启动/切换摄像头/重配流)" + description: "创建采集会话候选(常见于启动/切换摄像头/重配流)" - name: "setRepeatingRequest" thread: any - description: "预览/录制的重复请求(影响吞吐与延迟)" + description: "预览/录制重复 request activity candidate(不能据此推断 sensor trigger)" auto_pin: instructions: @@ -126,7 +133,7 @@ auto_pin: - pattern: "^VSYNC-app$" match_by: name priority: 1 - reason: "VSync (预览开始生产帧)" + reason: "VSync (宿主 UI/控制层节奏,不证明相机像素生产)" - pattern: "^main(\\s+\\d+)?$" match_by: name @@ -183,9 +190,9 @@ analysis: - id: multi_output_back_pressure name: "多路输出 back-pressure 拖慢预览" description: | - Camera 多路输出(preview/analysis/record)共享同一 capture pipeline。 - 任一路消费慢(典型如 ImageReader 不及时 release / Codec 堆积)会 back-pressure HAL, - 让预览看似卡顿。分析时必须分别看三路输出谁在回压,不能只盯 HAL 慢。 + Camera 多路输出(preview/analysis/record)可能竞争共享硬件与 buffer 资源。 + ImageReader 未及时 release / Codec 堆积导致 HAL back-pressure 只能作为候选解释; + 需要 buffer ownership、acquire/release 或 fence 证据支持,不能仅凭 PSS/RSS 增长判定泄漏。 detection_skill: gpu_render_in_range - id: release_fence_late_blocks_hal @@ -198,15 +205,16 @@ analysis: - id: textureview_extra_resample_overhead name: "TextureView 模式宿主 RT 采样开销" description: | - 预览承载若是 TextureView,宿主 RT updateTexImage acquire + GPU 重采样会引入额外延迟(通常 +1 帧)。 - SurfaceView 模式可走 device composition 避免此开销。 + 预览承载若是 TextureView,宿主 RT updateTexImage acquire + GPU 重采样可能引入额外延迟。 + SurfaceView 模式在设备与合成条件允许时可走 device composition 避免此开销。 detection_skill: render_thread_slices - - id: sensor_trigger_not_vsync_aligned - name: "sensor 采集节奏不对齐 vsync" + - id: camera_request_activity_not_vsync_aligned + name: "Camera request activity candidate 不对齐 vsync" description: | Camera 帧率不等于显示刷新率(典型 30fps 视频拍摄在 60Hz 屏)。 - 分析卡顿要分别看 capture frame rate(HAL processCaptureResult 频率)vs preview display frame rate(SF latch 频率)。 + `processCaptureRequest*` 等名称匹配只能标记 Camera request activity candidate,不能证明 sensor trigger。 + 分析卡顿要分别看有明确 identity 的 request/result/buffer 证据与 preview presentation 频率。 detection_skill: vsync_alignment_in_range recommended_skills: diff --git a/backend/skills/pipelines/surfaceview_blast.skill.yaml b/backend/skills/pipelines/surfaceview_blast.skill.yaml index 849a3eb3b..b8a05aea2 100644 --- a/backend/skills/pipelines/surfaceview_blast.skill.yaml +++ b/backend/skills/pipelines/surfaceview_blast.skill.yaml @@ -29,7 +29,7 @@ meta: # ↑ 独立 Producer 可能在跨进程:MediaCodec/Codec2/Camera HAL 进程;trace 时不能只盯主 App 进程 expected_layer_count: 2 # 宿主 + 独立 SurfaceView layer(Android 12+ BLAST 路径) bufferqueue_path: "BBQ_TRANSACTION_INPROC + INDEPENDENT_LAYER" - extra_rhythm_sources: ["video_codec_pacing", "camera_sensor_trigger", "game_engine_loop"] + extra_rhythm_sources: ["video_codec_pacing", "camera_request_activity", "game_engine_loop"] deviation_anchors: "independent_anchor_4_5_6_join_at_8" # 锚点偏离:宿主侧仍走完整 ①→⑥;独立 Producer 走自己的 ④⑤⑥; # ⑧ SF 在同一轮 latch 多 layer(宿主 + 独立);⑨→⑫ 与基线一致。 diff --git a/backend/skills/public-export.yaml b/backend/skills/public-export.yaml index ed05adff6..7f85ce7f1 100644 --- a/backend/skills/public-export.yaml +++ b/backend/skills/public-export.yaml @@ -1090,6 +1090,11 @@ skills: workflow: rendering-pipeline disposition: exported destination: references/generated/skills/pipeline_angle_gles_vulkan.md + camera_trace_evidence: + source: backend/skills/composite/camera_trace_evidence.skill.yaml + workflow: rendering-pipeline + disposition: exported + destination: references/generated/skills/camera_trace_evidence.md pipeline_camera_pipeline: source: backend/skills/pipelines/camera_pipeline.skill.yaml workflow: rendering-pipeline diff --git a/backend/skills/public-fixtures.yaml b/backend/skills/public-fixtures.yaml index 701f61e6c..472825132 100644 --- a/backend/skills/public-fixtures.yaml +++ b/backend/skills/public-fixtures.yaml @@ -3,7 +3,7 @@ schema_version: 1 fixtures: - id: smart-launch-light-api36 - source: test-traces/launch_light.pftrace + source: Trace/real/android-startup-light/trace.pftrace sha256: 6c5479fd1b765ee4d29692c43a8204b972bc0f97eb373aca98ea7e11e99fd8b4 license: AGPL-3.0-or-later android: {api: 36, qpr: unknown, oem: Google, build_type: user} @@ -12,7 +12,7 @@ fixtures: assertions: - {query_id: startup_slow_reasons/startup_overview, kind: non_empty, field: startup_type} - id: smart-launch-heavy-api36 - source: test-traces/lacunh_heavy.pftrace + source: Trace/real/android-startup-heavy/trace.pftrace sha256: 2a0c7b85e7f4a14e43b7f8d6de21172e05eba1ed3ad8ef7df539bb5e59b6ba63 license: AGPL-3.0-or-later android: {api: 36, qpr: unknown, oem: Xiaomi, build_type: user} @@ -20,7 +20,7 @@ fixtures: capture_config: embedded_in_trace assertions: [] - id: smart-aosp-scroll-api35 - source: test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace + source: Trace/real/android-scroll-standard/trace.pftrace sha256: df7eebc5c6ff19b48331f8f1bca346612d86a5ae26eae202d46842a83f87a653 license: AGPL-3.0-or-later android: {api: 35, qpr: unknown, oem: Nubia, build_type: user} @@ -29,7 +29,7 @@ fixtures: assertions: - {query_id: scrolling_analysis/frame_timeline_check, kind: field_equals, field: has_frame_timeline, value: 1} - id: smart-flutter-texture-api35 - source: test-traces/Scroll-Flutter-327-TextureView.pftrace + source: Trace/real/flutter-scroll-texture-view/trace.pftrace sha256: 645deaa9fc564e0ea33072c83f5ecf1d0a6f53a97055cc98c640e8294deb315e license: AGPL-3.0-or-later android: {api: 35, qpr: unknown, oem: Nubia, build_type: user} @@ -39,7 +39,7 @@ fixtures: - {query_id: rendering_pipeline_detection/thread_signals, kind: field_positive, field: flutter_ui_count, params: {package: com.example.friendscircle.v27.textureview}} - {query_id: rendering_pipeline_detection/slice_signals, kind: field_positive, field: texture_view_count, params: {package: com.example.friendscircle.v27.textureview}} - id: smart-flutter-surface-api35 - source: test-traces/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace + source: Trace/real/flutter-scroll-surface-view/trace.pftrace sha256: e876c56b5d7290acda596e73929c1a127df4255e9ca6951605dad40833e67a53 license: AGPL-3.0-or-later android: {api: 35, qpr: unknown, oem: Nubia, build_type: user} @@ -47,7 +47,7 @@ fixtures: capture_config: embedded_in_trace assertions: [] - id: smart-oppo-scroll-api36 - source: test-traces/scroll-demo-customer-scroll.pftrace + source: Trace/real/android-scroll-customer/trace.pftrace sha256: 1015ba963c49cd57d08d8997c4c48027026f2d6e41f7eb72b4b1356e39f7af3a license: AGPL-3.0-or-later android: {api: 36, qpr: unknown, oem: OPPO, build_type: user} diff --git a/backend/src/agentRuntime/__tests__/quickTraceFactEvidence.test.ts b/backend/src/agentRuntime/__tests__/quickTraceFactEvidence.test.ts index 9adf0a01e..dc39123e2 100644 --- a/backend/src/agentRuntime/__tests__/quickTraceFactEvidence.test.ts +++ b/backend/src/agentRuntime/__tests__/quickTraceFactEvidence.test.ts @@ -22,12 +22,13 @@ import { buildQuickTraceFactDirectAnswer } from '../quickTraceFactDirectAnswer'; import type { DataEnvelope } from '../../types/dataContract'; import { runClaimVerification } from '../../services/verifier/claimVerificationRunner'; import { getTraceProcessorPath } from '../../services/workingTraceProcessor'; +import { resolveTraceCase } from '../../utils/traceCorpus'; type QueryTrace = QuickTraceFactEvidenceInput['traceProcessor']['query']; const execFileAsync = promisify(execFile); const repoRoot = path.resolve(__dirname, '../../../..'); -const launchLightTracePath = path.join(repoRoot, 'test-traces', 'launch_light.pftrace'); +const launchLightTracePath = resolveTraceCase('launch_light.pftrace', repoRoot); const traceProcessorPath = getTraceProcessorPath(); const itWithLaunchLightTraceProcessor = fs.existsSync(traceProcessorPath) && fs.existsSync(launchLightTracePath) ? it diff --git a/backend/src/cli-user/bin.ts b/backend/src/cli-user/bin.ts index 3405c4917..af1237dea 100644 --- a/backend/src/cli-user/bin.ts +++ b/backend/src/cli-user/bin.ts @@ -605,7 +605,7 @@ function main(): void { captureCmd .command('android') .description('capture an Android Perfetto trace from a connected adb device') - .option('--preset ', 'capture preset: startup, scrolling, anr, game, memory, cpu, power, overview, full', parseCapturePreset) + .option('--preset ', 'capture preset: startup, scrolling, camera, anr, game, memory, cpu, power, overview, full', parseCapturePreset) .option('--config ', 'existing Perfetto textproto config file') .option('--app ', 'target Android package name or *') .requiredOption('--out ', 'output trace file path') @@ -766,7 +766,7 @@ function parseAnalysisMode(mode: string | undefined): CliAnalysisMode | undefine function parseCapturePreset(preset: string): CapturePresetId { if (isCapturePresetId(preset)) return preset; - throw new Error(`Invalid capture preset: ${preset}. Expected startup, scrolling, anr, game, memory, cpu, power, overview, or full.`); + throw new Error(`Invalid capture preset: ${preset}. Expected startup, scrolling, camera, anr, game, memory, cpu, power, overview, or full.`); } function parseCodebaseKind(kind: string | undefined): 'app_source' | 'aosp' | 'kernel_source' | 'oem_sdk' { diff --git a/backend/src/cli-user/commands/__tests__/captureSuggest.test.ts b/backend/src/cli-user/commands/__tests__/captureSuggest.test.ts index bfdd3b724..82e59f0d4 100644 --- a/backend/src/cli-user/commands/__tests__/captureSuggest.test.ts +++ b/backend/src/cli-user/commands/__tests__/captureSuggest.test.ts @@ -46,6 +46,22 @@ describe('runCaptureSuggestCommand', () => { }); }); + it('selects the Camera preset for Camera-domain requests', async () => { + const exitCode = await runCaptureSuggestCommand({ + request: '分析 Camera 打开到首帧预览延迟', + app: 'com.example.camera', + format: 'json', + }); + + expect(exitCode).toBe(0); + const payload = JSON.parse(String(consoleLogSpy.mock.calls[0]?.[0] ?? '{}')); + expect(payload.proposal).toMatchObject({ + preset: 'camera', + intent: 'camera', + app: 'com.example.camera', + }); + }); + it('keeps text output side-effect free and includes the config preview', async () => { const exitCode = await runCaptureSuggestCommand({ request: 'scrolling frame drops', diff --git a/backend/src/cli-user/services/__tests__/captureConfig.test.ts b/backend/src/cli-user/services/__tests__/captureConfig.test.ts index 2445da667..1c43a3f8d 100644 --- a/backend/src/cli-user/services/__tests__/captureConfig.test.ts +++ b/backend/src/cli-user/services/__tests__/captureConfig.test.ts @@ -17,6 +17,20 @@ import { } from '../captureConfig'; describe('capture config rendering', () => { + it('exposes and renders the shared Camera preset', () => { + expect(listCapturePresets().map(preset => preset.id)).toContain('camera'); + + const config = renderAndroidTraceConfig({ + target: 'android', + preset: 'camera', + app: 'com.example.camera', + durationSeconds: 20, + }); + + expect(config).toContain('SmartPerfetto capture preset: camera'); + expect(config).toContain('ftrace_events: "dmabuf_heap/dma_heap_stat"'); + }); + it('renders every built-in Android preset as a textproto config', () => { for (const preset of listCapturePresets()) { const config = renderAndroidTraceConfig({ diff --git a/backend/src/scripts/benchmarkTraceProcessorRss.ts b/backend/src/scripts/benchmarkTraceProcessorRss.ts index 94669ccf5..0c06f8e72 100644 --- a/backend/src/scripts/benchmarkTraceProcessorRss.ts +++ b/backend/src/scripts/benchmarkTraceProcessorRss.ts @@ -159,32 +159,32 @@ const DEFAULT_TRACE_CANDIDATES: TraceBenchmarkSpec[] = [ { scene: 'startup', label: 'launch-heavy-local', - path: '../test-traces/lacunh_heavy.pftrace', + path: '../Trace/real/android-startup-heavy/trace.pftrace', }, { scene: 'startup', label: 'launch-light-local', - path: '../test-traces/launch_light.pftrace', + path: '../Trace/real/android-startup-light/trace.pftrace', }, { scene: 'scroll', label: 'scroll-standard-local', - path: '../test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace', + path: '../Trace/real/android-scroll-standard/trace.pftrace', }, { scene: 'scroll', label: 'scroll-customer-local', - path: '../test-traces/scroll-demo-customer-scroll.pftrace', + path: '../Trace/real/android-scroll-customer/trace.pftrace', }, { scene: 'scroll', label: 'flutter-textureview-local', - path: '../test-traces/Scroll-Flutter-327-TextureView.pftrace', + path: '../Trace/real/flutter-scroll-texture-view/trace.pftrace', }, { scene: 'scroll', label: 'flutter-surfaceview-local', - path: '../test-traces/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace', + path: '../Trace/real/flutter-scroll-surface-view/trace.pftrace', }, ]; diff --git a/backend/src/scripts/diagnoseTrace.ts b/backend/src/scripts/diagnoseTrace.ts index 3eded40d1..298d5649c 100644 --- a/backend/src/scripts/diagnoseTrace.ts +++ b/backend/src/scripts/diagnoseTrace.ts @@ -11,7 +11,7 @@ import { getTraceProcessorService } from '../services/traceProcessorService'; import path from 'path'; async function diagnose() { - const tracePath = path.join(process.cwd(), '../test-traces/scroll-demo-customer-scroll.pftrace'); + const tracePath = path.join(process.cwd(), '../Trace/real/android-scroll-customer/trace.pftrace'); console.log('Trace:', path.basename(tracePath)); console.log('Size:', (require('fs').statSync(tracePath).size / 1024 / 1024).toFixed(2), 'MB\n'); diff --git a/backend/src/scripts/probeStdlibSchema.ts b/backend/src/scripts/probeStdlibSchema.ts index 8c37c7a5a..f02040380 100644 --- a/backend/src/scripts/probeStdlibSchema.ts +++ b/backend/src/scripts/probeStdlibSchema.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { getTraceProcessorService } from '../services/traceProcessorService'; +import { listTraceCases, resolveTraceCase } from '../utils/traceCorpus'; interface ProbeColumn { name: string; @@ -163,14 +164,12 @@ async function probeTrace(filePath: string): Promise { async function main(): Promise { const repoRoot = path.resolve(__dirname, '../../..'); - const traceDir = path.join(repoRoot, 'test-traces'); - const traceFiles = fs - .readdirSync(traceDir) - .filter((file) => file.endsWith('.pftrace')) - .map((file) => path.join(traceDir, file)); + const traceFiles = listTraceCases(repoRoot) + .filter(entry => entry.kind === 'real') + .map(entry => resolveTraceCase(entry.id, repoRoot)); if (traceFiles.length === 0) { - console.error(`No .pftrace files found in ${traceDir}`); + console.error(`No real trace cases found in ${path.join(repoRoot, 'Trace', 'catalog.json')}`); process.exit(1); } diff --git a/backend/src/scripts/runAgentTest.ts b/backend/src/scripts/runAgentTest.ts index 3018d781b..519114f8c 100644 --- a/backend/src/scripts/runAgentTest.ts +++ b/backend/src/scripts/runAgentTest.ts @@ -19,7 +19,7 @@ async function runAgentTest() { const testTracePath = path.join( process.cwd(), - '../test-traces/scroll-demo-customer-scroll.pftrace' + '../Trace/real/android-scroll-customer/trace.pftrace' ); if (!fs.existsSync(testTracePath)) { diff --git a/backend/src/scripts/runScrollingTest.ts b/backend/src/scripts/runScrollingTest.ts index bc50a7e32..a64e12a55 100644 --- a/backend/src/scripts/runScrollingTest.ts +++ b/backend/src/scripts/runScrollingTest.ts @@ -24,7 +24,7 @@ async function runScrollingSkillTest() { // Test trace path const testTracePath = path.join( process.cwd(), - '../test-traces/scroll-demo-customer-scroll.pftrace' + '../Trace/real/android-scroll-customer/trace.pftrace' ); if (!fs.existsSync(testTracePath)) { diff --git a/backend/src/scripts/testDeepLayerFix.ts b/backend/src/scripts/testDeepLayerFix.ts index f075466ea..a282c39a0 100644 --- a/backend/src/scripts/testDeepLayerFix.ts +++ b/backend/src/scripts/testDeepLayerFix.ts @@ -20,14 +20,14 @@ async function testDeepLayerFix() { // Use the scrolling test trace with actual jank data let testTracePath = path.join( process.cwd(), - '../test-traces/scroll-demo-customer-scroll.pftrace' + '../Trace/real/android-scroll-customer/trace.pftrace' ); if (!fs.existsSync(testTracePath)) { // Fallback to the lighter trace const fallbackPath = path.join( process.cwd(), - '../test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace' + '../Trace/real/android-scroll-standard/trace.pftrace' ); if (fs.existsSync(fallbackPath)) { testTracePath = fallbackPath; diff --git a/backend/src/scripts/verifyAgentSseScrolling.ts b/backend/src/scripts/verifyAgentSseScrolling.ts index 0ff98b1af..5237d54c7 100644 --- a/backend/src/scripts/verifyAgentSseScrolling.ts +++ b/backend/src/scripts/verifyAgentSseScrolling.ts @@ -188,14 +188,14 @@ interface SseSummary { skillCallCounts: Record; } -const DEFAULT_TRACE = '../test-traces/scroll-demo-customer-scroll.pftrace'; +const DEFAULT_TRACE = '../Trace/real/android-scroll-customer/trace.pftrace'; const DEFAULT_QUERY = '分析滑动性能'; function printUsage(): void { console.log('Usage: npx tsx src/scripts/verifyAgentSseScrolling.ts [options]'); console.log(''); console.log('Options:'); - console.log(' --trace Trace path (default: ../test-traces/scroll-demo-customer-scroll.pftrace)'); + console.log(' --trace Trace path (default: ../Trace/real/android-scroll-customer/trace.pftrace)'); console.log(' --reference-trace Reference trace path for raw dual-trace comparison'); console.log(' --query Analyze query (default: 分析滑动性能)'); console.log(' --timeout-ms SSE timeout in ms (default: 600000)'); diff --git a/backend/src/scripts/verifyCriticalPathE2E.ts b/backend/src/scripts/verifyCriticalPathE2E.ts index 2e84eb5ba..9a5cc181e 100644 --- a/backend/src/scripts/verifyCriticalPathE2E.ts +++ b/backend/src/scripts/verifyCriticalPathE2E.ts @@ -11,10 +11,11 @@ import * as fs from 'fs'; import * as path from 'path'; import {analyzeCriticalPath} from '../services/criticalPathAnalyzer'; import {getTraceProcessorService} from '../services/traceProcessorService'; +import {resolveTraceCase} from '../utils/traceCorpus'; async function main(): Promise { const repoRoot = path.resolve(__dirname, '../../..'); - const tracePath = path.join(repoRoot, 'test-traces', 'scroll-demo-customer-scroll.pftrace'); + const tracePath = resolveTraceCase('scroll-demo-customer-scroll.pftrace', repoRoot); if (!fs.existsSync(tracePath)) { throw new Error(`fixture missing: ${tracePath}`); } diff --git a/backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts b/backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts index cf6f807e9..2699d5def 100644 --- a/backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts +++ b/backend/src/scripts/verifyEnterpriseMultiTenantWindows.ts @@ -85,17 +85,17 @@ export interface EnterpriseWindowRegressionReport { } const DEFAULT_TRACE_CANDIDATES = [ - '../test-traces/launch_light.pftrace', - 'test-traces/launch_light.pftrace', - '../test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace', - 'test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace', + '../Trace/real/android-startup-light/trace.pftrace', + 'Trace/real/android-startup-light/trace.pftrace', + '../Trace/real/android-scroll-standard/trace.pftrace', + 'Trace/real/android-scroll-standard/trace.pftrace', ]; function printUsage(): void { console.log('Usage: npx tsx src/scripts/verifyEnterpriseMultiTenantWindows.ts [options]'); console.log(''); console.log('Options:'); - console.log(' --trace Fixture trace path. Defaults to launch_light from test-traces.'); + console.log(' --trace Fixture trace path. Defaults to the cataloged android-startup-light case.'); console.log(' --upload-root Temporary upload root. Defaults to an OS temp dir.'); console.log(' --output JSON report path. Defaults to backend/test-output.'); console.log(' --long-sql-ms Simulated long SQL window for D2 (default: 100).'); diff --git a/backend/src/services/__tests__/traceCaptureConfig.test.ts b/backend/src/services/__tests__/traceCaptureConfig.test.ts index 934d389c9..daf7e961b 100644 --- a/backend/src/services/__tests__/traceCaptureConfig.test.ts +++ b/backend/src/services/__tests__/traceCaptureConfig.test.ts @@ -6,12 +6,43 @@ import { describe, expect, it } from '@jest/globals'; import { addAtraceCategories, calculateCaptureBufferSizeKb, + getCapturePreset, listCapturePresets, renderAndroidTraceConfig, renderTraceConfigTemplate, } from '../traceCaptureConfig'; describe('shared trace capture config rendering', () => { + it('renders the Camera preset with binder, FrameTimeline, and DMA-BUF evidence', () => { + const preset = getCapturePreset('camera'); + const config = renderAndroidTraceConfig({ + target: 'android', + preset: 'camera', + app: 'com.example.camera', + durationSeconds: 20, + }); + + expect(preset.intent).toBe('camera'); + expect(config).toContain('atrace_categories: "camera"'); + expect(config).toContain('atrace_categories: "hal"'); + expect(config).toContain('ftrace_events: "dmabuf_heap/dma_heap_stat"'); + expect(config).toContain('ftrace_events: "ion/ion_stat"'); + expect(config).toContain('ftrace_events: "binder/binder_transaction"'); + expect(config).toContain('name: "android.surfaceflinger.frametimeline"'); + }); + + it('keeps Camera memory evidence in the full diagnostic preset', () => { + const config = renderAndroidTraceConfig({ + target: 'android', + preset: 'full', + app: '*', + durationSeconds: 20, + }); + + expect(config).toContain('ftrace_events: "dmabuf_heap/dma_heap_stat"'); + expect(config).toContain('ftrace_events: "ion/ion_stat"'); + }); + it('renders every built-in Android preset through the shared service', () => { for (const preset of listCapturePresets()) { const config = renderAndroidTraceConfig({ diff --git a/backend/src/services/__tests__/traceConfigProposal.test.ts b/backend/src/services/__tests__/traceConfigProposal.test.ts index 53a2246f7..08784eea0 100644 --- a/backend/src/services/__tests__/traceConfigProposal.test.ts +++ b/backend/src/services/__tests__/traceConfigProposal.test.ts @@ -6,6 +6,27 @@ import { describe, expect, it } from '@jest/globals'; import { buildTraceConfigProposal } from '../traceConfigProposal'; describe('buildTraceConfigProposal', () => { + it('routes Camera first-frame requests ahead of generic startup', () => { + const proposal = buildTraceConfigProposal({ + request: '分析 Camera 打开到首帧预览延迟', + app: 'com.example.camera', + outputLanguage: 'zh-CN', + }); + + expect(proposal.preset).toBe('camera'); + expect(proposal.intent).toBe('camera'); + }); + + it('keeps generic app first-frame requests on startup', () => { + expect(buildTraceConfigProposal({ request: 'debug app first frame' }).preset) + .toBe('startup'); + }); + + it('does not treat generic preview requests as Camera-domain requests', () => { + expect(buildTraceConfigProposal({ request: 'debug preview first frame' }).preset) + .toBe('startup'); + }); + it('maps startup requests to the startup preset and shared textproto renderer', () => { const proposal = buildTraceConfigProposal({ request: 'debug cold start first frame jank', diff --git a/backend/src/services/__tests__/traceProcessorService.test.ts b/backend/src/services/__tests__/traceProcessorService.test.ts index bcfa6291b..b9c7b4e18 100644 --- a/backend/src/services/__tests__/traceProcessorService.test.ts +++ b/backend/src/services/__tests__/traceProcessorService.test.ts @@ -34,6 +34,7 @@ import { supportsTraceProcessorCorsOriginsFlag, } from '../workingTraceProcessor'; import { isTraceProcessorQueryCancelledError } from '../traceProcessorCancellation'; +import { listTraceCases, resolveTraceCase } from '../../utils/traceCorpus'; // ============================================================================= // Test Environment Detection @@ -77,9 +78,6 @@ describe('TraceProcessorService cancellation', () => { const TRACE_PROCESSOR_PATH = process.env.TRACE_PROCESSOR_PATH || path.resolve(__dirname, '../../../../perfetto/out/ui/trace_processor_shell'); -// Path to test traces directory -const TEST_TRACES_DIR = path.resolve(__dirname, '../../../../../test-traces'); - // Check if trace_processor_shell is available function isTraceProcessorAvailable(): boolean { return fs.existsSync(TRACE_PROCESSOR_PATH); @@ -87,16 +85,19 @@ function isTraceProcessorAvailable(): boolean { // Check if test traces are available function isTestTracesAvailable(): boolean { - return fs.existsSync(TEST_TRACES_DIR); + try { + return listTraceCases().some(entry => entry.trace.materialization === 'committed'); + } catch { + return false; + } } // Get a test trace file path function getTestTracePath(): string | null { if (!isTestTracesAvailable()) return null; - const files = fs.readdirSync(TEST_TRACES_DIR); - const traceFile = files.find(f => f.endsWith('.pftrace') || f.endsWith('.perfetto-trace')); - return traceFile ? path.join(TEST_TRACES_DIR, traceFile) : null; + const traceCase = listTraceCases().find(entry => entry.trace.materialization === 'committed'); + return traceCase ? resolveTraceCase(traceCase.id) : null; } // ============================================================================= diff --git a/backend/src/services/renderingPipelineDetectionSkillGenerator.ts b/backend/src/services/renderingPipelineDetectionSkillGenerator.ts index 72e7f43d0..d79ce69f3 100644 --- a/backend/src/services/renderingPipelineDetectionSkillGenerator.ts +++ b/backend/src/services/renderingPipelineDetectionSkillGenerator.ts @@ -902,7 +902,7 @@ function buildLayerSignalsSql(): string { * - Swappy/SwappyVk frame pacing * - AChoreographer (NDK Choreographer) * - setFrameRate / setFrameRateCategory APIs - * - Camera HAL sensor trigger + * - Camera request activity candidate (slice-name match; not sensor-trigger proof) * - MediaCodec codec pacing * - Game engine main loop * @@ -982,7 +982,7 @@ function buildExtraRhythmSignalsSql(): string { WHEN COALESCE((SELECT SUM(cnt) FROM app_slices WHERE slice_name GLOB '*Swappy*' OR slice_name GLOB '*SwappyVk_*'), 0) > 0 THEN 'swappy_pacing' WHEN COALESCE((SELECT SUM(cnt) FROM app_slices WHERE slice_name GLOB '*AChoreographer*'), 0) > 0 THEN 'achoreographer' WHEN COALESCE((SELECT SUM(cnt) FROM app_slices WHERE slice_name GLOB '*PlayerLoop*' OR slice_name GLOB '*FEngineLoop*' OR slice_name GLOB '*MainLoop*'), 0) > 0 THEN 'engine_main_loop' - WHEN COALESCE((SELECT SUM(cnt) FROM app_slices WHERE slice_name GLOB '*processCaptureRequest*'), 0) > 0 THEN 'camera_sensor_trigger' + WHEN COALESCE((SELECT SUM(cnt) FROM app_slices WHERE slice_name GLOB '*processCaptureRequest*'), 0) > 0 THEN 'camera_request_activity' WHEN COALESCE((SELECT SUM(cnt) FROM app_slices WHERE slice_name GLOB '*releaseOutputBuffer*'), 0) > 0 THEN 'video_codec_pacing' ELSE 'vsync_app_only' END as primary_rhythm_source diff --git a/backend/src/services/skillEngine/__tests__/cameraTraceEvidenceSchema.test.ts b/backend/src/services/skillEngine/__tests__/cameraTraceEvidenceSchema.test.ts new file mode 100644 index 000000000..528cc212c --- /dev/null +++ b/backend/src/services/skillEngine/__tests__/cameraTraceEvidenceSchema.test.ts @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) +// This file is part of SmartPerfetto. See LICENSE for details. + +import fs from 'fs'; +import path from 'path'; +import {describe, expect, it} from '@jest/globals'; +import Database from 'better-sqlite3'; +import yaml from 'js-yaml'; + +import {SkillRegistry} from '../skillLoader'; + +const skillPath = path.join( + process.cwd(), + 'skills', + 'composite', + 'camera_trace_evidence.skill.yaml', +); +const skill = yaml.load(fs.readFileSync(skillPath, 'utf-8')) as any; + +const EXPECTED_STEP_IDS = [ + 'evidence_coverage', + 'camera_process_candidates', + 'camera_slice_candidates', + 'camera_binder_summary', + 'camera_dmabuf_summary', + 'pixel_camera_stage_summary', +] as const; + +function getStep(id: string): any { + const step = skill.steps?.find((candidate: any) => candidate.id === id); + expect(step).toBeDefined(); + return step; +} + +function expectTypedColumns(stepId: string, expected: Record): void { + const columns = getStep(stepId).display?.columns; + expect(Array.isArray(columns)).toBe(true); + + for (const [name, type] of Object.entries(expected)) { + expect(columns).toEqual(expect.arrayContaining([ + expect.objectContaining({name, type}), + ])); + } +} + +function renderSqlForSqlite(sql: string): string { + return sql + .split('${start_ts}').join('NULL') + .split('${end_ts}').join('NULL') + .split('${max_rows|20}').join('20'); +} + +describe('camera_trace_evidence skill schema', () => { + it('routes only requests with an explicit Camera domain anchor', () => { + const registry = new SkillRegistry(); + const loaded = registry.loadSingleSkill( + path.join(process.cwd(), 'skills'), + 'composite/camera_trace_evidence.skill.yaml', + ); + + expect(loaded?.name).toBe('camera_trace_evidence'); + for (const question of [ + 'debug preview first frame', + 'analyze capture latency', + '分析预览首帧', + 'generic dmabuf memory', + ]) { + expect(registry.findMatchingSkill(question)).toBeUndefined(); + } + for (const question of [ + 'analyze Camera preview latency', + '分析相机首帧预览', + 'inspect cameraserver DMA-BUF', + ]) { + expect(registry.findMatchingSkill(question)?.name).toBe('camera_trace_evidence'); + } + }); + + it('summarizes legacy ION-only Camera allocation deltas', () => { + const database = new Database(':memory:'); + database.function('trace_start', () => 0); + database.function('trace_end', () => 10_000); + database.exec(` + CREATE TABLE android_dmabuf_allocs ( + ts INTEGER, + buf_size INTEGER, + process_name TEXT, + thread_name TEXT, + upid INTEGER, + pid INTEGER, + utid INTEGER, + tid INTEGER + ); + CREATE TABLE counter (ts INTEGER, track_id INTEGER, value INTEGER); + CREATE TABLE thread_counter_track (id INTEGER, name TEXT, utid INTEGER); + CREATE TABLE thread (utid INTEGER, tid INTEGER, upid INTEGER, name TEXT); + CREATE TABLE process (upid INTEGER, pid INTEGER, name TEXT); + + INSERT INTO process VALUES (1, 100, 'cameraserver'); + INSERT INTO thread VALUES (2, 101, 1, 'CameraWorker'); + INSERT INTO thread_counter_track VALUES (3, 'mem.ion_change', 2); + INSERT INTO counter VALUES (100, 3, 4096), (200, 3, -1024); + `); + + try { + const sql = renderSqlForSqlite(getStep('camera_dmabuf_summary').sql); + const rows = database.prepare(sql).all() as Array>; + + expect(rows).toEqual([ + expect.objectContaining({ + process_name: 'cameraserver', + upid: 1, + pid: 100, + memory_source: 'legacy_ion', + allocation_count: 1, + allocation_bytes: 4096, + release_bytes: 1024, + observed_net_delta_bytes: 3072, + peak_event_bytes: 4096, + }), + ]); + } finally { + database.close(); + } + }); + + it('declares the evidence-first Camera composite contract', () => { + expect(skill).toMatchObject({ + name: 'camera_trace_evidence', + type: 'composite', + category: 'rendering', + tier: 'B', + }); + expect(skill.prerequisites.required_tables).toEqual( + expect.arrayContaining(['slice', 'thread', 'process']), + ); + expect(skill.prerequisites.modules).toEqual(expect.arrayContaining([ + 'slices.with_context', + 'android.binder', + 'android.frames.timeline', + 'android.memory.dmabuf', + 'linux.cpu.frequency', + 'pixel.camera', + ])); + expect(skill.steps.map((step: any) => step.id)).toEqual(EXPECTED_STEP_IDS); + }); + + it('keeps all inputs optional and window controls bounded in production SQL', () => { + expect(skill.inputs).toEqual([ + expect.objectContaining({name: 'start_ts', type: 'timestamp', required: false}), + expect.objectContaining({name: 'end_ts', type: 'timestamp', required: false}), + expect.objectContaining({name: 'max_rows', type: 'integer', required: false}), + ]); + + for (const step of skill.steps) { + expect(step.sql).toContain('trace_start()'); + expect(step.sql).toContain('trace_end()'); + expect(step.sql).toMatch(/MIN\(MAX\(COALESCE\(\$\{max_rows\|20\}/); + } + }); + + it('publishes stable coverage and typed evidence detail columns', () => { + expectTypedColumns('evidence_coverage', { + evidence_family: 'string', + status: 'string', + row_count: 'number', + source: 'string', + limitation: 'string', + }); + expectTypedColumns('camera_process_candidates', { + identity_kind: 'string', + process_name: 'string', + thread_name: 'string', + upid: 'number', + pid: 'number', + utid: 'number', + tid: 'number', + source: 'string', + limitation: 'string', + }); + expectTypedColumns('camera_slice_candidates', { + ts: 'timestamp', + dur_ns: 'duration', + slice_name: 'string', + process_name: 'string', + thread_name: 'string', + upid: 'number', + utid: 'number', + source: 'string', + limitation: 'string', + }); + expectTypedColumns('camera_binder_summary', { + ts: 'timestamp', + dur_ns: 'duration', + client_process: 'string', + client_thread: 'string', + server_process: 'string', + server_thread: 'string', + client_upid: 'number', + client_utid: 'number', + server_upid: 'number', + server_utid: 'number', + source: 'string', + limitation: 'string', + }); + expectTypedColumns('camera_dmabuf_summary', { + process_name: 'string', + upid: 'number', + pid: 'number', + memory_source: 'string', + allocation_count: 'number', + allocation_bytes: 'bytes', + release_bytes: 'bytes', + observed_net_delta_bytes: 'bytes', + peak_event_bytes: 'bytes', + source: 'string', + limitation: 'string', + }); + expectTypedColumns('pixel_camera_stage_summary', { + cam_id: 'number', + node: 'string', + port_group: 'string', + frame_count: 'number', + avg_duration_ns: 'duration', + max_duration_ns: 'duration', + source: 'string', + limitation: 'string', + }); + }); + + it('publishes both buffer-memory evidence paths and their interpretation boundary', () => { + const coverageStep = getStep('evidence_coverage'); + const memoryStep = getStep('camera_dmabuf_summary'); + + expect(memoryStep.name).toContain('DMA-BUF/legacy ION'); + expect(memoryStep.display.title).toContain('DMA-BUF/legacy ION'); + expect(memoryStep.synthesize.fields).toEqual(expect.arrayContaining([ + expect.objectContaining({key: 'memory_source'}), + ])); + for (const step of [coverageStep, memoryStep]) { + expect(step.sql).toContain('android_dmabuf_allocs'); + expect(step.sql).toContain('thread_counter_track'); + expect(step.sql).toContain('mem.ion_change'); + expect(step.sql).toContain('retained memory'); + expect(step.sql).toContain('leak proof'); + } + }); + + it('keeps each step self-describing for evidence and report consumers', () => { + for (const step of skill.steps) { + expect(step.display).toEqual(expect.objectContaining({ + layer: expect.stringMatching(/^(overview|list)$/), + level: expect.stringMatching(/^(summary|detail)$/), + columns: expect.any(Array), + })); + expect(step.display.columns.length).toBeGreaterThan(0); + expect(step.display.columns.every((column: any) => ( + typeof column.name === 'string' && typeof column.type === 'string' + ))).toBe(true); + expect(step.save_as).toBe(step.id); + expect(step.synthesize).toEqual(expect.objectContaining({ + role: expect.stringMatching(/^(overview|list)$/), + fields: expect.any(Array), + })); + expect(step.synthesize.fields.length).toBeGreaterThan(0); + } + }); + + it('avoids unsupported timing and leak conclusions', () => { + const productionText = JSON.stringify(skill).toLowerCase(); + expect(productionText).not.toMatch(/time[_ -]?to[_ -]?(open|first[_ -]?frame)/); + expect(productionText).not.toMatch(/retained[_ -]?at[_ -]?trace[_ -]?end/); + expect(skill.steps.every((step: any) => step.type !== 'diagnostic')).toBe(true); + }); +}); diff --git a/backend/src/services/traceCaptureConfig.ts b/backend/src/services/traceCaptureConfig.ts index b34655b4f..1d891d7eb 100644 --- a/backend/src/services/traceCaptureConfig.ts +++ b/backend/src/services/traceCaptureConfig.ts @@ -12,6 +12,7 @@ import { export type CapturePresetId = | 'startup' | 'scrolling' + | 'camera' | 'anr' | 'game' | 'memory' @@ -76,6 +77,11 @@ const BINDER_EVENTS = [ 'binder/binder_unlock', ]; +const CAMERA_MEMORY_EVENTS = [ + 'dmabuf_heap/dma_heap_stat', + 'ion/ion_stat', +]; + const IO_EVENTS = [ 'block/block_rq_issue', 'block/block_rq_complete', @@ -125,6 +131,17 @@ export const CAPTURE_PRESETS: CapturePresetDefinition[] = [ dataSources: [...COMMON_DATA_SOURCES, 'android.surfaceflinger.frametimeline', 'android.input.inputevent'], description: 'Scrolling and frame-jank capture with FrameTimeline, input, scheduler, and CPU/GPU frequency.', }, + { + id: 'camera', + label: 'Android Camera', + intent: 'camera', + defaultDurationSeconds: 20, + bufferSizeKb: 98304, + atraceCategories: ['camera', 'hal', 'gfx', 'view', 'binder_driver', 'freq', 'sched'], + ftraceEvents: [...COMMON_FTRACE_EVENTS, ...BINDER_EVENTS, ...CAMERA_MEMORY_EVENTS], + dataSources: [...COMMON_DATA_SOURCES, 'android.surfaceflinger.frametimeline'], + description: 'Camera request, binder, scheduler, preview presentation, and DMA-BUF/ION allocation evidence.', + }, { id: 'anr', label: 'Android ANR/main-thread block', @@ -231,6 +248,7 @@ export const CAPTURE_PRESETS: CapturePresetDefinition[] = [ ...BINDER_EVENTS, ...IO_EVENTS, ...MEMORY_EVENTS, + ...CAMERA_MEMORY_EVENTS, 'irq/irq_handler_entry', 'irq/irq_handler_exit', 'sync/sync_timeline', diff --git a/backend/src/services/traceConfigGenerator.ts b/backend/src/services/traceConfigGenerator.ts index 24161c1c7..1a426313f 100644 --- a/backend/src/services/traceConfigGenerator.ts +++ b/backend/src/services/traceConfigGenerator.ts @@ -5,7 +5,7 @@ /** * Trace Config Generator (Spark Plan 07) * - * Given an analysis `intent` (scrolling / startup / anr / memory / generic) + * Given an analysis `intent` (scrolling / startup / camera / anr / memory / generic) * the generator returns a `TraceConfigGeneratorContract` listing the * Perfetto data sources that should be enabled, plus the canonical custom * slice schema and a self-description block that should be embedded inside @@ -28,6 +28,7 @@ import { export type TraceIntent = | 'scrolling' | 'startup' + | 'camera' | 'anr' | 'memory' | 'gpu' @@ -75,6 +76,24 @@ const STARTUP_FRAGMENTS: PerfettoConfigFragment[] = [ {dataSource: 'linux.process_stats', reason: 'process create / app launch attribution'}, ]; +const CAMERA_FRAGMENTS: PerfettoConfigFragment[] = [ + { + dataSource: 'linux.ftrace', + reason: 'camera request activity, scheduler, binder, DMA-BUF/ION allocation events, and vendor atrace slices', + options: { + sched_switch: 'true', + sched_blocked_reason: 'true', + binder_transaction: 'true', + dma_heap_stat: 'true', + ion_stat: 'true', + }, + }, + { + dataSource: 'android.surfaceflinger.frametimeline', + reason: 'presented preview frame correlation when FrameTimeline is available', + }, +]; + const ANR_FRAGMENTS: PerfettoConfigFragment[] = [ {dataSource: 'linux.ftrace', reason: 'sched + binder events to localize main-thread blockage', options: {sched_switch: 'true', sched_blocked_reason: 'true', binder_transaction: 'true', binder_lock: 'true'}}, {dataSource: 'android.input.inputevent', reason: 'input dispatch timeout decoration'}, @@ -107,6 +126,8 @@ function pickFragmentsForIntent(intent: TraceIntent): PerfettoConfigFragment[] { return [...FOUNDATION_FRAGMENTS, ...SCROLLING_FRAGMENTS]; case 'startup': return [...FOUNDATION_FRAGMENTS, ...STARTUP_FRAGMENTS]; + case 'camera': + return [...FOUNDATION_FRAGMENTS, ...CAMERA_FRAGMENTS]; case 'anr': return [...FOUNDATION_FRAGMENTS, ...ANR_FRAGMENTS]; case 'memory': diff --git a/backend/src/services/traceConfigProposal.ts b/backend/src/services/traceConfigProposal.ts index 8d30b25ac..c87830d2e 100644 --- a/backend/src/services/traceConfigProposal.ts +++ b/backend/src/services/traceConfigProposal.ts @@ -60,6 +60,7 @@ interface IntentRule { preset: CapturePresetId; confidence: TraceConfigProposalConfidence; rationale: string; + requiredKeywords?: string[]; keywords: string[]; } @@ -70,6 +71,20 @@ interface RuleMatch { } const INTENT_RULES: IntentRule[] = [ + { + preset: 'camera', + confidence: 'high', + rationale: 'Camera investigations need request activity, binder, scheduler, preview presentation, and DMA-BUF/ION allocation evidence.', + requiredKeywords: [ + 'camera', 'camera2', 'camerax', 'cameraserver', 'camera hal', + '摄像头', '相机', '取景器', + ], + keywords: [ + 'open camera', 'camera open', 'camera startup', 'first preview', + 'preview frame', 'capture request', 'capture result', 'hal3', + '打开相机', '相机启动', '首帧预览', '预览首帧', '拍照延迟', + ], + }, { preset: 'startup', confidence: 'high', @@ -220,7 +235,14 @@ const INTENT_RULES: IntentRule[] = [ }, ]; +const DOMAIN_MATCH_BONUS = Math.max( + ...INTENT_RULES + .filter(rule => !rule.requiredKeywords) + .map(rule => rule.keywords.length), +) + 1; + const PRESET_RATIONALE_ZH: Partial> = { + camera: 'Camera 分析需要覆盖 request activity、binder、调度、预览呈现和 DMA-BUF/ION 分配信号。', startup: '启动分析需要覆盖 launch、首帧、调度、binder、IO 和 FrameTimeline 信号。', scrolling: '滑动和卡顿分析需要 FrameTimeline、input、调度、CPU/GPU 频率和 binder 上下文。', anr: 'ANR 分析需要 input、主线程调度、binder、IO 和 logcat 上下文。', @@ -384,8 +406,14 @@ function classifyRequest(request: string): RuleMatch { const normalized = request.toLowerCase(); let best: RuleMatch | undefined; for (const rule of INTENT_RULES) { - const matches = rule.keywords.filter(keyword => normalized.includes(keyword.toLowerCase())); - const score = matches.length; + const requiredMatches = (rule.requiredKeywords ?? []) + .filter(keyword => normalized.includes(keyword.toLowerCase())); + if (rule.requiredKeywords && requiredMatches.length === 0) continue; + const keywordMatches = rule.keywords + .filter(keyword => normalized.includes(keyword.toLowerCase())); + const matches = unique([...requiredMatches, ...keywordMatches]); + const score = keywordMatches.length + + (requiredMatches.length > 0 ? DOMAIN_MATCH_BONUS : 0); if (!best || score > best.score) { best = { rule, score, matches }; } diff --git a/backend/src/tests/renderingPipelineDetectionGenerator.test.ts b/backend/src/tests/renderingPipelineDetectionGenerator.test.ts index 4f94e7bb1..f30458fe4 100644 --- a/backend/src/tests/renderingPipelineDetectionGenerator.test.ts +++ b/backend/src/tests/renderingPipelineDetectionGenerator.test.ts @@ -59,6 +59,11 @@ describe('rendering_pipeline_detection generator', () => { expect(activeStep.sql).toContain('eglSwapBuffers'); expect(activeStep.sql).toContain('vkQueuePresentKHR'); + const rhythmStep = skill.steps?.find((s) => s.id === 'extra_rhythm_signals') as any; + expect(rhythmStep).toBeTruthy(); + expect(rhythmStep.sql).toContain("THEN 'camera_request_activity'"); + expect(rhythmStep.sql).not.toContain('camera_sensor_trigger'); + const layerSignalsStep = skill.steps?.find((s) => s.id === 'layer_signals') as any; expect(layerSignalsStep).toBeTruthy(); expect(layerSignalsStep.sql).toContain('android_frames_layers'); diff --git a/backend/src/types/__tests__/sparkContracts.test.ts b/backend/src/types/__tests__/sparkContracts.test.ts index 9b73f73b6..37f40959b 100644 --- a/backend/src/types/__tests__/sparkContracts.test.ts +++ b/backend/src/types/__tests__/sparkContracts.test.ts @@ -895,14 +895,14 @@ describe('Plan 18 — DomainSkillEvalContract', () => { cases: [ { caseId: 'scrolling/jank/heavy_mixed', - tracePath: 'test-traces/scroll-demo-customer-scroll.pftrace', + tracePath: 'Trace/real/android-scroll-customer/trace.pftrace', skillId: 'scrolling_analysis', description: 'Customer scrolling, mixed jank', groundTruthSource: 'manual annotation 2026-04-02', }, { caseId: 'startup/heavy/lacunh', - tracePath: 'test-traces/lacunh_heavy.pftrace', + tracePath: 'Trace/real/android-startup-heavy/trace.pftrace', skillId: 'startup_analysis', description: 'Heavy app startup', }, diff --git a/backend/src/utils/traceCorpus.ts b/backend/src/utils/traceCorpus.ts new file mode 100644 index 000000000..1f0c7fce0 --- /dev/null +++ b/backend/src/utils/traceCorpus.ts @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +import fs from 'node:fs'; +import path from 'node:path'; + +export type TraceCatalogCase = { + id: string; + kind: 'real' | 'constructed'; + aliases?: string[]; + case_dir: string; + trace: {file: string; materialization: 'committed' | 'base-plus-overlay'}; + construction?: {output: string}; +}; + +function defaultRepoRoot(): string { + const cwd = process.cwd(); + if (fs.existsSync(path.join(cwd, 'Trace', 'catalog.json'))) return cwd; + const parent = path.resolve(cwd, '..'); + if (fs.existsSync(path.join(parent, 'Trace', 'catalog.json'))) return parent; + throw new Error(`Cannot locate Trace/catalog.json from ${cwd}`); +} + +export function listTraceCases(repoRoot = defaultRepoRoot()): TraceCatalogCase[] { + const catalogPath = path.join(repoRoot, 'Trace', 'catalog.json'); + const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8')) as {cases?: TraceCatalogCase[]}; + if (!Array.isArray(catalog.cases)) throw new Error(`Invalid trace catalog: ${catalogPath}`); + return catalog.cases; +} + +export function resolveTraceCase(selector: string, repoRoot = defaultRepoRoot()): string { + const matches = listTraceCases(repoRoot).filter( + entry => entry.id === selector || entry.aliases?.includes(selector), + ); + if (matches.length !== 1) { + throw new Error(matches.length === 0 + ? `Unknown trace case: ${selector}` + : `Ambiguous trace case selector: ${selector}`); + } + const entry = matches[0]; + if (entry.trace.materialization === 'base-plus-overlay') { + if (!entry.construction?.output) throw new Error(`Constructed case has no output: ${entry.id}`); + return path.resolve(repoRoot, entry.construction.output); + } + return path.resolve(repoRoot, entry.case_dir, entry.trace.file); +} diff --git a/backend/tests/agent-eval/evaluationRunner.ts b/backend/tests/agent-eval/evaluationRunner.ts index c74617106..21673855a 100644 --- a/backend/tests/agent-eval/evaluationRunner.ts +++ b/backend/tests/agent-eval/evaluationRunner.ts @@ -27,9 +27,10 @@ import { loadScenarios, loadAllScenarios, LoadOptions } from './scenarioLoader'; import { CodeGrader } from './codeGrader'; import { ModelGrader, ModelGraderOptions } from './modelGrader'; import { ProcessGrader } from './processGrader'; +import { resolveTraceCase } from '../../src/utils/traceCorpus'; -// Default trace directory -const DEFAULT_TRACE_DIR = path.join(__dirname, '../../../test-traces'); +// Sentinel for catalog-backed default fixture resolution. +const DEFAULT_TRACE_DIR = path.resolve(__dirname, '../../..'); export interface RunnerOptions { /** Backend base URL */ @@ -258,7 +259,9 @@ export class EvaluationRunner { const input = scenario.input; // Get trace file path - const tracePath = path.join(this.options.traceDir, input.traceFile); + const tracePath = this.options.traceDir === DEFAULT_TRACE_DIR + ? resolveTraceCase(input.traceFile, DEFAULT_TRACE_DIR) + : path.join(this.options.traceDir, input.traceFile); if (!fs.existsSync(tracePath)) { throw new Error(`Trace file not found: ${tracePath}`); } diff --git a/backend/tests/e2e/fullAnalysisFlow.test.ts b/backend/tests/e2e/fullAnalysisFlow.test.ts index 1ee8d0769..a2377bf2d 100644 --- a/backend/tests/e2e/fullAnalysisFlow.test.ts +++ b/backend/tests/e2e/fullAnalysisFlow.test.ts @@ -12,7 +12,7 @@ * * Prerequisites: * - Backend running with trace_processor_shell available - * - Test traces in ../test-traces/ directory + * - Trace fixtures registered in ../Trace/catalog.json * - AI service configured in .env (or tests will use degraded/fallback mode) * * Run with: diff --git a/backend/tests/integration/testApp.ts b/backend/tests/integration/testApp.ts index feebbf551..d678c3ab6 100644 --- a/backend/tests/integration/testApp.ts +++ b/backend/tests/integration/testApp.ts @@ -24,6 +24,7 @@ import { // Import services import { TraceProcessorService, getTraceProcessorService } from '../../src/services/traceProcessorService'; +import { resolveTraceCase } from '../../src/utils/traceCorpus'; // ============================================================================= // Test App Factory @@ -82,7 +83,7 @@ export function createTestApp() { * Load a test trace file and return its traceId */ export async function loadTestTrace(traceName: string): Promise { - const tracePath = path.resolve(process.cwd(), '..', 'test-traces', traceName); + const tracePath = resolveTraceCase(traceName); if (!fs.existsSync(tracePath)) { throw new Error(`Test trace not found: ${tracePath}`); @@ -112,7 +113,7 @@ export async function cleanupTrace(traceId: string): Promise { * Get test trace path */ export function getTestTracePath(traceName: string): string { - return path.resolve(process.cwd(), '..', 'test-traces', traceName); + return resolveTraceCase(traceName); } /** diff --git a/backend/tests/jank_detection_baseline.md b/backend/tests/jank_detection_baseline.md index b58974bb2..00dd469bc 100644 --- a/backend/tests/jank_detection_baseline.md +++ b/backend/tests/jank_detection_baseline.md @@ -18,7 +18,7 @@ ### 1. Heavy Jank Trace -**Trace 文件**: `test-traces/app_aosp_scrolling_heavy_jank.pftrace` +**Trace 文件**: `Trace/real/android-scroll-customer/trace.pftrace` | 指标 | 期望值 | |------|--------| @@ -43,7 +43,7 @@ ### 2. Light Jank Trace -**Trace 文件**: `test-traces/app_aosp_scrolling_light.pftrace` +**Trace 文件**: `Trace/real/android-scroll-standard/trace.pftrace` | 指标 | 期望值 | |------|--------| @@ -141,10 +141,10 @@ SELECT * FROM jank_analysis; ```bash # Heavy jank trace -./perfetto/out/ui/trace_processor_shell test-traces/app_aosp_scrolling_heavy_jank.pftrace --query-file /path/to/verify_jank.sql +./perfetto/out/ui/trace_processor_shell Trace/real/android-scroll-customer/trace.pftrace --query-file /path/to/verify_jank.sql # Light jank trace -./perfetto/out/ui/trace_processor_shell test-traces/app_aosp_scrolling_light.pftrace --query-file /path/to/verify_jank.sql +./perfetto/out/ui/trace_processor_shell Trace/real/android-scroll-standard/trace.pftrace --query-file /path/to/verify_jank.sql ``` 预期输出应与上述基准数据匹配。 diff --git a/backend/tests/skill-eval/camera_trace_evidence.eval.ts b/backend/tests/skill-eval/camera_trace_evidence.eval.ts new file mode 100644 index 000000000..8703df210 --- /dev/null +++ b/backend/tests/skill-eval/camera_trace_evidence.eval.ts @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) +// This file is part of SmartPerfetto. See LICENSE for details. + +import {afterAll, beforeAll, describe, expect, it} from '@jest/globals'; +import { + createSkillEvaluator, + describeWithTrace, + getTestTracePath, + SkillEvaluator, +} from './runner'; + +const TRACE_FILE = 'launch_light.pftrace'; +const STEP_IDS = [ + 'evidence_coverage', + 'camera_process_candidates', + 'camera_slice_candidates', + 'camera_binder_summary', + 'camera_dmabuf_summary', + 'pixel_camera_stage_summary', +] as const; +const EVIDENCE_FAMILIES = [ + 'camera_process_thread_identity', + 'camera_slice_candidates', + 'binder_transactions', + 'scheduler_context', + 'cpu_frequency_context', + 'frame_timeline', + 'dmabuf_allocations', + 'pixel_camera_frames', +] as const; + +describeWithTrace('camera_trace_evidence skill', TRACE_FILE, () => { + let evaluator: SkillEvaluator; + + beforeAll(async () => { + evaluator = createSkillEvaluator('camera_trace_evidence'); + await evaluator.loadTrace(getTestTracePath(TRACE_FILE)); + }, 60000); + + afterAll(async () => { + await evaluator?.cleanup(); + await new Promise(resolve => setTimeout(resolve, 2500)); + }); + + it('executes every evidence step safely on a non-Camera trace', async () => { + const results = await evaluator.executeStepSequence([...STEP_IDS], { + start_ts: 2, + end_ts: 1, + max_rows: 1000, + }); + + expect(results.map(result => result.stepId)).toEqual(STEP_IDS); + for (const result of results) { + expect(result.success).toBe(true); + expect(result.error).toBeUndefined(); + expect(Array.isArray(result.data)).toBe(true); + } + }, 60000); + + it('reports all eight stable evidence families with bounded statuses', async () => { + const result = await evaluator.executeStep('evidence_coverage'); + + expect(result.success).toBe(true); + expect(result.data.map(row => row.evidence_family)).toEqual(EVIDENCE_FAMILIES); + for (const row of result.data) { + expect(['available', 'vendor_specific', 'missing']).toContain(row.status); + expect(Number(row.row_count)).toBeGreaterThanOrEqual(0); + expect(typeof row.source).toBe('string'); + expect(typeof row.limitation).toBe('string'); + } + }, 30000); + + it('returns Pixel stage rows as typed details or an empty list', async () => { + const result = await evaluator.executeStep('pixel_camera_stage_summary'); + + expect(result.success).toBe(true); + expect(Array.isArray(result.data)).toBe(true); + for (const row of result.data) { + expect(row).toEqual(expect.objectContaining({ + cam_id: expect.any(Number), + node: expect.any(String), + port_group: expect.any(String), + frame_count: expect.any(Number), + avg_duration_ns: expect.any(Number), + max_duration_ns: expect.any(Number), + source: 'pixel_camera_frames', + limitation: expect.any(String), + })); + } + }, 30000); +}); diff --git a/backend/tests/skill-eval/process_identity_resolver.eval.ts b/backend/tests/skill-eval/process_identity_resolver.eval.ts index c8ded0919..a0b70d21b 100644 --- a/backend/tests/skill-eval/process_identity_resolver.eval.ts +++ b/backend/tests/skill-eval/process_identity_resolver.eval.ts @@ -5,11 +5,11 @@ * and does not rank launcher/system context above the traced app by default. */ -import path from 'node:path'; import { describe, expect, it, beforeAll } from '@jest/globals'; import { ensureSkillRegistryInitialized, skillRegistry } from '../../src/services/skillEngine/skillLoader'; import { createSkillExecutor, SkillExecutor } from '../../src/services/skillEngine/skillExecutor'; import { getTraceProcessorService, TraceProcessorService } from '../../src/services/traceProcessorService'; +import { resolveTraceCase } from '../../src/utils/traceCorpus'; const TRACE_CASES = [ { file: 'lacunh_heavy.pftrace', expectedTop: /^com\.example\./ }, @@ -35,7 +35,7 @@ async function withTrace( traceFile: string, callback: (traceId: string) => Promise, ): Promise { - const tracePath = path.resolve(process.cwd(), '..', 'test-traces', traceFile); + const tracePath = resolveTraceCase(traceFile); const traceId = await traceProcessor.loadTraceFromFilePath(tracePath); try { return await callback(traceId); diff --git a/backend/tests/skill-eval/runner.ts b/backend/tests/skill-eval/runner.ts index 624836476..219f09263 100644 --- a/backend/tests/skill-eval/runner.ts +++ b/backend/tests/skill-eval/runner.ts @@ -16,6 +16,10 @@ import { validateSkillInputs } from '../../src/services/skillEngine/skillValidat import yaml from 'js-yaml'; import fs from 'fs'; +const traceCaseCatalog = require('../../../Trace/tools/lib/catalog.cjs') as { + resolveCaseTrace(repoRoot: string, selector: string): string; +}; + // ============================================================================= // Types // ============================================================================= @@ -63,13 +67,14 @@ export class SkillEvaluator { private skill: SkillDefinition | null = null; private availablePrerequisiteModules: string[] | null = null; private static sharedTraceProcessor: TraceProcessorService | null = null; + private static skillRegistry: Map | null = null; constructor(skillId: string) { this.skillId = skillId; // 使用共享的 TraceProcessorService 实例,避免重复启动进程 if (!SkillEvaluator.sharedTraceProcessor) { SkillEvaluator.sharedTraceProcessor = new TraceProcessorService( - path.join(process.cwd(), 'uploads', 'test-traces') + path.join(process.cwd(), 'uploads', 'trace-corpus-eval') ); } this.traceProcessor = SkillEvaluator.sharedTraceProcessor; @@ -77,7 +82,7 @@ export class SkillEvaluator { /** * 加载 trace 文件 - * @param tracePath 相对于项目根目录的路径,如 'test-traces/scrolling.pftrace' + * @param tracePath Trace catalog 解析出的绝对路径,或相对于项目根目录的路径 */ async loadTrace(tracePath: string): Promise { const absolutePath = path.resolve(process.cwd(), '..', tracePath); @@ -91,18 +96,23 @@ export class SkillEvaluator { console.log(`[SkillEvaluator] Trace loaded with ID: ${this.traceId}`); this.availablePrerequisiteModules = null; - // 创建 executor - this.executor = createSkillExecutor(this.traceProcessor); - - // 加载 skill 定义 - await this.loadSkill(); + await this.selectSkill(this.skillId); + } - // 注册 skill - if (this.skill) { - this.executor.registerSkill(this.skill); - // 也需要注册子 skills(如果有 iterator 步骤) - await this.registerDependentSkills(); + /** Reuse the currently loaded trace while switching to another corpus Skill. */ + async selectSkill(skillId: string): Promise { + if (!this.traceId) { + throw new Error('SkillEvaluator not initialized. Call loadTrace() first.'); } + this.skillId = skillId; + this.skill = null; + this.availablePrerequisiteModules = null; + this.executor = createSkillExecutor(this.traceProcessor); + this.executor.setFragmentRegistry(this.loadFragmentRegistry(path.join(process.cwd(), 'skills'))); + await this.loadSkill(); + if (!this.skill) throw new Error(`Skill not found: ${skillId}`); + this.executor.registerSkill(this.skill); + await this.registerDependentSkills(); } /** @@ -110,36 +120,65 @@ export class SkillEvaluator { */ private async loadSkill(): Promise { const skillsDir = path.join(process.cwd(), 'skills'); + const skill = this.getSkillRegistry(skillsDir).get(this.skillId); + if (skill) { + this.skill = skill; + this.availablePrerequisiteModules = null; + console.log(`[SkillEvaluator] Loaded skill: ${skill.name}`); + return; + } + throw new Error(`Skill not found: ${this.skillId}`); + } - // 搜索 skill 文件 - const searchDirs = ['atomic', 'composite', 'base']; - - for (const dir of searchDirs) { - const dirPath = path.join(skillsDir, dir); - if (!fs.existsSync(dirPath)) continue; - - const files = fs.readdirSync(dirPath); - for (const file of files) { - if (!file.endsWith('.skill.yaml') && !file.endsWith('.skill.yml')) continue; - - const filePath = path.join(dirPath, file); + private getSkillRegistry(skillsDir: string): Map { + if (SkillEvaluator.skillRegistry) return SkillEvaluator.skillRegistry; + const registry = new Map(); + const stack = [skillsDir]; + while (stack.length > 0) { + const current = stack.pop()!; + if (!fs.existsSync(current)) continue; + for (const entry of fs.readdirSync(current, {withFileTypes: true})) { + const absolute = path.join(current, entry.name); + if (entry.isDirectory()) { + if (entry.name !== '_template') stack.push(absolute); + continue; + } + if ( + !entry.isFile() + || entry.name.startsWith('_') + || (!entry.name.endsWith('.skill.yaml') && !entry.name.endsWith('.skill.yml')) + ) continue; try { - const content = fs.readFileSync(filePath, 'utf-8'); - const skill = yaml.load(content) as SkillDefinition; - - if (skill && skill.name === this.skillId) { - this.skill = skill; - this.availablePrerequisiteModules = null; - console.log(`[SkillEvaluator] Loaded skill: ${skill.name}`); - return; - } - } catch (e) { - // 忽略解析错误 + const skill = yaml.load(fs.readFileSync(absolute, 'utf8')) as SkillDefinition; + if (skill?.name && !registry.has(skill.name)) registry.set(skill.name, skill); + } catch { + // The normal Skill validators report malformed YAML with file context. } } } + SkillEvaluator.skillRegistry = registry; + return registry; + } - throw new Error(`Skill not found: ${this.skillId}`); + private loadFragmentRegistry(skillsDir: string): Map { + const fragmentsRoot = path.join(skillsDir, 'fragments'); + const registry = new Map(); + if (!fs.existsSync(fragmentsRoot)) return registry; + + const stack = [fragmentsRoot]; + while (stack.length > 0) { + const current = stack.pop()!; + for (const entry of fs.readdirSync(current, {withFileTypes: true})) { + const absolute = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(absolute); + } else if (entry.isFile() && entry.name.endsWith('.sql')) { + const key = path.relative(skillsDir, absolute).split(path.sep).join('/'); + registry.set(key, fs.readFileSync(absolute, 'utf8')); + } + } + } + return registry; } /** @@ -232,31 +271,7 @@ export class SkillEvaluator { * 查找并加载 skill */ private async findAndLoadSkill(skillName: string, skillsDir: string): Promise { - const searchDirs = ['atomic', 'composite', 'base']; - - for (const dir of searchDirs) { - const dirPath = path.join(skillsDir, dir); - if (!fs.existsSync(dirPath)) continue; - - const files = fs.readdirSync(dirPath); - for (const file of files) { - if (!file.endsWith('.skill.yaml') && !file.endsWith('.skill.yml')) continue; - - const filePath = path.join(dirPath, file); - try { - const content = fs.readFileSync(filePath, 'utf-8'); - const skill = yaml.load(content) as SkillDefinition; - - if (skill && skill.name === skillName) { - return skill; - } - } catch (e) { - // 忽略解析错误 - } - } - } - - return null; + return this.getSkillRegistry(skillsDir).get(skillName) ?? null; } /** @@ -323,6 +338,11 @@ export class SkillEvaluator { } const executor = this.executor as any; + // Perfetto stdlib tables declared by a Skill do not exist until their + // modules are included. Match the production execution order before + // checking required_tables, otherwise valid module-owned tables look + // absent in the regression harness. + const moduleIncludes = await this.getAvailablePrerequisiteModules(); const prereqCheck = await executor.checkPrerequisites(this.skill, this.traceId); if (!prereqCheck.success) { throw new Error(`Skipped: ${prereqCheck.error}`); @@ -334,7 +354,7 @@ export class SkillEvaluator { inherited: {}, results: {}, variables: {}, - moduleIncludes: await this.getAvailablePrerequisiteModules(), + moduleIncludes, }; const results: EvalStepResult[] = []; @@ -671,7 +691,7 @@ export function createSkillEvaluator(skillId: string): SkillEvaluator { * 获取测试 trace 文件路径 */ export function getTestTracePath(traceName: string): string { - return path.join('test-traces', traceName); + return traceCaseCatalog.resolveCaseTrace(path.resolve(process.cwd(), '..'), traceName); } /** @@ -682,7 +702,7 @@ export function getTestTracePath(traceName: string): string { * while still exercising the suite when fixtures are available. * * Mirrors `loadTrace`'s path semantics: jest runs from backend/, traces live in - * /test-traces, so we resolve relative to cwd then '..'. + * Trace catalog paths are absolute; path.resolve keeps them unchanged. */ export function describeWithTrace( suiteName: string, diff --git a/backend/tests/trace-corpus/corpusRunner.test.ts b/backend/tests/trace-corpus/corpusRunner.test.ts new file mode 100644 index 000000000..e123444de --- /dev/null +++ b/backend/tests/trace-corpus/corpusRunner.test.ts @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +import path from 'path'; + +import { + loadCorpus, + resolveParameterTokens, + runCorpusRegression, +} from './corpusRunner'; + +const repoRoot = path.resolve(__dirname, '../../..'); + +describe('Trace corpus regression runner', () => { + it('loads the generated catalog and exact current coverage inventory', () => { + const corpus = loadCorpus(repoRoot); + + expect(corpus.cases).toHaveLength(18); + expect(corpus.coverage.missing).toEqual({skills: [], strategies: []}); + expect(corpus.coverage.covered.skills.length).toBeGreaterThan(200); + expect(corpus.coverage.covered.strategies.length).toBeGreaterThan(20); + }); + + it('resolves trace and fixture identity tokens without changing literals', () => { + expect(resolveParameterTokens( + { + start_ts: '${trace_start}', + end_ts: '${trace_end}', + upid: '${fixture_upid}', + utid: '${fixture_utid}', + package: 'com.smartperfetto.fixture', + }, + {trace_start: '10', trace_end: '20', fixture_upid: 30, fixture_utid: 40}, + )).toEqual({ + start_ts: '10', + end_ts: '20', + upid: 30, + utid: 40, + package: 'com.smartperfetto.fixture', + }); + }); + + it('executes startup_analysis and startup Strategy against the constructed startup case', async () => { + const result = await runCorpusRegression(repoRoot, { + caseIds: ['startup-lifecycle'], + targetIds: ['startup_analysis', 'startup'], + writeEvidence: false, + }); + + expect(result.failures).toEqual([]); + expect(result.executed).toEqual(expect.arrayContaining([ + 'startup-lifecycle:skill:startup_analysis', + 'startup-lifecycle:strategy:startup', + ])); + }, 120_000); +}); diff --git a/backend/tests/trace-corpus/corpusRunner.ts b/backend/tests/trace-corpus/corpusRunner.ts new file mode 100644 index 000000000..ca3776a90 --- /dev/null +++ b/backend/tests/trace-corpus/corpusRunner.ts @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +import fs from 'fs'; +import path from 'path'; + +import {classifyScene} from '../../src/agentv3/sceneClassifier'; +import {loadStrategies} from '../../src/agentv3/strategyLoader'; +import {createSkillEvaluator, type SkillEvaluator} from '../skill-eval/runner'; + +type TokenContext = { + trace_start: string; + trace_end: string; + fixture_upid: number; + fixture_utid: number; +}; + +type CorpusExpectation = { + id: string; + type: 'skill' | 'strategy'; + target: string; + mode?: 'semantic' | 'graceful_empty' | 'unavailable' | 'definition'; + source_file?: string; + parameters?: Record; + required_steps?: string[]; + semantic_step?: string; + limitation_reason?: string; + expected_error?: string; + required_marker?: string; + query?: string; + expected_strategy?: string; +}; + +type CorpusCase = { + id: string; + kind: 'real' | 'constructed'; + case_dir: string; + manifest_path: string; + construction?: {output: string}; + coverage: {expectations: CorpusExpectation[]}; +}; + +export type CorpusRunResult = { + executed: string[]; + failures: Array<{case_id: string; target: string; reason: string}>; +}; + +export function loadCorpus(repoRoot: string): { + cases: CorpusCase[]; + coverage: any; +} { + return { + cases: JSON.parse(fs.readFileSync(path.join(repoRoot, 'Trace/catalog.json'), 'utf8')).cases, + coverage: JSON.parse(fs.readFileSync(path.join(repoRoot, 'Trace/coverage.json'), 'utf8')), + }; +} + +export function resolveParameterTokens( + parameters: Record, + context: TokenContext, +): Record { + const tokenValues = new Map([ + ['${trace_start}', context.trace_start], + ['${trace_end}', context.trace_end], + ['${fixture_upid}', context.fixture_upid], + ['${fixture_utid}', context.fixture_utid], + ]); + return Object.fromEntries( + Object.entries(parameters).map(([key, value]) => [ + key, + typeof value === 'string' && tokenValues.has(value) ? tokenValues.get(value) : value, + ]), + ); +} + +async function loadTokenContext(evaluator: SkillEvaluator): Promise { + const result = await evaluator.executeSQL(` + SELECT + printf('%d', trace_start()) AS trace_start, + printf('%d', trace_end()) AS trace_end, + COALESCE((SELECT upid FROM process WHERE name = 'com.smartperfetto.fixture' ORDER BY upid DESC LIMIT 1), 0) AS fixture_upid, + COALESCE(( + SELECT t.utid FROM thread t + JOIN process p USING (upid) + WHERE p.name = 'com.smartperfetto.fixture' AND t.name = 'main' + ORDER BY t.utid DESC LIMIT 1 + ), 0) AS fixture_utid + `); + if (result.error || result.rows.length !== 1) { + throw new Error(`cannot resolve trace tokens: ${result.error ?? 'no row'}`); + } + const row = result.rows[0]; + return { + trace_start: String(row[0]), + trace_end: String(row[1]), + fixture_upid: Number(row[2]), + fixture_utid: Number(row[3]), + }; +} + +async function assertMarker(evaluator: SkillEvaluator, marker: string | undefined): Promise { + if (!marker) return; + const escaped = marker.replace(/'/g, "''"); + const result = await evaluator.executeSQL(`SELECT COUNT(*) AS count FROM slice WHERE name = '${escaped}'`); + if (result.error || Number(result.rows[0]?.[0] ?? 0) < 1) { + throw new Error(`required marker is absent: ${marker}`); + } +} + +function validateDefinition(repoRoot: string, expectation: CorpusExpectation): void { + if (!expectation.source_file) throw new Error('definition expectation has no source_file'); + const sourcePath = path.resolve(repoRoot, expectation.source_file); + if (!fs.existsSync(sourcePath)) throw new Error(`definition source is missing: ${expectation.source_file}`); + const source = fs.readFileSync(sourcePath, 'utf8'); + if (!new RegExp(`^name:\\s*["']?${expectation.target}["']?\\s*$`, 'm').test(source)) { + throw new Error(`definition source does not declare ${expectation.target}`); + } +} + +async function runSkillExpectation( + evaluator: SkillEvaluator, + expectation: CorpusExpectation, + tokenContext: TokenContext, +): Promise { + await assertMarker(evaluator, expectation.required_marker); + if (expectation.mode === 'definition') return; + await evaluator.selectSkill(expectation.target); + const requiredSteps = expectation.required_steps ?? []; + if (requiredSteps.length === 0) throw new Error('execute expectation has no required_steps'); + const params = resolveParameterTokens(expectation.parameters ?? {}, tokenContext); + const semanticStep = expectation.semantic_step ?? requiredSteps[requiredSteps.length - 1]; + let results; + try { + results = await evaluator.executeStepSequence(requiredSteps, params); + } catch (error: any) { + const message = error?.message ?? String(error); + if (expectation.mode === 'unavailable' && expectation.expected_error && message.includes(expectation.expected_error)) { + return; + } + throw error; + } + const failed = results.find((result) => + !result.success && + !(result.stepId !== semanticStep && result.error === 'Condition not met'), + ); + if (failed) { + const message = `${failed.stepId} failed: ${failed.error ?? 'unknown error'}`; + if (expectation.mode === 'unavailable' && expectation.expected_error && message.includes(expectation.expected_error)) { + return; + } + throw new Error(message); + } + const semanticResult = results.find((result) => result.stepId === semanticStep); + if (!semanticResult) throw new Error(`semantic step was not executed: ${semanticStep}`); + if (expectation.mode === 'graceful_empty') { + if (semanticResult.data.length !== 0) { + throw new Error(`graceful-empty expectation unexpectedly returned ${semanticResult.data.length} row(s): ${semanticStep}`); + } + return; + } + if (expectation.mode === 'unavailable') { + throw new Error(`unavailable expectation unexpectedly executed successfully: ${semanticStep}`); + } + if (semanticResult.data.length === 0) { + throw new Error(`semantic step returned no rows: ${semanticStep ?? requiredSteps.join(', ')}`); + } +} + +async function runStrategyExpectation( + evaluator: SkillEvaluator, + expectation: CorpusExpectation, +): Promise { + await assertMarker(evaluator, expectation.required_marker); + const strategy = loadStrategies().get(expectation.target); + if (!strategy) throw new Error(`Strategy loader cannot resolve ${expectation.target}`); + if (strategy.strategyKind !== 'contract_only') { + const actual = classifyScene(expectation.query ?? ''); + if (actual !== (expectation.expected_strategy ?? expectation.target)) { + throw new Error(`classifier returned ${actual} for query ${JSON.stringify(expectation.query)}`); + } + } +} + +export async function runCorpusRegression( + repoRoot: string, + options: { + caseIds?: string[]; + targetIds?: string[]; + writeEvidence?: boolean; + } = {}, +): Promise { + const corpus = loadCorpus(repoRoot); + const selectedCases = corpus.cases.filter((entry) => + entry.kind === 'constructed' && (!options.caseIds || options.caseIds.includes(entry.id)), + ); + const targetFilter = options.targetIds ? new Set(options.targetIds) : null; + const result: CorpusRunResult = {executed: [], failures: []}; + + for (const entry of selectedCases) { + const expectations = entry.coverage.expectations.filter((expectation) => + !targetFilter || targetFilter.has(expectation.target), + ); + if (expectations.length === 0) continue; + const tracePath = path.resolve(repoRoot, entry.construction!.output); + if (!fs.existsSync(tracePath)) { + for (const expectation of expectations) { + result.failures.push({case_id: entry.id, target: expectation.target, reason: `materialized trace missing: ${tracePath}`}); + } + continue; + } + const executable = expectations.find((expectation) => expectation.type === 'skill' && expectation.mode !== 'definition'); + const evaluator = createSkillEvaluator(executable?.target ?? 'global_trace_sanity_check'); + try { + await evaluator.loadTrace(tracePath); + const tokenContext = await loadTokenContext(evaluator); + for (const expectation of expectations) { + const executionKey = `${entry.id}:${expectation.type}:${expectation.target}`; + try { + if (expectation.type === 'skill') { + if (expectation.mode === 'definition') validateDefinition(repoRoot, expectation); + await runSkillExpectation(evaluator, expectation, tokenContext); + } else { + await runStrategyExpectation(evaluator, expectation); + } + result.executed.push(executionKey); + } catch (error: any) { + result.failures.push({case_id: entry.id, target: expectation.target, reason: error?.message ?? String(error)}); + } + } + } catch (error: any) { + for (const expectation of expectations) { + result.failures.push({case_id: entry.id, target: expectation.target, reason: error?.message ?? String(error)}); + } + } finally { + await evaluator.cleanup(); + } + + if (options.writeEvidence !== false) { + const evidencePath = path.join(repoRoot, 'Trace/.generated/constructed', entry.id, 'regression-result.json'); + const caseEvidence = { + schema_version: 1, + case_id: entry.id, + executed: result.executed.filter((key) => key.startsWith(`${entry.id}:`)), + failures: result.failures.filter((failure) => failure.case_id === entry.id), + }; + fs.writeFileSync(evidencePath, `${JSON.stringify(caseEvidence, null, 2)}\n`); + } + } + return result; +} diff --git a/backend/tests/trace-corpus/trace_corpus_regression.ts b/backend/tests/trace-corpus/trace_corpus_regression.ts new file mode 100644 index 000000000..b654da2fe --- /dev/null +++ b/backend/tests/trace-corpus/trace_corpus_regression.ts @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright (C) 2024-2026 Gracker (Chris) + +import path from 'path'; + +import {runCorpusRegression} from './corpusRunner'; + +function optionValues(name: string): string[] | undefined { + const values: string[] = []; + for (let index = 0; index < process.argv.length; index++) { + if (process.argv[index] === name && process.argv[index + 1]) values.push(process.argv[index + 1]); + } + return values.length > 0 ? values : undefined; +} + +async function main(): Promise { + if (process.argv.includes('--quiet')) { + const writeSummary = console.log.bind(console); + console.log = (...args: unknown[]) => { + if (String(args[0] ?? '').startsWith('Trace corpus regression')) writeSummary(...args); + }; + } + const repoRoot = path.resolve(__dirname, '../../..'); + const result = await runCorpusRegression(repoRoot, { + caseIds: optionValues('--case'), + targetIds: optionValues('--target'), + writeEvidence: true, + }); + for (const failure of result.failures) { + console.error(`[FAIL] ${failure.case_id}:${failure.target} - ${failure.reason}`); + } + if (result.failures.length > 0) { + throw new Error( + `Trace corpus regression failed: ${result.executed.length} passed, ${result.failures.length} failed`, + ); + } + console.log(`Trace corpus regression passed: ${result.executed.length} expectation(s)`); +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/backend/tests/verify_jank_detection.sh b/backend/tests/verify_jank_detection.sh index c08b91264..ec2bf4733 100755 --- a/backend/tests/verify_jank_detection.sh +++ b/backend/tests/verify_jank_detection.sh @@ -76,7 +76,7 @@ echo "=== Jank Detection Regression Test ===" echo "" # 测试 Heavy Jank Trace -HEAVY_TRACE="$PROJECT_ROOT/test-traces/app_aosp_scrolling_heavy_jank.pftrace" +HEAVY_TRACE="$PROJECT_ROOT/Trace/real/android-scroll-customer/trace.pftrace" if [ -f "$HEAVY_TRACE" ]; then echo "Testing: Heavy Jank Trace" RESULT=$("$TRACE_PROCESSOR" "$HEAVY_TRACE" -Q "$VERIFY_SQL" 2>/dev/null | tail -1) @@ -114,7 +114,7 @@ fi echo "" # 测试 Light Jank Trace -LIGHT_TRACE="$PROJECT_ROOT/test-traces/app_aosp_scrolling_light.pftrace" +LIGHT_TRACE="$PROJECT_ROOT/Trace/real/android-scroll-standard/trace.pftrace" if [ -f "$LIGHT_TRACE" ]; then echo "Testing: Light Jank Trace" RESULT=$("$TRACE_PROCESSOR" "$LIGHT_TRACE" -Q "$VERIFY_SQL" 2>/dev/null | tail -1) diff --git a/docs/architecture/perfetto-ai-rfc-0025/07-batch-trace-lifecycle.md b/docs/architecture/perfetto-ai-rfc-0025/07-batch-trace-lifecycle.md index b495ddf2c..c4c6a235a 100644 --- a/docs/architecture/perfetto-ai-rfc-0025/07-batch-trace-lifecycle.md +++ b/docs/architecture/perfetto-ai-rfc-0025/07-batch-trace-lifecycle.md @@ -177,7 +177,7 @@ Validation completed for this implementation: - `npm run cli:pack-check` passed, including backend build and CLI package manifest validation. - `npm run test:scene-trace-regression` passed across all 6 canonical traces. -- Real CLI smoke passed on `test-traces/launch_light.pftrace`, wrote JSON/HTML +- Real CLI smoke passed on `Trace/real/android-startup-light/trace.pftrace`, wrote JSON/HTML artifacts, and produced promotable `startup.total_ms = 301.839437` and `startup.first_frame_ms = 299.818888`. - `git diff --check` passed. @@ -659,7 +659,7 @@ Validation: ```bash cd backend npx jest src/cli-user/commands/__tests__/batch.test.ts --runInBand -npm run cli:dev -- batch skill startup_analysis ../test-traces/launch_light.pftrace --format json +npm run cli:dev -- batch skill startup_analysis ../Trace/real/android-startup-light/trace.pftrace --format json ``` Expected result: CLI batch can run one real trace, emit JSON, and write diff --git a/docs/architecture/perfetto-ai-rfc-0025/08-ai-disable-and-capability-disclosure.md b/docs/architecture/perfetto-ai-rfc-0025/08-ai-disable-and-capability-disclosure.md index 883dc241b..64e5cb9c2 100644 --- a/docs/architecture/perfetto-ai-rfc-0025/08-ai-disable-and-capability-disclosure.md +++ b/docs/architecture/perfetto-ai-rfc-0025/08-ai-disable-and-capability-disclosure.md @@ -474,7 +474,7 @@ Verification notes: `compare`, and `capture android --analyze`. - CLI deterministic smoke covered `provider list`, `capture presets`, `capture config`, `query`, and deterministic `skill` execution. -- Browser smoke loaded `test-traces/launch_light.pftrace` in dev mode and +- Browser smoke loaded `Trace/real/android-startup-light/trace.pftrace` in dev mode and verified the AI Assistant disabled disclosure, disabled input/send controls, and zero analyze requests. - `perfetto/ui` production build and `./scripts/update-frontend.sh` passed for diff --git a/docs/getting-started/code-aware-analysis.en.md b/docs/getting-started/code-aware-analysis.en.md index a303e0768..67db9108d 100644 --- a/docs/getting-started/code-aware-analysis.en.md +++ b/docs/getting-started/code-aware-analysis.en.md @@ -33,7 +33,7 @@ npm run cli -- codebase symbols MainActivity --codebase-id cb_xxx npm run cli -- run --format json \ --code-aware metadata_only \ --codebase-id cb_xxx \ - ../test-traces/lacunh_heavy.pftrace \ + ../Trace/real/android-startup-heavy/trace.pftrace \ "Find the startup bottleneck and map it to source code" ``` @@ -67,8 +67,8 @@ npm run verify:codebase-aware The local full E2E uses: -- `test-traces/lacunh_heavy.pftrace` -- `test-traces/launch_light.pftrace` +- `Trace/real/android-startup-heavy/trace.pftrace` +- `Trace/real/android-startup-light/trace.pftrace` - `/Users/chris/Code/HighPerformanceFriendsCircle` The E2E covers both paths: diff --git a/docs/getting-started/code-aware-analysis.md b/docs/getting-started/code-aware-analysis.md index 2340b985a..031ccab49 100644 --- a/docs/getting-started/code-aware-analysis.md +++ b/docs/getting-started/code-aware-analysis.md @@ -33,7 +33,7 @@ npm run cli -- codebase symbols MainActivity --codebase-id cb_xxx npm run cli -- run --format json \ --code-aware metadata_only \ --codebase-id cb_xxx \ - ../test-traces/lacunh_heavy.pftrace \ + ../Trace/real/android-startup-heavy/trace.pftrace \ "结合源码定位启动慢原因" ``` @@ -67,8 +67,8 @@ npm run verify:codebase-aware 本机完整 E2E 会使用: -- `test-traces/lacunh_heavy.pftrace` -- `test-traces/launch_light.pftrace` +- `Trace/real/android-startup-heavy/trace.pftrace` +- `Trace/real/android-startup-light/trace.pftrace` - `/Users/chris/Code/HighPerformanceFriendsCircle` E2E 覆盖两条路径: diff --git a/docs/getting-started/quick-start.en.md b/docs/getting-started/quick-start.en.md index 05d49bff5..5c05d6882 100644 --- a/docs/getting-started/quick-start.en.md +++ b/docs/getting-started/quick-start.en.md @@ -88,3 +88,13 @@ and [Testing Rules](../../.claude/rules/testing.md): Release, npm, Docker, or portable-package changes also need the [Release Runbook](../reference/release.en.md) and [Release Rules](../../.claude/rules/release.md). + +## 7. Trace Corpus + +The repository's [Trace corpus](../../Trace/README.md) separates real cases from reproducible constructed cases. Each real case keeps its trace, analysis results, logs, provenance, and Android/API metadata in one directory. Constructed cases store deterministic overlays on real base traces and cover the current Skill and Strategy inventory. + +- Check indexes, hashes, publication approvals, and exact coverage: `npm run trace:validate` +- Build every combined trace: `npm run trace:build` +- Run the full release regression: `npm run trace:regression` + +New captures must enter the ignored `.private/` staging area through `import-real`. Run `promote-real` only after recording license, consent, privacy review, and sanitization review. See [Trace/README.md](../../Trace/README.md) for complete commands, the constructed-scenario template, coverage quality levels, and Android-version conventions. diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index f7af04af9..61663a90e 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -92,3 +92,13 @@ 发布、npm、Docker 或免安装包相关改动还需要读 [发布手册](../reference/release.md) 和 [发布规则](../../.claude/rules/release.md)。 + +## 7. Trace 案例库 + +仓库的 [Trace 案例库](../../Trace/README.md) 分为真实案例和可复现构造案例。真实案例按“一例一目录”保存 trace、分析结果、日志、来源和 Android/API 元数据;构造案例保存真实 base trace 上的确定性 overlay,并覆盖当前全部 Skill 与 Strategy。 + +- 检查索引、哈希、发布审批和精确覆盖:`npm run trace:validate` +- 构建所有组合 trace:`npm run trace:build` +- 运行完整发布回归:`npm run trace:regression` + +新抓取的真实 trace 必须先用 `import-real` 进入被 Git 忽略的 `.private/` 暂存区。完成许可、同意记录、隐私和脱敏审查后,再显式执行 `promote-real`。完整命令、构造场景模板、覆盖质量分级和 Android 版本约定见 [Trace/README.md](../../Trace/README.md)。 diff --git a/docs/reference/cli.en.md b/docs/reference/cli.en.md index a4b329f36..f6ea3768c 100644 --- a/docs/reference/cli.en.md +++ b/docs/reference/cli.en.md @@ -275,7 +275,9 @@ on Android Q/API 29 and newer, and use a packaged or explicitly supplied smp capture presets smp capture suggest "debug startup jank" --app com.example.app --format json smp capture suggest "investigate scrolling frame drops; do not record yet" --app com.example.app +smp capture suggest "Analyze Camera open-to-first-preview latency" --app com.example.camera smp capture config --preset startup --app com.example.app --duration 10 --out startup.pbtxt +smp capture config --preset camera --app com.example.camera --duration 20 smp capture config --preset cpu --app '*' --duration 30 --categories dalvikviktime my_custom_tag --out cpu-custom.pbtxt smp capture config --preset power --app com.example.app --duration 60 --out power.pbtxt @@ -288,9 +290,15 @@ smp capture android --preset overview --app com.example.app --duration 10 --kill smp capture android --preset game --app com.example.game --duration 20 --out game.perfetto-trace --analyze --query "Find launch and frame pacing issues" --mode fast ``` -Available presets: `startup`, `scrolling`, `anr`, `game`, `memory`, `cpu`, +Available presets: `startup`, `scrolling`, `camera`, `anr`, `game`, `memory`, `cpu`, `power`, `overview`, and `full`. `power` enables `android.power` battery counters, power rails, suspend/wakeup ftrace, and `android.network_packets`. +`camera` collects Camera/HAL/vendor atrace candidates, Binder, scheduler, +FrameTimeline, and DMA-BUF or legacy ION events. These tracepoints are optional +and vary by Android release, kernel, and vendor implementation. Even with this +preset, a trace may lack portable Camera open, request/result, buffer, or +preview-presentation anchors. SmartPerfetto reports that evidence gap instead +of fabricating an open-to-first-frame number. `smp capture suggest` is side-effect free: it maps natural language to a built-in preset and returns rationale, warnings, recommended commands, and a textproto preview rendered by the same config renderer. It does not call an LLM, diff --git a/docs/reference/cli.md b/docs/reference/cli.md index b1052ab0c..7cec9c827 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -261,7 +261,9 @@ Perfetto 的 Android/Linux system tracing 路线:Android Q/API 29 及以上优 smp capture presets smp capture suggest "debug startup jank" --app com.example.app --format json smp capture suggest "分析滑动掉帧,先不要真的抓取" --app com.example.app +smp capture suggest "分析 Camera 打开到首帧预览延迟" --app com.example.camera smp capture config --preset startup --app com.example.app --duration 10 --out startup.pbtxt +smp capture config --preset camera --app com.example.camera --duration 20 smp capture config --preset cpu --app '*' --duration 30 --categories dalvikviktime my_custom_tag --out cpu-custom.pbtxt smp capture config --preset power --app com.example.app --duration 60 --out power.pbtxt @@ -274,9 +276,14 @@ smp capture android --preset overview --app com.example.app --duration 10 --kill smp capture android --preset game --app com.example.game --duration 20 --out game.perfetto-trace --analyze --query "分析启动和帧节奏问题" --mode fast ``` -内置预设包括:`startup`、`scrolling`、`anr`、`game`、`memory`、`cpu`、 +内置预设包括:`startup`、`scrolling`、`camera`、`anr`、`game`、`memory`、`cpu`、 `power`、`overview`、`full`。`power` 会开启 `android.power` 的 battery counters、power rails、suspend/wakeup 相关 ftrace 和 `android.network_packets`。 +`camera` 会采集 Camera/HAL/厂商 atrace 候选、Binder、scheduler、FrameTimeline, +以及 DMA-BUF 或旧版 ION 事件;这些 tracepoint 都是可选的,会随 Android 版本、 +内核和厂商实现而变化。即使使用该预设,trace 仍可能缺少可移植的 Camera open、 +request/result、buffer 或预览 presentation 锚点。SmartPerfetto 会把这种情况报告为 +证据缺口,而不会编造“打开到首帧”耗时。 `smp capture suggest` 是无副作用的采集建议入口:它只根据自然语言确定内置 preset,返回 rationale、warning、推荐命令和同一 renderer 生成的 textproto 预览;不会调用 LLM、ADB、tracebox,也不会录制设备。真正执行仍需要用户显式运行 diff --git a/docs/rendering_pipelines/camera_pipeline.md b/docs/rendering_pipelines/camera_pipeline.md index 20cb3c82e..61fd399af 100644 --- a/docs/rendering_pipelines/camera_pipeline.md +++ b/docs/rendering_pipelines/camera_pipeline.md @@ -5,7 +5,7 @@ Camera 是 Android 系统中数据量最大、实时性要求最高的子系统 ## 1. 核心架构:多流并发 (Multi-Stream) 与简单的 View 渲染不同,Camera 系统天生就是**多消费者 (Multiple Consumers)** 的。 -Camera HAL (Hardware Abstraction Layer) 可以同时向多个 Surface 输出数据,而且通常是**零拷贝 (Zero Copy)** 的。 +Camera HAL (Hardware Abstraction Layer) 可以同时向多个 Surface 输出数据;具体 buffer topology、复制路径与 ZSL 行为取决于设备 capability、实现和 App 配置,不能预设为统一的零拷贝拓扑。 ### 关键组件 @@ -28,9 +28,9 @@ Camera HAL (Hardware Abstraction Layer) 可以同时向多个 Surface 输出数 ### 阶段二:Request & Produce (生产) 1. **setRepeatingRequest**: App 下发一个循环请求(通常用于预览)。 -2. **ISP Processing**: 传感器 (Sensor) 曝光 -> ISP 去噪/白平衡 -> 输出 RAW/YUV。 -3. **Buffer Fill**: HAL 直接向各个 Surface 的 BufferQueue 填充数据。 - * *注意*: 现代 HAL 通常直接操作 GraphicBuffer,不经过 CPU 拷贝。 +2. **Request / Result**: Camera2/HAL3 request 按序提交,但流水线中可同时有多个 request in-flight;一个 request 可返回 partial metadata 和一个或多个输出 buffer。 +3. **ISP Processing**: 传感器 (Sensor) 曝光 -> ISP 去噪/白平衡 -> 输出 RAW/YUV。 +4. **Buffer Fill**: HAL 按已配置的输出 Surface 交付 buffer;是否有额外复制由能力、实现与 App 配置决定。 ### 阶段三:Consume (消费/渲染) @@ -42,12 +42,12 @@ Camera HAL (Hardware Abstraction Layer) 可以同时向多个 Surface 输出数 #### Case B: Recording (录像) * **MediaCodec Input Surface**: HAL 填充 Buffer -> MediaCodec (Encoder) -> H.264/265 bitstream。 - * *路径*: 全程硬件加速零拷贝(HAL 直接向 Encoder 的 Input Surface 写入 GraphicBuffer),不经过 CPU。 + * *路径*: 在支持的设备与流组合上可使用硬件 buffer 路径;不能仅凭 Surface 配置断言全程零拷贝。 #### Case C: Analysis (AI/CV) * **ImageReader**: HAL 填充 Buffer -> App `onImageAvailable`。 * App 通过 `image.getPlanes()` 获取 YUV 数据指针 (ByteBuffer)。 - * *性能坑点*: 如果 App 拿到 Buffer 后处理太慢(不及时 close),会导致 HAL 没有空闲 Buffer 可用,从而发生**掉帧 (Frame Drop)**。 + * *性能假设*: App 长时间持有 image 可能导致 backpressure,但只有 trace 中存在 buffer ownership、acquire/release 或 fence 证据时才应报告这一结论。 --- @@ -92,10 +92,7 @@ sequenceDiagram ## 4. 性能特征与调优 ### 4.1 ZSL (Zero Shutter Lag) -为了解决”按下快门到真正拍照”的延迟: -* Camera HAL 一直在后台以全分辨率捕获 **reprocessable YUV/RAW** 帧,存入一个环形缓冲区 (Ring Buffer)。 -* 当用户按快门时,系统从缓冲区中”捞”出最近的一帧,通过 **Reprocessing Pipeline**(ISP 再处理:降噪、HDR 合成等)生成最终的 JPEG/HEIC 输出。 -* *注*: ZSL 缓冲区存储的是未压缩的 YUV 或 RAW 数据(而非 JPEG),以保留最大后处理空间。Reprocessing 通过 `CameraCaptureSession.createReprocessableCaptureSession()` 配置。 +ZSL 用于降低按下快门到输出照片的延迟,但其可用性、历史帧缓存格式、reprocessing 路径和 buffer topology 都取决于设备 capability、HAL/OEM 实现与 App 配置。不能假设所有设备都会持续保存全分辨率 YUV/RAW 环形缓冲区,也不能从 `createReprocessableCaptureSession()` 名称单独还原实际 ZSL 数据路径。 ### 4.2 SurfaceView vs TextureView * **更倾向 SurfaceView** 的场景:高帧率/高分辨率预览、追求更低延迟或更低功耗时,SurfaceView 往往更有优势。 @@ -103,12 +100,14 @@ sequenceDiagram ### 4.3 内存抖动 * **ImageReader**: 务必复用。很多初学者在 `onImageAvailable` 里 `new byte[]` 来拷贝数据,这是性能杀手。应该优先使用 NDK、GPU、Vulkan、OpenGL ES 或其他现代加速路径直接处理 `ByteBuffer`;RenderScript 已废弃,不应再作为推荐方案。 +* **证据边界**: PSS/RSS 增长只能说明进程内存变化,不能单独证明 ImageReader 泄漏或 backpressure;需要关联 buffer ownership、acquire/release、队列深度或 fence 证据。 ## 5. 常见 Trace 分析 在 Perfetto 中: -* **CameraProvider / CameraService / vendor camera threads**: 若数据源启用且设备支持,可用于观察 HAL / service / request 流转。 +* **CameraProvider / CameraService / vendor camera threads**: 若数据源启用且设备支持,可作为 HAL / service / request activity candidate;vendor slice 名本身不能证明具体 request/result/buffer 阶段。 * **dma_buf**: 可用于监控 GraphicBuffer / DMA-BUF 的内存分配,Camera 预览通常也是大内存消耗户。 * **注意**: 缺少 `CameraProvider` 或 `CaptureSession` slice 并不代表没有 Camera 压力,很多设备只暴露更粗粒度的 proto / vendor 事件。 +* **Pixel fast path**: `pixel.camera` 解析只是 Pixel 设备上可选的 vendor-specific 快速路径,不是可移植的 Android Camera contract;仍需用稳定 identity 和明确生命周期锚点交叉验证。 ## 6. HAL3 Request-Buffer 生命周期 (Deep Dive) @@ -116,16 +115,33 @@ sequenceDiagram ### 6.1 Request 状态机 +Camera2/HAL3 request 按序进入管线,但多个 request 可以同时处于 in-flight。每个 request 可能分批返回 partial metadata,并向一个或多个配置好的 Surface 交付 buffer;最终 metadata 完成与各 buffer 的交付/呈现不是同一个事件。 + ```mermaid stateDiagram-v2 + state "per-target output / buffer tracking" as OutputTracking [*] --> Pending: capture() - Pending --> InFlight: HAL dequeue - InFlight --> Completed: Result + Buffer + Pending --> InFlight: request accepted + InFlight --> PartialResult: partial metadata (optional) + PartialResult --> InFlight: more work / buffers + InFlight --> FinalMetadataObserved: final TotalCaptureResult + FinalMetadataObserved --> OutputTracking: reconcile configured targets + InFlight --> OutputTracking: target buffer observed + OutputTracking --> InFlight: final metadata or expected outputs still missing + OutputTracking --> RequestComplete: final metadata + every target output accounted for + RequestComplete --> [*] InFlight --> Failed: Error - Completed --> [*] Failed --> [*] ``` +Request completion 必须按配置的目标 Surface 分别跟踪其 buffer 状态;final metadata 不能直接把 request 标为完成。Preview presentation 是 buffer 离开 Camera request/result 生命周期后的下游独立事件,必须用单独的 consumer/display 锚点关联,不能并入 request completion。 + +```mermaid +flowchart LR + TargetBuffer[accounted target buffer] -. same buffer / layer identity .-> Consumer[consumer acquire / latch] + Consumer --> Presentation[explicit preview presentation anchor] +``` + ### 6.2 Buffer 生命周期 | 阶段 | 触发者 | Buffer 状态 | @@ -138,21 +154,28 @@ stateDiagram-v2 ### 6.3 Session Callback (性能关键) -`CameraCaptureSession.CaptureCallback` 提供了精细的时间戳信息: +`CameraCaptureSession.CaptureCallback` 提供候选锚点,但每个回调都需要同一 session/request/frame identity 下的关联证据: -| 回调方法 | 触发时机 | 性能分析用途 | +| 回调方法 | 候选锚点 | 必要关联证据与事实边界 | |:---|:---|:---| -| `onCaptureStarted` | Sensor 曝光开始 | 测量 Request 下发延迟 | -| `onCaptureProgressed` | 部分 Metadata 就绪 | 3A (AE/AF/AWB) 收敛速度分析 | -| `onCaptureCompleted` | 全部 Metadata 就绪 | 测量 Pipeline 总耗时 | -| `onCaptureFailed` | HAL 报错 | 掉帧根因定位 | -| `onCaptureBufferLost` | Buffer 丢失 | BufferQueue 压力分析 | - -### 6.4 典型掉帧场景 - -1. **Buffer Starvation**: Consumer (`ImageReader`) 处理太慢,`image.close()` 不及时,导致 HAL 无法获取空闲 Buffer。 - * *Trace*: 可能看到 Camera / BufferQueue / binder / consumer 相关等待,但不保证稳定出现 `CameraProvider::dequeueBuffer` 这类 slice。 -2. **Pipeline Stall**: ISP 处理某些特效(HDR、夜景)耗时过长,超过帧间隔。 - * *Trace*: `onCaptureCompleted` 到 `onCaptureStarted` 间隔不稳定。 +| `onCaptureStarted` | capture-start callback / exposure timestamp | 只有同时存在 request submission 锚点,才能计算 submission-to-capture 延迟;单独回调不能表示“Request 下发延迟” | +| `onCaptureProgressed` | partial capture result | 必须关联同 identity,且 metadata 中实际存在 AE/AF/AWB state 字段,才能分析 3A;partial metadata 本身不证明 3A 状态或收敛 | +| `onCaptureCompleted` | final `TotalCaptureResult` | 只证明 final metadata 返回;不等于目标 buffer 已交付、预览已呈现或 pipeline 总耗时完成,仍需关联每个目标 output/buffer、fence 与 presentation 证据 | +| `onCaptureFailed` | capture failure callback | 必须关联同 identity 和错误信息;不能单独定位 HAL、ISP 或 consumer 根因 | +| `onCaptureBufferLost` | 某个目标 Surface 的 buffer-lost callback | 必须关联同 identity、目标 Surface 与 buffer/fence 证据,才能判断受影响输出和压力来源 | + +### 6.4 启动、首帧与 3A 证据边界 + +* `CameraDevice.StateCallback.onOpened` 只证明设备打开完成,不证明首个 capture result、首个输出 buffer 或首个预览 presentation。 +* 首个预览 timing 必须先建立稳定的 session/camera identity,再分别找到明确的 request、result、buffer 与 presentation 锚点;vendor slice-name inventory 单独只能标为 candidate。 +* 首帧到达时 AE/AF/AWB 仍可能处于 searching 状态。有 trace metadata 证据时应把 3A 状态单独报告,不能把“首帧已到达”等同于“3A 已收敛”。 +* `prepare()` 是 buffer 预分配的延迟/内存取舍:它可能减少后续分配抖动,也可能推迟首次输出并增加内存占用,因此不是 blanket first-frame fix。 + +### 6.5 典型掉帧场景 + +1. **Buffer Starvation 候选**: Consumer (`ImageReader`) 处理太慢、`image.close()` 不及时可能让 HAL 缺少空闲 Buffer。 + * *Trace*: 只有看到 buffer ownership、acquire/release、队列或 fence 证据时才把它报告为受支持的 backpressure 假设;`CameraProvider::dequeueBuffer` 等 vendor 名称单独仍只是 candidate。 +2. **Pipeline Stall 候选**: HDR、夜景等处理可能让 Camera pipeline 延迟增长,但不能仅凭回调间隔归因 ISP。 + * *Trace*: 只有在同一 session/request/frame identity 下串联 request、result、各目标 buffer/fence 与 presentation 证据,并能把延迟定位到对应阶段时,才把 stall 报告为受支持的候选;`onCaptureCompleted` 与 `onCaptureStarted` 的间隔本身不足以定位 ISP。 3. **Binder Congestion**: CameraService 与 App 之间的 IPC 拥塞。 * *Trace*: `binder transaction` 耗时异常。 diff --git a/docs/superpowers/plans/2026-07-12-camera-evidence-first-analysis.md b/docs/superpowers/plans/2026-07-12-camera-evidence-first-analysis.md new file mode 100644 index 000000000..a142f306d --- /dev/null +++ b/docs/superpowers/plans/2026-07-12-camera-evidence-first-analysis.md @@ -0,0 +1,554 @@ +# Camera Evidence-First Analysis Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a fact-checked Camera capture and analysis path that reports which evidence is present, which evidence is vendor-specific, and which evidence is missing before making Camera latency or memory claims. + +**Architecture:** Extend the existing deterministic capture preset/intent registry, then add one composite YAML Skill that consumes stable Perfetto tables and optional Pixel stdlib tables. Keep capture routing, evidence collection, rendering-pipeline classification, reports, snapshots, CLI output, and public Skill projection on their existing boundaries; do not add Camera-specific frontend or agent-runtime branches. + +**Tech Stack:** Node.js 24, TypeScript strict mode, Jest, Perfetto TraceConfig textproto, PerfettoSQL stdlib, SmartPerfetto YAML Skills, Markdown documentation. + +## Global Constraints + +- Treat `openCamera -> onOpened`, first capture result, first output buffer, and first presented preview frame as different milestones. Never collapse them into one metric. +- Do not require 3A convergence for first-frame latency and do not encode `CameraCaptureSession.prepare()` as a universal optimization. +- Treat names such as `processCaptureRequest` and `processCaptureResult` as vendor/app slice candidates, not stable platform contracts. +- Do not emit fixed good/bad thresholds or infer a memory leak from PSS/RSS growth alone. +- Keep `camera_trace_evidence` executable on a non-Camera trace: every optional source must return a typed coverage row or an empty detail list, never a missing-table error. +- Preserve the user's existing `.gitignore` modification and all unrelated worktree changes. +- Do not edit generated files, the `perfetto/` submodule, committed frontend assets, provider/session code, or report/snapshot contracts. + +--- + +### Task 1: Add a deterministic Camera capture contract + +**Files:** +- Modify: `backend/src/services/__tests__/traceCaptureConfig.test.ts` +- Modify: `backend/src/services/__tests__/traceConfigProposal.test.ts` +- Modify: `backend/src/cli-user/services/__tests__/captureConfig.test.ts` +- Modify: `backend/src/cli-user/commands/__tests__/captureSuggest.test.ts` +- Modify: `backend/src/cli-user/bin.ts` +- Modify: `backend/src/services/traceConfigGenerator.ts` +- Modify: `backend/src/services/traceCaptureConfig.ts` +- Modify: `backend/src/services/traceConfigProposal.ts` + +- [ ] **Step 1: Write the failing preset and routing tests** + +Add focused assertions equivalent to: + +```ts +it('renders the Camera preset with binder, FrameTimeline, and DMA-BUF evidence', () => { + const preset = getCapturePreset('camera'); + const config = renderAndroidTraceConfig({ + target: 'android', + preset: 'camera', + app: 'com.example.camera', + durationSeconds: 20, + }); + + expect(preset.intent).toBe('camera'); + expect(config).toContain('atrace_categories: "camera"'); + expect(config).toContain('atrace_categories: "hal"'); + expect(config).toContain('ftrace_events: "dmabuf_heap/dma_heap_stat"'); + expect(config).toContain('ftrace_events: "ion/ion_stat"'); + expect(config).toContain('ftrace_events: "binder/binder_transaction"'); + expect(config).toContain('name: "android.surfaceflinger.frametimeline"'); +}); + +it('routes Camera first-frame requests ahead of generic startup', () => { + const proposal = buildTraceConfigProposal({ + request: '分析 Camera 打开到首帧预览延迟', + app: 'com.example.camera', + outputLanguage: 'zh-CN', + }); + expect(proposal.preset).toBe('camera'); + expect(proposal.intent).toBe('camera'); +}); + +it('keeps generic app first-frame requests on startup', () => { + expect(buildTraceConfigProposal({request: 'debug app first frame'}).preset) + .toBe('startup'); +}); +``` + +Mirror the existing CLI service and command style to assert that `camera` is accepted by `capture config --preset`, appears in preset listings, and is selected by `capture suggest` for a Camera-domain request. Update both hardcoded preset lists in `backend/src/cli-user/bin.ts`: the `--preset` help text and `parseCapturePreset` error text. + +- [ ] **Step 2: Run the focused tests and confirm RED** + +Run: + +```bash +cd backend +npx jest \ + src/services/__tests__/traceCaptureConfig.test.ts \ + src/services/__tests__/traceConfigProposal.test.ts \ + src/cli-user/services/__tests__/captureConfig.test.ts \ + src/cli-user/commands/__tests__/captureSuggest.test.ts --runInBand +npx tsx src/cli-user/bin.ts capture android --help | grep -q camera +``` + +Expected: Jest failures because `camera` is not a valid preset/intent and Camera requests currently collide with `startup`/`scrolling` keywords; the CLI help smoke exits non-zero because the preset help does not list `camera` yet. + +- [ ] **Step 3: Add the Camera intent and capture preset** + +Extend `TraceIntent` with `'camera'`, add `CAMERA_FRAGMENTS`, and handle it in `pickFragmentsForIntent`: + +```ts +const CAMERA_FRAGMENTS: PerfettoConfigFragment[] = [ + { + dataSource: 'linux.ftrace', + reason: 'camera request activity, scheduler, binder, DMA-BUF/ION allocation events, and vendor atrace slices', + options: { + sched_switch: 'true', + sched_blocked_reason: 'true', + binder_transaction: 'true', + dma_heap_stat: 'true', + ion_stat: 'true', + }, + }, + { + dataSource: 'android.surfaceflinger.frametimeline', + reason: 'presented preview frame correlation when FrameTimeline is available', + }, +]; +``` + +Add a shared `CAMERA_MEMORY_EVENTS` constant and a `camera` preset: + +```ts +const CAMERA_MEMORY_EVENTS = [ + 'dmabuf_heap/dma_heap_stat', + 'ion/ion_stat', +]; + +{ + id: 'camera', + label: 'Android Camera', + intent: 'camera', + defaultDurationSeconds: 20, + bufferSizeKb: 98304, + atraceCategories: ['camera', 'hal', 'gfx', 'view', 'binder_driver', 'freq', 'sched'], + ftraceEvents: [...COMMON_FTRACE_EVENTS, ...BINDER_EVENTS, ...CAMERA_MEMORY_EVENTS], + dataSources: [...COMMON_DATA_SOURCES, 'android.surfaceflinger.frametimeline'], + description: 'Camera request, binder, scheduler, preview presentation, and DMA-BUF/ION allocation evidence.', +}, +``` + +Also add `CAMERA_MEMORY_EVENTS` to the `full` preset so its documented maximum-coverage behavior includes these evidence sources. + +- [ ] **Step 4: Make Camera routing domain-aware** + +Extend `IntentRule` with `requiredKeywords?: string[]`, add a Camera rule before startup, and calculate a deterministic domain bonus only when at least one required Camera token matches: + +```ts +{ + preset: 'camera', + confidence: 'high', + rationale: 'Camera investigations need request activity, binder, scheduler, preview presentation, and DMA-BUF/ION allocation evidence.', + requiredKeywords: [ + 'camera', 'camera2', 'camerax', 'cameraserver', 'camera hal', + '摄像头', '相机', '取景器', + ], + keywords: [ + 'open camera', 'camera open', 'camera startup', 'first preview', + 'preview frame', 'capture request', 'capture result', 'hal3', + '打开相机', '相机启动', '首帧预览', '预览首帧', '拍照延迟', + ], +}, +``` + +In `classifyRequest`, exclude any rule whose `requiredKeywords` do not match; for a matched domain rule add a score larger than the maximum possible generic keyword count. Keep ambiguous `preview` / `预览` as scoring keywords rather than domain anchors so a generic UI preview request is not stolen by Camera routing. This makes `Camera + 首帧` deterministic without reclassifying generic `app first frame` or `preview first frame` requests. Add the Chinese preset rationale and leave all existing fallback behavior intact. + +- [ ] **Step 5: Run the focused tests and confirm GREEN** + +Run the command from Step 2. Expected: all four suites pass. + +- [ ] **Step 6: Commit the capture contract** + +```bash +git add \ + backend/src/services/traceConfigGenerator.ts \ + backend/src/services/traceCaptureConfig.ts \ + backend/src/services/traceConfigProposal.ts \ + backend/src/services/__tests__/traceCaptureConfig.test.ts \ + backend/src/services/__tests__/traceConfigProposal.test.ts \ + backend/src/cli-user/services/__tests__/captureConfig.test.ts \ + backend/src/cli-user/commands/__tests__/captureSuggest.test.ts \ + backend/src/cli-user/bin.ts +git commit -m "feat: add evidence-first camera capture preset" +``` + +--- + +### Task 2: Correct Camera request-activity semantics + +**Files:** +- Modify: `backend/src/tests/renderingPipelineDetectionGenerator.test.ts` +- Modify: `backend/src/services/renderingPipelineDetectionSkillGenerator.ts` +- Modify: `backend/skills/atomic/pipeline_4feature_scoring.skill.yaml` +- Modify: `backend/skills/pipelines/android_view_mixed.skill.yaml` +- Modify: `backend/skills/pipelines/surfaceview_blast.skill.yaml` +- Modify: `backend/skills/pipelines/camera_pipeline.skill.yaml` +- Modify: `docs/rendering_pipelines/camera_pipeline.md` + +- [ ] **Step 1: Write the failing generator regression** + +Extend the existing generator test: + +```ts +const rhythmStep = skill.steps?.find((s) => s.id === 'rhythm_source_signals') as any; +expect(rhythmStep.sql).toContain("THEN 'camera_request_activity'"); +expect(rhythmStep.sql).not.toContain('camera_sensor_trigger'); +``` + +- [ ] **Step 2: Run the focused test and confirm RED** + +```bash +cd backend +npx jest src/tests/renderingPipelineDetectionGenerator.test.ts --runInBand +``` + +Expected: failure because the generated SQL still labels a candidate request slice as a sensor trigger. + +- [ ] **Step 3: Rename the evidence label at every source location** + +Change `camera_sensor_trigger` and `sensor_trigger_not_vsync_aligned` to `camera_request_activity` and `camera_request_activity_not_vsync_aligned` in the generator and affected pipeline Skills. Keep the detection condition as a candidate slice-name match, but change user-facing explanations from “sensor trigger” to “Camera request activity candidate”. Do not rename unrelated pipeline IDs or generated files. + +- [ ] **Step 4: Remove overclaims from the Camera pipeline knowledge** + +Apply these exact semantic corrections in `camera_pipeline.skill.yaml` and `camera_pipeline.md`: + +- Camera2/HAL3 requests are ordered and multiple requests may be in flight; each request can produce partial metadata and one or more buffers. +- `onOpened` proves device-open completion, not first result, first buffer, or first preview presentation. +- First preview timing requires stable session/camera identity plus explicit request/result/buffer/presentation anchors; vendor slice-name inventory alone cannot prove it. +- 3A can still be searching when the first frame arrives; report 3A state separately when trace evidence exists. +- `prepare()` is a buffer preallocation tradeoff that may delay first output and increase memory; it is not a blanket first-frame fix. +- ZSL behavior and buffer topology are capability-, implementation-, and app-dependent. +- ImageReader backpressure is only a supported hypothesis when buffer ownership/acquire-release evidence exists; PSS/RSS growth alone is insufficient to call a leak. +- Pixel `pixel.camera` parsing is an optional vendor-specific fast path, not a portable Android Camera contract. + +- [ ] **Step 5: Run the focused generator test and Skill validation** + +```bash +cd backend +npx jest src/tests/renderingPipelineDetectionGenerator.test.ts --runInBand +npm run validate:skills +``` + +Expected: generator regression and all Skill schemas pass. + +- [ ] **Step 6: Commit the semantic corrections** + +```bash +git add \ + backend/src/tests/renderingPipelineDetectionGenerator.test.ts \ + backend/src/services/renderingPipelineDetectionSkillGenerator.ts \ + backend/skills/atomic/pipeline_4feature_scoring.skill.yaml \ + backend/skills/pipelines/android_view_mixed.skill.yaml \ + backend/skills/pipelines/surfaceview_blast.skill.yaml \ + backend/skills/pipelines/camera_pipeline.skill.yaml \ + docs/rendering_pipelines/camera_pipeline.md +git commit -m "fix: make camera pipeline evidence conditional" +``` + +--- + +### Task 3: Add the `camera_trace_evidence` Skill + +**Files:** +- Create: `backend/skills/composite/camera_trace_evidence.skill.yaml` +- Create: `backend/src/services/skillEngine/__tests__/cameraTraceEvidenceSchema.test.ts` +- Create: `backend/tests/skill-eval/camera_trace_evidence.eval.ts` + +- [ ] **Step 1: Write all failing Skill contract and real-trace tests** + +Load the YAML using the same helper/pattern as neighboring Skill schema tests and assert: + +```ts +expect(skill).toMatchObject({ + name: 'camera_trace_evidence', + type: 'composite', + category: 'rendering', +}); +expect(skill.prerequisites.modules).toEqual(expect.arrayContaining([ + 'slices.with_context', + 'android.binder', + 'android.frames.timeline', + 'android.memory.dmabuf', + 'linux.cpu.frequency', + 'pixel.camera', +])); +expect(stepIds).toEqual([ + 'evidence_coverage', + 'camera_process_candidates', + 'camera_slice_candidates', + 'camera_binder_summary', + 'camera_dmabuf_summary', + 'pixel_camera_stage_summary', +]); +``` + +Also inspect every `display.columns` definition so coverage includes the approved stable schema `evidence_family`, `status`, `row_count`, `source`, and `limitation`, while detail steps expose typed timestamp/duration/process/thread/source fields where applicable. + +Before the Skill exists, also create `camera_trace_evidence.eval.ts` using `describeWithTrace('camera_trace_evidence skill', 'launch_light.pftrace', ...)`. Execute `evidence_coverage`, `camera_process_candidates`, `camera_slice_candidates`, `camera_binder_summary`, `camera_dmabuf_summary`, and `pixel_camera_stage_summary`; assert each succeeds without missing-table errors. Assert the eight required evidence families are present—Camera process/thread identity, Camera candidate slices, Binder, scheduler, CPU frequency, FrameTimeline, DMA-BUF, and optional Pixel Camera—and statuses are one of `available`, `vendor_specific`, or `missing`. Assert that Pixel rows are either a typed detail list or empty. + +- [ ] **Step 2: Run the schema test and confirm RED** + +```bash +cd backend +npx jest src/services/skillEngine/__tests__/cameraTraceEvidenceSchema.test.ts --runInBand +``` + +Then run the real trace evaluation as part of the same RED phase: + +```bash +npx jest tests/skill-eval/camera_trace_evidence.eval.ts --runInBand +``` + +Expected: both suites fail because the Skill file does not exist. The schema test proves the static contract, while the real-trace test is written before production SQL so malformed or missing-table SQL cannot reach GREEN without executing successfully. + +- [ ] **Step 3: Implement the composite Skill** + +Create a tier-B composite Skill with Camera/open/preview/capture/DMA-BUF triggers, no required input, optional `start_ts`, `end_ts`, and `max_rows`, and these modules: + +```yaml +prerequisites: + required_tables: [slice, thread, process] + modules: + - slices.with_context + - android.binder + - android.frames.timeline + - android.memory.dmabuf + - linux.cpu.frequency + - pixel.camera +``` + +Use a single-row-per-evidence-family coverage query. Because included Perfetto modules create empty tables when their trace events are absent, query their row counts directly. The query must cover Camera process/thread identity, vendor Camera slices, Camera-related Binder transactions, scheduler rows, CPU-frequency rows, FrameTimeline, DMA-BUF allocations, and optional Pixel Camera rows. The stable row shape is: + +```sql +SELECT 'camera_process_thread_identity' AS evidence_family, + CASE WHEN candidate_count > 0 THEN 'available' ELSE 'missing' END AS status, + candidate_count AS row_count, + 'process,thread' AS source, + 'Name-based identity candidates are not proof of a specific Camera milestone.' AS limitation +FROM ( + SELECT + (SELECT COUNT(*) FROM process + WHERE lower(COALESCE(name, '')) GLOB '*camera*' + OR lower(COALESCE(name, '')) GLOB '*camx*' + OR lower(COALESCE(name, '')) GLOB '*mtkcam*') + + + (SELECT COUNT(*) FROM thread + WHERE lower(COALESCE(name, '')) GLOB '*camera*' + OR lower(COALESCE(name, '')) GLOB '*camx*' + OR lower(COALESCE(name, '')) GLOB '*mtkcam*') AS candidate_count +) +UNION ALL +SELECT 'camera_slice_candidates' AS evidence_family, + CASE WHEN COUNT(*) > 0 THEN 'vendor_specific' ELSE 'missing' END, + COUNT(*), + 'thread_slice', + 'Slice names are implementation-specific candidates and require identity/anchor verification.' +FROM thread_slice +WHERE lower(COALESCE(name, '')) GLOB '*camera*' + OR lower(COALESCE(name, '')) GLOB '*capture*' + OR lower(COALESCE(name, '')) GLOB '*preview*' +UNION ALL +SELECT 'binder_transactions' AS evidence_family, + CASE WHEN COUNT(*) > 0 THEN 'available' ELSE 'missing' END, + COUNT(*), + 'android_binder_txns', + 'Binder rows provide cross-process correlation; interface names and endpoints must still be verified.' +FROM android_binder_txns +WHERE lower(COALESCE(client_process, '')) GLOB '*camera*' + OR lower(COALESCE(server_process, '')) GLOB '*camera*' + OR lower(COALESCE(client_process, '')) GLOB '*camx*' + OR lower(COALESCE(server_process, '')) GLOB '*camx*' + OR lower(COALESCE(client_process, '')) GLOB '*mtkcam*' + OR lower(COALESCE(server_process, '')) GLOB '*mtkcam*' +UNION ALL +SELECT 'scheduler_context', + CASE WHEN COUNT(*) > 0 THEN 'available' ELSE 'missing' END, + COUNT(*), + 'sched_slice', + 'Scheduler rows provide execution context only after Camera thread identity is established.' +FROM sched_slice +UNION ALL +SELECT 'cpu_frequency_context', + CASE WHEN COUNT(*) > 0 THEN 'available' ELSE 'missing' END, + COUNT(*), + 'cpu_frequency_counters', + 'CPU-frequency rows provide system context and do not by themselves attribute Camera latency.' +FROM cpu_frequency_counters +UNION ALL +SELECT 'frame_timeline', + CASE WHEN COUNT(*) > 0 THEN 'available' ELSE 'missing' END, + COUNT(*), + 'actual_frame_timeline_slice', + 'FrameTimeline can support presentation correlation only after preview surface identity is established.' +FROM actual_frame_timeline_slice +UNION ALL +SELECT 'dmabuf_allocations', + CASE WHEN COUNT(*) > 0 THEN 'available' ELSE 'missing' END, + COUNT(*), + 'android_dmabuf_allocs', + 'DMA-BUF allocation deltas are memory evidence; they do not alone prove a leak.' +FROM android_dmabuf_allocs +UNION ALL +SELECT 'pixel_camera_frames', + CASE WHEN COUNT(*) > 0 THEN 'vendor_specific' ELSE 'missing' END, + COUNT(*), + 'pixel_camera_frames', + 'pixel.camera is an optional Pixel slice parser, not a portable Android Camera contract.' +FROM pixel_camera_frames; +``` + +For process and slice candidates, use `lower(COALESCE(name, ''))` with Camera/vendor patterns (`camera`, `cameraserver`, `camera.provider`, `camx`, `mtkcam`) and return identity fields. For Binder, include only transactions whose client/server process matches those candidate patterns. For DMA-BUF, aggregate signed `buf_size` by process and report allocation count, allocation bytes, release bytes, observed net delta within the selected window, and peak event size. Never call that windowed net delta retained-at-trace-end memory or a leak: allocations may predate the window and releases may occur after it. For Pixel stages, group by `cam_id`, `node`, and `port_group`, returning frame count plus average/max duration. Clamp `max_rows` to 1–100 and normalize optional time bounds with `trace_start()`/`trace_end()`. + +Every step must declare `display.layer`, `display.level`, typed columns, stable source/identity fields, `save_as`, and an evidence-oriented `synthesize` mapping. Do not add diagnostic rules with fixed thresholds. + +- [ ] **Step 4: Run both prewritten tests and Skill validation to confirm GREEN** + +```bash +cd backend +npx jest src/services/skillEngine/__tests__/cameraTraceEvidenceSchema.test.ts --runInBand +npx jest tests/skill-eval/camera_trace_evidence.eval.ts --runInBand +npm run validate:skills +``` + +Expected: both tests and Skill validation pass. The Skill executes on `launch_light.pftrace`; absent Camera/Pixel evidence is represented as coverage/empty data, not an exception. If the local fixture is absent, the suite must be explicitly skipped by `describeWithTrace` and the later six-trace regression remains the executable integration gate. + +- [ ] **Step 5: Commit the Skill and tests** + +```bash +git add \ + backend/skills/composite/camera_trace_evidence.skill.yaml \ + backend/src/services/skillEngine/__tests__/cameraTraceEvidenceSchema.test.ts \ + backend/tests/skill-eval/camera_trace_evidence.eval.ts +git commit -m "feat: add camera trace evidence skill" +``` + +--- + +### Task 4: Export and document the Camera workflow + +**Files:** +- Modify: `backend/skills/public-export.yaml` +- Modify: `README.md` +- Modify: `README.zh-CN.md` +- Modify: `docs/reference/cli.md` +- Modify: `docs/reference/cli.en.md` + +- [ ] **Step 1: Add the public projection policy entry** + +Add an exported runtime-skill mapping entry under `skills:` adjacent to the other rendering-pipeline workflows: + +```yaml +camera_trace_evidence: + source: backend/skills/composite/camera_trace_evidence.skill.yaml + workflow: rendering-pipeline + disposition: exported + destination: references/generated/skills/camera_trace_evidence.md +``` + +- [ ] **Step 2: Document the preset and its evidence limits** + +Add `camera` to the preset tables/lists and include one command in each language: + +```bash +smp capture suggest --request "分析 Camera 打开到首帧预览延迟" --app com.example.camera +smp capture config --preset camera --app com.example.camera --duration 20 +``` + +State that the preset collects Camera/vendor atrace candidates, Binder, scheduler, FrameTimeline, and DMA-BUF/legacy ION events. Explicitly say that a trace may still lack portable open/result/buffer/presentation anchors, and SmartPerfetto will report that gap instead of fabricating a first-frame number. + +- [ ] **Step 3: Commit the SmartPerfetto export policy and docs** + +```bash +git add \ + backend/skills/public-export.yaml \ + README.md README.zh-CN.md \ + docs/reference/cli.md docs/reference/cli.en.md +git commit -m "docs: expose camera evidence workflow" +``` + +- [ ] **Step 4: Regenerate and verify the sibling public projection** + +First inspect the sibling `Perfetto-Skills` checkout and preserve any unrelated local changes. If it is clean or its changes do not overlap generated export outputs, run its exporter without `--check`, validate the catalog, and commit only the generated projection/provenance changes: + +```bash +PUBLIC_REPO=/Users/chris/Code/SmartPerfetto/Perfetto-Skills +git -C "$PUBLIC_REPO" status --short +python3 "$PUBLIC_REPO/tools/export_from_smartperfetto.py" \ + --source "$PWD" +python3 "$PUBLIC_REPO/tools/validate_catalog.py" \ + --catalog "$PUBLIC_REPO/catalog/smartperfetto-export.json" \ + --skill-root "$PUBLIC_REPO/skills/perfetto-performance-analysis" +git -C "$PUBLIC_REPO" add \ + catalog/smartperfetto-export.json \ + skills/perfetto-performance-analysis +git -C "$PUBLIC_REPO" commit -m "feat: export camera evidence workflow" +PERFETTO_SKILLS_DIR="$PUBLIC_REPO" npm run verify:public-skills +``` + +If the sibling checkout is absent, missing tools, or has overlapping local changes, record the exact `NOT AVAILABLE` or conflict reason and do not hand-edit generated public files. Do not push either repository. + +--- + +### Task 5: Verify, simplify, and review the complete change + +**Files:** +- Review: all files changed in Tasks 1–4 + +- [ ] **Step 1: Run the complete project-defined verification** + +Run from the repository root: + +```bash +npm run verify:pr +``` + +This is the repository's required pre-landing gate and includes Skill validation, backend type/build/tests, CLI checks, trace-processor availability, and the six-trace scene regression. Do not replace it with checks from another repository. + +- [ ] **Step 2: Run the configured simplifier or the documented fallback** + +Use the first available option in this order: + +1. current-environment `/simplify`; +2. a simplification script explicitly defined by the repository; +3. `code-simplifier` on only the files changed by this plan. + +If none is available, perform a manual behavior-preserving simplification review of only those files and run: + +```bash +git diff --check +``` + +Record simplifier availability; unavailable simplification tooling is not a blocker. + +- [ ] **Step 3: Perform an architecture and evidence audit** + +Confirm from the final diff that: + +- no Camera prompt prose or SQL was hardcoded in TypeScript; +- capture presets still flow through the shared registry and renderer; +- Camera-domain routing does not steal generic app first-frame requests; +- no metric conflates device-open, capture result, output buffer, or preview presentation; +- vendor slice matches are labeled as candidates; +- no fixed latency threshold, universal `prepare()` recommendation, or PSS/RSS leak verdict was added; +- optional Pixel and DMA-BUF tables are safe on traces with zero rows; +- DataEnvelope metadata remains usable by chat, reports, snapshots, CLI, and comparison; +- no unrelated worktree file, generated frontend artifact, or Perfetto submodule revision changed. + +- [ ] **Step 4: Re-run affected checks after any revision** + +Run the smallest focused suite for each revision, then repeat `npm run verify:pr` if runtime/Skill behavior changed after the full gate. + +- [ ] **Step 5: Record final repository state** + +```bash +git status --short +git log --oneline -5 +``` + +Expected: only the user's pre-existing `.gitignore` change remains uncommitted; implementation commits are present locally and nothing has been pushed. diff --git a/docs/superpowers/plans/2026-07-13-trace-case-corpus.md b/docs/superpowers/plans/2026-07-13-trace-case-corpus.md new file mode 100644 index 000000000..cc0883dfe --- /dev/null +++ b/docs/superpowers/plans/2026-07-13-trace-case-corpus.md @@ -0,0 +1,364 @@ +# Trace Case Corpus Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a manifest-driven real and constructed Perfetto trace corpus under `Trace/`, migrate the six existing fixtures, enforce exact Skill/Strategy coverage, and provide reproducible build and release-regression commands. + +**Architecture:** A Git-native catalog is the source of truth. Real cases retain captured binaries and evidence; constructed cases store declarative scenarios and small protobuf overlays that are composed with a real base trace into ignored build outputs. Generated indexes, a catalog resolver, and executable coverage expectations eliminate hard-coded fixture lists. + +**Tech Stack:** Node.js 24 CommonJS tooling, `node:test`, JSON Schema-compatible validation, the repository's `protobufjs` and YAML dependencies, pinned `trace_processor_shell`, existing TypeScript Skill evaluator, Markdown/JSON generated indexes. + +## Global Constraints + +- The root directory is exactly `Trace/`, with separate `real/` and `constructed/` libraries. +- Existing unrelated changes remain untouched. +- Prompt content stays in strategies and deterministic SQL stays in Skills. +- No generated file is hand-edited; generators own `Trace/README.md`, sub-indexes, `catalog.json`, `coverage.json`, and overlay binaries. +- Public real cases require explicit license, consent/source, privacy review, and sanitization review. +- New real traces stage under ignored `Trace/real/.private/`; tracked private binaries are invalid. +- Coverage inventory is derived from current Skill YAML and Strategy frontmatter; counts are never hard-coded. +- Constructed output records Android compatibility, base hash, generator version, and trace-processor version. +- The existing project verification commands in `.claude/rules/testing.md` remain authoritative. + +--- + +### Task 1: Corpus schemas and catalog validator + +**Files:** +- Create: `Trace/schema/case.schema.json` +- Create: `Trace/schema/scenario.schema.json` +- Create: `Trace/tools/lib/catalog.cjs` +- Create: `Trace/tools/lib/hash.cjs` +- Create: `Trace/tools/__tests__/catalog.test.cjs` + +**Interfaces:** +- Produces: `loadCatalog(repoRoot)`, `validateCatalog(repoRoot, options)`, `discoverCoverageTargets(repoRoot)`, and `resolveCaseTrace(repoRoot, selector)`. +- Consumes: `case.json` files under `Trace/real/*` and `Trace/constructed/*`. + +- [ ] **Step 1: Write failing catalog tests** + +Use `node:test` fixtures to assert duplicate ids, path traversal, missing files, +hash drift, invalid public provenance, stale coverage ids, missing Skill/Strategy +coverage, and legacy filename resolution. The positive fixture must contain one +real and one constructed case. + +- [ ] **Step 2: Verify the tests fail for the missing module** + +Run: `node --test Trace/tools/__tests__/catalog.test.cjs` + +Expected: FAIL with `Cannot find module '../lib/catalog.cjs'`. + +- [ ] **Step 3: Implement schemas, hashing, discovery, and validation** + +Implement exact runtime inventory by parsing YAML `name` fields and Strategy +frontmatter ids/names. Validate explicit expectations, safe relative paths, +files/hashes, globally unique ids, case kind/directory agreement, construction +references, Android API/range consistency, and the public publication gate. + +- [ ] **Step 4: Verify catalog tests pass** + +Run: `node --test Trace/tools/__tests__/catalog.test.cjs` + +Expected: PASS with zero failed tests. + +### Task 2: Deterministic Perfetto overlay generator + +**Files:** +- Create: `Trace/tools/lib/perfetto-proto.cjs` +- Create: `Trace/tools/lib/generator.cjs` +- Create: `Trace/tools/__tests__/generator.test.cjs` +- Create: `Trace/constructed/_templates/scenario.example.json` + +**Interfaces:** +- Consumes: `buildCase(repoRoot, caseManifest, options)` and scenario JSON signal definitions. +- Produces: valid `trace.overlay.pftrace`, materialized base-plus-overlay trace, and `build-provenance.json`. + +- [ ] **Step 1: Write failing generator tests** + +Cover deterministic byte output, base-plus-overlay protobuf concatenation, +lossless 64-bit timestamps, non-conflicting identity allocation, incremental +state isolation, process/thread and slice packets, scheduler rows, +frame-timeline rows, counter rows, malformed scenario rejection, and changed +base/scenario hash detection. + +- [ ] **Step 2: Verify generator tests fail** + +Run: `node --test Trace/tools/__tests__/generator.test.cjs` + +Expected: FAIL with missing generator module. + +- [ ] **Step 3: Implement proto loading and signal builders** + +Load `perfetto/protos/perfetto/trace/trace.proto` through `protobufjs` with a +repository-root import resolver. Implement focused builders for process trees, +ftrace/atrace, scheduler and blocked-reason events, Binder, TrackEvent slices, +frame timeline, counters, GPU, memory, power/thermal, input, Android logs, +media/network, and named pipeline tags. + +- [ ] **Step 4: Implement materialization and parse verification** + +Probe base bounds and existing identities with the pinned trace processor, place +scenario events inside the bounds, allocate deterministic non-conflicting ids, +encode isolated overlay sequences, concatenate protobuf bytes, reload the +combined trace, and emit hashes plus tool versions. + +- [ ] **Step 5: Verify generator tests pass** + +Run: `node --test Trace/tools/__tests__/generator.test.cjs` + +Expected: PASS and the test-created trace contains the asserted SQL rows. + +### Task 3: CLI, generated indexes, and safe real-case import + +**Files:** +- Create: `Trace/tools/trace-corpus.cjs` +- Create: `Trace/tools/lib/indexer.cjs` +- Create: `Trace/tools/lib/import-real.cjs` +- Create: `Trace/tools/__tests__/cli.test.cjs` +- Generate: `Trace/README.md` +- Generate: `Trace/real/README.md` +- Generate: `Trace/constructed/README.md` +- Generate: `Trace/catalog.json` +- Generate: `Trace/coverage.json` + +**Interfaces:** +- Produces CLI commands `validate`, `index`, `build`, `import-real`, `promote-real`, `resolve`, and `coverage`. +- Consumes catalog and generator interfaces from Tasks 1-2. + +- [ ] **Step 1: Write failing CLI/import/index tests** + +Assert deterministic index order, Android matrix rendering, ignored +private-by-default imports, explicit promotion gates, rejection of tracked +private cases, copied result/log files, atomic failure cleanup, trace metadata +probe, and `index --check` stale-file detection. + +- [ ] **Step 2: Verify CLI tests fail** + +Run: `node --test Trace/tools/__tests__/cli.test.cjs` + +Expected: FAIL with missing CLI/helper modules. + +- [ ] **Step 3: Implement the CLI and staged import** + +Use an adjacent temporary directory, compute hashes, probe the trace, write a +private draft manifest under `Trace/real/.private/`, validate, and rename. +Promotion is a second explicit operation and never fills approval fields from +implicit defaults. + +- [ ] **Step 4: Implement generated indexes and check mode** + +Render links, scenes, Android/API ranges, publication state, base case, +coverage targets, results, and logs. Put a generated-file banner in every owned +artifact and compare bytes in `--check` mode. + +- [ ] **Step 5: Verify CLI tests pass** + +Run: `node --test Trace/tools/__tests__/cli.test.cjs` + +Expected: PASS with zero failed tests. + +### Task 4: Migrate the six real traces without losing evidence + +**Files:** +- Move: `test-traces/launch_light.pftrace` and report into `Trace/real/android-startup-light/` +- Move: `test-traces/lacunh_heavy.pftrace` and report into `Trace/real/android-startup-heavy/` +- Move: `test-traces/scroll_Standard-AOSP-App-Without-PreAnimation.pftrace` and report into `Trace/real/android-scroll-standard/` +- Move: `test-traces/scroll-demo-customer-scroll.pftrace` and report into `Trace/real/android-scroll-customer/` +- Move: `test-traces/Scroll-Flutter-327-TextureView.pftrace` and report into `Trace/real/flutter-scroll-texture-view/` +- Move: `test-traces/Scroll-Flutter-SurfaceView-Wechat-Wenyiwen.pftrace` and report into `Trace/real/flutter-scroll-surface-view/` +- Create: one `case.json` per directory + +**Interfaces:** +- Produces six real case ids and legacy filename aliases. +- Consumes `import-real`/catalog metadata rules. + +- [ ] **Step 1: Add a failing migration fixture test** + +Extend catalog tests to require all six legacy filenames to resolve, all six +trace hashes to match their manifests, and each historical FPS report to be +listed as an analysis result. + +- [ ] **Step 2: Verify the migration test fails against the flat directory** + +Run: `node --test Trace/tools/__tests__/catalog.test.cjs` + +Expected: FAIL because the six manifests do not exist. + +- [ ] **Step 3: Probe metadata and move files with Git history** + +Use `git mv`, keep original binary filenames as `trace.pftrace`, retain the +original report basename under `analysis/`, and write factual metadata only. +Unknown capture metadata remains `null`; logs remain `[]`. + +- [ ] **Step 4: Generate indexes and verify migration tests** + +Run: `node Trace/tools/trace-corpus.cjs index && node --test Trace/tools/__tests__/catalog.test.cjs` + +Expected: PASS; the generated real index contains six entries. + +### Task 5: Constructed scenario families and exact coverage assignment + +**Files:** +- Create under `Trace/constructed/`: case directories for scheduler contention, startup, frame/jank, input latency, Binder/I/O, memory pressure, power/thermal, GPU, system state, Linux runtime, media/network/camera, and framework rendering pipelines. +- Create per directory: `case.json`, `scenario.json`, `trace.overlay.pftrace`, `analysis/expected.json`. + +**Interfaces:** +- Produces explicit assignment for every discovered runtime Skill and Strategy. +- Consumes generator signal families and case expectation schema. + +- [ ] **Step 1: Add a failing 100% coverage assertion** + +The test computes current inventory and asserts `missing.skills`, +`missing.strategies`, `stale.skills`, and `stale.strategies` are empty and every +target has an executable expectation. + +- [ ] **Step 2: Verify the coverage assertion reports the complete missing set** + +Run: `node --test Trace/tools/__tests__/catalog.test.cjs --test-name-pattern='coverage'` + +Expected: FAIL and list the current uncovered ids without a hard-coded count. + +- [ ] **Step 3: Add scenarios and explicit target assignments by evidence family** + +Each case gets a single diagnostic purpose, stable fixture process/thread ids, +Android compatibility, required parameters, and predicates that distinguish a +real signal from graceful-empty execution. Strategy cases include routing query +fixtures and required trace evidence. + +- [ ] **Step 4: Build all constructed cases** + +Run: `node Trace/tools/trace-corpus.cjs build --all` + +Expected: every overlay and combined trace is parseable; provenance hashes are +written under `Trace/.generated/`. + +- [ ] **Step 5: Verify exact 100% inventory coverage** + +Run: `node Trace/tools/trace-corpus.cjs coverage --check` + +Expected: PASS with no missing/stale target and all expectations executable. + +### Task 6: Execute Skill and Strategy regression expectations + +**Files:** +- Create: `backend/tests/trace-corpus/trace_corpus_regression.ts` +- Create: `backend/tests/trace-corpus/trace_corpus_strategy_regression.test.ts` +- Modify: `backend/tests/skill-eval/runner.ts` +- Modify: `backend/package.json` + +**Interfaces:** +- Produces `npm run trace:validate`, `trace:build`, `trace:test`, and `trace:regression`. +- Consumes case selectors, per-Skill parameters, and predicates from `case.json`. + +- [ ] **Step 1: Write failing runner tests** + +Assert catalog resolution, one-load/many-Skill execution, parameter token +resolution, required-step checks, row/value predicates, graceful-empty checks, +strategy routing fixtures, and per-case result/log persistence. + +- [ ] **Step 2: Verify runner tests fail** + +Run: `cd backend && npx jest --runInBand tests/trace-corpus/trace_corpus_strategy_regression.test.ts` + +Expected: FAIL because the corpus runner is absent. + +- [ ] **Step 3: Implement the deterministic regression runner** + +Reuse `SkillEvaluator`; load each materialized trace once, execute all assigned +Skills, evaluate manifest predicates, and write evidence without changing chat, +report, or snapshot contracts. Strategy tests use the real loader/classifier. + +- [ ] **Step 4: Run the full corpus regression** + +Run: `cd backend && npm run trace:regression` + +Expected: PASS for all catalog cases and expectations. + +### Task 7: Remove legacy path coupling and integrate repository gates + +**Files:** +- Modify: all non-archived source/test/script files that directly reference `test-traces/` +- Modify: `scripts/e2e/dual-trace-e2e-paths.cjs` +- Modify: `.github/workflows/backend-agent-regression-gate.yml` +- Modify: `package.json` +- Modify: `backend/package.json` +- Modify: `.gitignore` + +**Interfaces:** +- Produces one catalog-based fixture resolver for TS/CJS/shell callers. +- Consumes real case ids and legacy aliases from Task 4. + +- [ ] **Step 1: Add a failing no-legacy-reference check** + +Catalog validation scans maintained source paths and rejects new direct +`test-traces/` references, excluding archived historical documentation. + +- [ ] **Step 2: Verify it fails and lists current callers** + +Run: `node Trace/tools/trace-corpus.cjs validate` + +Expected: FAIL with the maintained files that still use legacy paths. + +- [ ] **Step 3: Migrate callers and package scripts** + +Replace filename arrays with catalog selectors/resolution. Add root wrappers and +backend commands without changing existing scene regression behavior. + +- [ ] **Step 4: Add gate and path-filter integration** + +Run fast catalog validation in backend `verify:pr`. Trigger the regression +workflow for `Trace/**`, Skills, Strategies, corpus tools, and resolver changes. + +- [ ] **Step 5: Verify legacy coupling is gone** + +Run: `node Trace/tools/trace-corpus.cjs validate` + +Expected: PASS with no maintained direct `test-traces/` references. + +### Task 8: Documentation, build, regression, and completion audit + +**Files:** +- Modify: `docs/getting-started/quick-start.md` +- Modify: `docs/getting-started/quick-start.en.md` +- Modify: `.claude/rules/testing.md` +- Regenerate: all Trace indexes and reports + +**Interfaces:** +- Documents real-case ingest, constructed-case authoring, Android versioning, + privacy/publication gates, CI/release commands, and result/log locations. + +- [ ] **Step 1: Generate and check all owned artifacts** + +Run: `node Trace/tools/trace-corpus.cjs index && node Trace/tools/trace-corpus.cjs validate --check-generated` + +Expected: PASS with no stale generated file. + +- [ ] **Step 2: Run focused corpus tests and existing six-case regressions** + +Run: `node --test Trace/tools/__tests__/*.test.cjs` + +Run: `cd backend && npm run test:scene-trace-regression && npm run test:state-timeline-regression && npm run trace:regression` + +Expected: all commands exit 0 and preserve per-case evidence. + +- [ ] **Step 3: Run project build and PR verification** + +Run: `cd backend && npm run build` + +Run: `npm run verify:pr` + +Expected: both commands exit 0. + +- [ ] **Step 4: Simplify and review only changed code** + +Use the available project simplifier; if unavailable, perform an architectural +self-review, then run `git diff --check`. Confirm there is no duplicated catalog, +hard-coded inventory count, generated-file hand edit, or unrelated rewrite. + +- [ ] **Step 5: Audit every original requirement against current evidence** + +Check dual `Trace/` libraries, six migrated real cases, future atomic import, +results/log preservation, generated README indexes, reproducible constructed +traces, exact Skill/Strategy coverage, extension/version mechanics, Android +matrix, release regression, build, and tests. Any missing or indirect evidence +keeps the goal active. diff --git a/docs/superpowers/specs/2026-07-12-camera-evidence-first-design.md b/docs/superpowers/specs/2026-07-12-camera-evidence-first-design.md new file mode 100644 index 000000000..119036b99 --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-camera-evidence-first-design.md @@ -0,0 +1,278 @@ +# Camera Evidence-First Analysis Design + +## Context + +The article [Camera 性能分析:Perfetto 与延迟拆解实战](https://mp.weixin.qq.com/s/ozvWDCh9exIfqQ2v0vypdA) +contains a useful workflow—capture, quantify, decompose, and compare—but mixes +portable Android Camera contracts with device-specific trace names, illustrative +timings, and SQL that is not safe across cameras, sessions, tracks, or partial +results. + +SmartPerfetto already has four adjacent capabilities: + +- deterministic Android capture presets and capture proposal routing; +- a `CAMERA_PIPELINE` teaching/detection definition; +- DMA-BUF analysis through Perfetto's `android.memory.dmabuf` module; +- raw-trace and persisted-result comparison contracts. + +The missing product layer is a fact-checked Camera capture path plus a +deterministic evidence-coverage Skill. Without a representative Camera trace +fixture, the product must not claim portable Open/First-Frame stage timing. + +## Goals + +1. Add a first-class `camera` capture intent and preset to the existing CLI and + deterministic proposal flow. +2. Correct Camera runtime knowledge that currently overstates generic Android + guarantees or treats vendor slice names as semantic facts. +3. Add a portable `camera_trace_evidence` Skill that reports which Camera + evidence is actually present before any interpretation. +4. Reuse existing DataEnvelope, reports, snapshots, CLI artifacts, and + `compare_skill` behavior without adding a Camera-only output channel. +5. Keep every conclusion evidence-scoped and return `N/A` when the trace cannot + support a metric. + +## Non-Goals + +- No fixed Camera Open, first-frame, or pipeline-stage thresholds. +- No claim that `processCaptureRequest`, `processCaptureResult`, + `CameraService::connect`, or similar names exist on every device. +- No generic computation of request-to-result latency from unpartitioned slice + timestamps. +- No leak conclusion from PSS growth alone. +- No dedicated Camera UI, generated frontend contract, Perfetto submodule + change, or committed frontend rebuild. +- No automatic sensor pre-open, 3A reuse, dummy requests, or HAL tuning advice. + +## Fact-Checked Metric Boundaries + +SmartPerfetto will preserve these distinct concepts: + +| Metric | Start | End | Availability | +| --- | --- | --- | --- | +| Camera device open | `openCamera` invocation | `CameraDevice.StateCallback.onOpened` | Requires app/framework anchors; not inferred from arbitrary vendor slices | +| Session configuration | session creation request | `CameraCaptureSession.StateCallback.onConfigured` | Requires explicit anchors | +| Request-to-result metadata | submitted capture request | matching final `TotalCaptureResult` | Requires request/frame identity and must not mix partial results | +| First output buffer | first request for a Surface | first image/buffer available on that Surface | Surface-specific; distinct from display | +| Camera launch/startup | camera open start | first preview image available | CTS-compatible conceptual scope; includes open, configure, and preview start | +| First preview presented | camera open or request start | corresponding preview buffer presented | Requires buffer/frame identity through the display pipeline | + +The evidence Skill does not manufacture these metrics. It reports whether the +anchors and identities required for each one are present. + +## Authoritative Technical Rules + +- Camera2 is an ordered, multi-request pipeline. Each request produces result + metadata plus one or more output buffers. +- `onOpened` means the device is ready for capture-session configuration; it is + not equivalent to first preview. +- Camera CTS launch measurement includes device open, stream configuration, + and first preview image availability. +- Camera 3A starts inactive when a device opens. Stream reconfiguration within + the same open device does not reset 3A, but previous-device-session reuse is + not a portable guarantee. +- `CameraCaptureSession.prepare()` trades later steadiness for delayed first + output and potentially higher memory use; it is not a universal first-frame + optimization. +- `ImageReader` can stall its producer when acquired images reach `maxImages` + and are not closed. +- DMA-BUF allocation data comes from `dmabuf_heap/dma_heap_stat`; legacy ION + data may require `ion/ion_stat`. Binder tracing improves gralloc attribution. +- ATrace categories and vendor slice names vary by Android version and device. + `atrace_apps` uses an exact app name or the documented global `*`; partial + package wildcards are not part of the product contract. + +Primary references: + +- https://developer.android.com/topic/performance/tracing +- https://developer.android.com/reference/android/hardware/camera2/package-summary +- https://android.googlesource.com/platform/cts/+/c0dd022/tests/tests/hardware/src/android/hardware/camera2/cts/PerformanceTest.java +- https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession +- https://source.android.com/docs/core/camera/camera3_3Amodes +- https://developer.android.com/reference/android/media/ImageReader +- https://perfetto.dev/docs/getting-started/atrace +- https://perfetto.dev/docs/getting-started/memory-profiling +- https://perfetto.dev/docs/analysis/stdlib-docs + +## Architecture + +### 1. Capture Contract + +Extend `TraceIntent` and `CapturePresetId` with `camera`. The built-in preset is +a deterministic, portable superset for Camera investigation: + +- ATrace categories: `camera`, `hal`, `gfx`, `view`, `binder_driver`, `freq`, + and `sched`; +- ftrace: existing scheduler/frequency and Binder events plus + `dmabuf_heap/dma_heap_stat` and `ion/ion_stat`; +- data sources: existing process/system/log sources and SurfaceFlinger + FrameTimeline; +- default duration: 20 seconds; +- minimum buffer: 96 MiB. + +Unsupported optional ftrace events remain a trace capability issue rather than +a reason to invent data. The analysis Skill reports absence explicitly. + +The deterministic proposal classifier adds a Camera rule before generic +startup and scrolling rules. Camera-domain tokens such as `camera`, `相机`, +`camera2`, `cameraserver`, `camera HAL`, and `取景器` select the Camera preset. +Ambiguous `preview` / `预览` terms increase Camera-rule relevance but do not +establish the Camera domain by themselves. Generic `first frame`, `首帧`, or UI +preview requests without a Camera-domain token continue to select the startup +preset. + +### 2. Runtime Knowledge Correction + +Update the committed Camera pipeline doc and YAML definition so they describe +observable contracts rather than implementation assumptions: + +- distinguish request activity from sensor exposure; +- describe SurfaceView/TextureView and zero-copy paths as common or preferred, + not universal; +- make ZSL capability-dependent; +- remove the claim that a Camera request slice proves a sensor trigger; +- describe callback timestamps and callback delivery separately; +- preserve the existing warning that Camera trace visibility is OEM- and + configuration-dependent. + +The rendering-pipeline signal remains a heuristic named +`camera_request_activity`; it must not be labeled `camera_sensor_trigger`. + +### 3. `camera_trace_evidence` Skill + +Create a composite YAML Skill with four deterministic layers. + +#### L0: Coverage + +Return one row per evidence family: + +- Camera process/thread identity; +- Camera-related slices; +- Binder transactions; +- scheduler and CPU-frequency context; +- SurfaceFlinger FrameTimeline; +- DMA-BUF allocation events; +- Pixel Camera stdlib rows. + +Each row contains `evidence_family`, `status`, `row_count`, `source`, and +`limitation`. Status is one of `available`, `vendor_specific`, or `missing`. + +#### L1: Generic Camera Inventory + +List Camera-related processes, threads, and candidate slices with timestamp, +duration, process, thread, and slice name. Matching is intentionally broad and +the output is labeled `candidate`; names alone do not assign stage semantics. + +#### L2: Memory Evidence + +Reuse the `android.memory.dmabuf` module to summarize Camera-related DMA-BUF +allocation/release totals by process. Do not duplicate the existing deep +`dmabuf_analysis` lifecycle workflow. The new Skill exposes only the Camera +coverage and correlation entry point and recommends `dmabuf_analysis` for deep +investigation. + +#### L3: Pixel Camera Evidence + +Include `pixel.camera` and summarize `pixel_camera_frames` by camera id, node, +and port group when rows exist. Empty results are normal on non-Pixel traces and +must not generate an error or a generic Camera conclusion. + +The Skill does not emit Open/First-Frame timings. Its conclusion is limited to +what evidence exists and what can be analyzed next. + +### 4. Comparison + +No new comparison implementation is needed. `compare_skill` can execute +`camera_trace_evidence` against current and reference traces. Because the Skill +has a stable schema, capability and evidence-count differences can be compared +without pretending that missing vendor data is a performance delta. + +### 5. Public Projection and Documentation + +The new Skill is portable and belongs in `backend/skills/public-export.yaml`. +Update Chinese and English CLI reference documentation and README capture +examples for the `camera` preset. Correct the existing runtime-read Camera +pipeline document in place. + +## Data Flow + +```text +Natural-language capture request + -> deterministic Camera-aware classifier + -> camera capture preset + -> Perfetto trace with portable Camera context + -> camera_trace_evidence + -> coverage matrix + -> generic candidate inventory + -> Camera DMA-BUF correlation + -> optional Pixel frame/node evidence + -> DataEnvelope + -> chat / report / CLI artifact / snapshot / compare_skill +``` + +## Error and Uncertainty Handling + +- Missing evidence is a successful Skill result with `status=missing`, not a + fabricated zero-duration metric. +- Vendor-named candidates use `status=vendor_specific` until a stable module or + explicit user-provided mapping establishes semantics. +- Empty `pixel_camera_frames` means the Pixel branch is unavailable, not that + Camera activity was absent. +- No single-trace PSS or DMA-BUF increase is labeled a leak. +- Capture documentation calls DMA-BUF and ION events optional by device/kernel. +- Generic startup routing remains unchanged unless Camera-domain context is + present. + +## Testing Strategy + +Implementation follows red-green-refactor. + +1. Add failing unit tests for the `camera` preset, Camera-domain routing, and + generic first-frame routing preservation. +2. Add failing config assertions for Camera categories, FrameTimeline, + DMA-BUF, ION, scheduler, and Binder coverage. +3. Add a failing Skill contract test that loads `camera_trace_evidence`, checks + its stable coverage schema, and exercises safe empty results on an existing + non-Camera canonical trace. +4. Validate the Pixel branch SQL against the vendored Perfetto v57.2 stdlib + schema; no Pixel performance claim is asserted without fixture rows. +5. Run Skill and Strategy validators, scene regression, public Skill export + verification, focused capture tests, TypeScript checks, build, and the root + `npm run verify:pr` gate. + +## Files Expected to Change + +- `backend/src/services/traceConfigGenerator.ts` +- `backend/src/services/traceCaptureConfig.ts` +- `backend/src/services/traceConfigProposal.ts` +- focused tests under `backend/src/services/__tests__/` and + `backend/src/cli-user/services/__tests__/` +- `backend/src/cli-user/bin.ts` +- `backend/src/services/renderingPipelineDetectionSkillGenerator.ts` +- `backend/src/tests/renderingPipelineDetectionGenerator.test.ts` +- `backend/skills/atomic/pipeline_4feature_scoring.skill.yaml` +- `backend/skills/pipelines/android_view_mixed.skill.yaml` +- `backend/skills/pipelines/surfaceview_blast.skill.yaml` +- `backend/skills/pipelines/camera_pipeline.skill.yaml` +- `backend/skills/composite/camera_trace_evidence.skill.yaml` +- `backend/skills/public-export.yaml` +- `docs/rendering_pipelines/camera_pipeline.md` +- `README.md`, `README.zh-CN.md` +- `docs/reference/cli.md`, `docs/reference/cli.en.md` + +No frontend, Perfetto submodule, release, provider, session, or API contract +files should change. + +## Acceptance Criteria + +- Camera capture requests route to a dedicated preset without breaking generic + startup first-frame requests. +- The generated Camera config contains the fact-checked evidence sources. +- Runtime Camera knowledge contains no universal vendor slice assumption, no + fixed stage threshold, and no first-frame/3A conflation. +- `camera_trace_evidence` runs safely on a trace with no Camera evidence and + returns an explicit coverage result. +- Pixel Camera evidence is optional and schema-backed. +- The Skill is available through normal invocation, reports, snapshots, CLI, + and comparison without a Camera-specific adapter. +- All project-defined verification gates required by the touched surfaces pass. diff --git a/docs/superpowers/specs/2026-07-13-trace-case-corpus-design.md b/docs/superpowers/specs/2026-07-13-trace-case-corpus-design.md new file mode 100644 index 000000000..9cb887c3e --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-trace-case-corpus-design.md @@ -0,0 +1,213 @@ +# Trace Case Corpus Design + +## Purpose + +SmartPerfetto needs two source-controlled trace libraries under `Trace/`: + +1. A real-case library that preserves a representative trace together with its + analysis result, analysis logs, provenance, Android metadata, and a public + index. +2. A constructed-case library that deterministically adds targeted Perfetto + packets to a real base trace, producing repeatable regression fixtures for + every runtime Skill and Strategy. + +The corpus is a test and publication asset. It is deliberately separate from +`backend/knowledge/cases/`, whose Markdown files are recallable diagnostic +knowledge rather than binary trace fixtures. + +## Considered Approaches + +### Flat folders of trace files + +This would be easy to start but would preserve the current hard-coded coupling +between six filenames and individual tests. It cannot prove coverage, capture +provenance, or keep README indexes correct as the corpus grows. + +### Database or object-store catalog + +This would scale to private enterprise data, but it would make GitHub review, +offline use, and deterministic releases harder. It is not the right primary +format for a public reference corpus. + +### Selected: Git-native manifests plus reproducible overlays + +Each case is a directory with a versioned `case.json`. Real cases contain the +captured trace and evidence files. Constructed cases contain a declarative +scenario and a small valid Perfetto overlay trace. The build tool concatenates +the base `Trace` protobuf and overlay `Trace` protobuf into a materialized trace; +protobuf repeated packet fields make the concatenation valid, while avoiding +dozens of copies of the same large base trace in Git. + +Generated indexes and a coverage report are derived from manifests. No test or +documentation surface owns a second hard-coded case list. + +## Repository Layout + +```text +Trace/ + README.md generated public index and usage + catalog.json generated machine-readable catalog + coverage.json generated Skill/Strategy coverage report + schema/ + case.schema.json + scenario.schema.json + real/ + README.md generated real-case index + .private/ ignored local staging for new imports + / + case.json + trace.pftrace + analysis/ + result.* + logs/* + constructed/ + README.md generated constructed-case index + / + case.json + scenario.json + trace.overlay.pftrace + analysis/expected.json + tools/ + trace-corpus.cjs + lib/*.cjs + __tests__/*.test.cjs + .generated/ ignored materialized combined traces/logs +``` + +`Trace/.generated/constructed//trace.pftrace` is the directly openable +base-plus-overlay output. Release automation may archive this directory, but it +is not committed. + +## Case Contract + +Every `case.json` has these stable sections: + +- `schema_version`, `id`, `kind`, `title`, `description`, `scene`, and `tags`. +- `trace`: committed source filename, SHA-256, format, and materialization mode. +- `android`: release, API level, device/build metadata, and an optional tested + compatibility range. +- `source`: origin, capture/import time, license, consent, sanitization review, + and publication state. +- `analysis`: result files and log files that must exist. +- `coverage`: explicit Skill ids, Strategy ids, and executable expectations. +- `construction` for constructed cases: base case id, scenario filename, + generator version, deterministic seed, and output path. + +IDs are lowercase kebab-case and globally unique. Paths are relative, cannot +escape their case directory, and are checked for exact filename case. + +## Real-Case Ingest Flow + +`node Trace/tools/trace-corpus.cjs import-real ...` performs an atomic staged +import. It computes hashes, probes trace bounds and Android metadata with the +pinned `trace_processor_shell`, copies the trace/results/logs, writes the +manifest, validates the full catalog, and regenerates indexes. + +Imports default to `private/draft` under ignored `Trace/real/.private/`; they do +not enter the tracked catalog or generated public index. A separate explicit +promotion command moves a case into the tracked library only when its manifest +has a redistributable license, source/consent record, completed privacy review, +and completed sanitization review. Validation rejects tracked private cases and +public cases without those fields. This prevents an accidental `git add` from +publishing app, device, path, account, or logcat data. + +The six existing `test-traces/` fixtures are migrated with history-preserving +`git mv`. Their existing FPS reports become analysis results. Missing historical +analysis logs are represented as an empty list, not fabricated evidence. Since +the binaries already exist in Git history, their manifests record that legacy +publication fact while leaving unknown consent/license/privacy fields unknown; +the index must not misrepresent them as newly approved public examples. + +## Constructed Trace Model + +The generator loads Perfetto's checked-in `.proto` graph through the repository's +existing `protobufjs` dependency. It emits packets for the declared signal +families, including process/thread trees, atrace/TrackEvent slices, scheduler +switches and blocked reasons, Binder transactions, frame-timeline events, +counters, GPU work, memory pressure, power/thermal state, input, media/network, +and framework/pipeline tags. + +Scenario timestamps are relative to a safe anchor inside the base trace. The +builder probes the base bounds, translates relative timestamps, writes a valid +overlay, concatenates it with the base, and then verifies the combined trace by +loading it with the pinned `trace_processor_shell`. Every build records base, +scenario, overlay, output, generator, and trace-processor hashes/versions. + +All timestamps and protobuf `int64`/`uint64` values remain decimal strings or +`Long` values until encoding; JavaScript `number` is forbidden for nanosecond +timestamps. Before generation, the builder queries existing pids/tids, packet +sequence ids where observable, and track ids/UUIDs, then deterministically +allocates non-conflicting synthetic identities. Overlay packets clear +incremental state and use their own trusted sequence so they cannot inherit or +corrupt the base trace's interned data. + +Constructed cases are grouped by one diagnostic behavior, not by implementation +file. A case may cover several Skills that consume the same evidence family, +but every Skill and Strategy id is listed explicitly. New source files are not +covered by a wildcard: adding one makes the coverage gate fail until a maintainer +assigns it to an executable case. + +## Coverage Semantics + +Coverage is computed from current repository truth: + +- Runtime Skills are discovered from `backend/skills/**/*.skill.yaml`, excluding + authoring templates and using the YAML `name` as the stable runtime id. +- Strategies are discovered from `backend/strategies/*.strategy.md` frontmatter. +- A target counts as covered only when exactly one or more case manifests list + it and at least one executable expectation references it. +- Skill expectations execute the real Skill engine and assert success, required + steps, row/value predicates, or an explicit graceful-empty contract. +- Strategy expectations assert deterministic routing inputs plus required + trace evidence. Provider-backed end-to-end analysis is a separate optional + release tier and cannot replace deterministic coverage. + +`coverage.json` reports covered, duplicate, and missing targets. `validate` +fails on any missing target, stale target, missing expectation, hash drift, +schema error, stale generated index, or unparseable trace. This is the meaning +of 100% corpus coverage; it is not a raw line-coverage percentage. + +## Regression and Release Flow + +- `npm run trace:validate`: manifests, hashes, publication gates, indexes, and + exact Skill/Strategy inventory coverage. +- `npm run trace:build`: regenerate every overlay and materialized trace, then + verify parseability and provenance. +- `npm run trace:test`: execute deterministic case expectations and preserve + per-case JSON results plus logs under `Trace/.generated/`. +- `npm run trace:regression`: validate, build, and test the complete corpus. + +The backend PR gate runs `trace:validate` and the existing six-case scene +regression through the catalog resolver. Full corpus regression is suitable for +release and explicit corpus changes. CI path filters include `Trace/**`, the +corpus tools, Skills, and Strategies. + +## Compatibility and Migration + +Tests resolve fixtures by case id or legacy filename through one catalog helper. +There is no compatibility symlink because repository symlinks are unreliable on +Windows. Direct `test-traces/...` references are migrated to the helper or new +catalog path. A validation scan rejects new legacy references. + +Android versions are data, not directory names. A case can declare an exact +capture API level and a tested min/max range. When behavior differs by Android +version, separate case ids share a scenario family and declare non-overlapping +version ranges; the index renders the matrix. + +## Failure Handling + +All write commands use a temporary staging directory and rename only after a +complete validation. Generated output is disposable. A failed probe, generator, +Skill, or expectation leaves source cases untouched and writes a diagnostic log +with command, exit code, and stderr. Hash or provenance mismatches are fatal. +Signals that cannot be expressed faithfully with the checked-in Perfetto schema +are rejected as unsupported generator capabilities; a tag-only substitute does +not count as semantic coverage for a Skill that queries another table. + +## Non-Goals + +- The first version does not upload artifacts to GitHub or publish a release. +- It does not turn private traces into public traces automatically. +- It does not merge binary fixtures into the AI knowledge-case store. +- It does not require every constructed case to commit a duplicated combined + trace; reproducibility and release artifacts provide that deliverable. diff --git a/package.json b/package.json index afe3f1560..8245d59d4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,10 @@ "test:governance": "node --test scripts/__tests__/check-perfetto-skills-impact.test.mjs", "test:e2e:dual-trace": "node scripts/run-dual-trace-browser-e2e.cjs", "test:e2e:dual-trace-real": "node scripts/run-dual-trace-browser-e2e.cjs --real-provider", + "trace:validate": "node Trace/tools/trace-corpus.cjs validate --check-generated", + "trace:build": "node Trace/tools/trace-corpus.cjs build --check", + "trace:test": "npm --prefix backend run trace:test", + "trace:regression": "npm run trace:validate && npm run trace:build && npm run trace:test", "rust:fmt": "cargo fmt --manifest-path rust/flamegraph-analyzer/Cargo.toml -- --check", "rust:check": "cargo check --manifest-path rust/flamegraph-analyzer/Cargo.toml", "rust:test": "cargo test --manifest-path rust/flamegraph-analyzer/Cargo.toml", diff --git a/scripts/__tests__/check-perfetto-skills-impact.test.mjs b/scripts/__tests__/check-perfetto-skills-impact.test.mjs index 4a817381b..5b3d62b64 100644 --- a/scripts/__tests__/check-perfetto-skills-impact.test.mjs +++ b/scripts/__tests__/check-perfetto-skills-impact.test.mjs @@ -186,3 +186,18 @@ test('repository rules expose the impact gate and decision states', () => { assert.match(agents, new RegExp(token)); } }); + +test('public Skill verification checks the pinned imported snapshot', () => { + const script = readFileSync( + new URL('../verify-public-skill-export.sh', import.meta.url), + 'utf8', + ); + assert.match(script, /tools\/sync_smartperfetto\.py/); + assert.match(script, /tools\/validate_catalog\.py/); + assert.match(script, /--check/); + assert.doesNotMatch(script, /tools\/export_from_smartperfetto\.py/); + assert.ok( + script.indexOf('python3 "$SYNC_CHECKER"') < + script.indexOf('python3 "$VALIDATOR"'), + ); +}); diff --git a/scripts/e2e/dual-trace-e2e-paths.cjs b/scripts/e2e/dual-trace-e2e-paths.cjs index d0149e1b0..6811f81e4 100644 --- a/scripts/e2e/dual-trace-e2e-paths.cjs +++ b/scripts/e2e/dual-trace-e2e-paths.cjs @@ -7,6 +7,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); +const {resolveCaseTrace} = require("../../Trace/tools/lib/catalog.cjs"); const ROOT_DIR = path.resolve(__dirname, "../.."); const PATHS = Object.freeze({ @@ -20,7 +21,6 @@ const PATHS = Object.freeze({ "ui", "playwright.smartperfetto.config.ts", ), - fixtures: path.join(ROOT_DIR, "test-traces"), }); function requireFile(filePath, label) { @@ -34,14 +34,8 @@ function resolveTooling() { "Committed frontend server", ); requireFile(PATHS.playwrightConfig, "SmartPerfetto Playwright config"); - requireFile( - path.join(PATHS.fixtures, "launch_light.pftrace"), - "Light launch Trace fixture", - ); - requireFile( - path.join(PATHS.fixtures, "lacunh_heavy.pftrace"), - "Heavy launch Trace fixture", - ); + requireFile(resolveCaseTrace(ROOT_DIR, "launch_light.pftrace"), "Light launch Trace fixture"); + requireFile(resolveCaseTrace(ROOT_DIR, "lacunh_heavy.pftrace"), "Heavy launch Trace fixture"); return { playwrightCli: require.resolve("@playwright/test/cli", { paths: [PATHS.ui], @@ -96,6 +90,7 @@ function prepareRunWorkspace() { backendLogDir: path.join(temporaryDir, "backend-logs"), uploadDir: path.join(temporaryDir, "uploads"), traceUploadDir: path.join(temporaryDir, "uploads", "traces"), + fixtureDir: path.join(temporaryDir, "fixtures"), enterpriseDataDir: path.join(temporaryDir, "enterprise"), sceneReportDir: path.join(temporaryDir, "scene-reports"), sceneJobArtifactDir: path.join(temporaryDir, "scene-job-artifacts"), @@ -109,6 +104,7 @@ function prepareRunWorkspace() { workspace.backendDataDir, workspace.backendLogDir, workspace.traceUploadDir, + workspace.fixtureDir, workspace.enterpriseDataDir, workspace.sceneReportDir, workspace.sceneJobArtifactDir, @@ -116,6 +112,9 @@ function prepareRunWorkspace() { fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); fs.chmodSync(directory, 0o700); } + for (const selector of ["launch_light.pftrace", "lacunh_heavy.pftrace"]) { + fs.copyFileSync(resolveCaseTrace(ROOT_DIR, selector), path.join(workspace.fixtureDir, selector)); + } return workspace; } diff --git a/scripts/e2e/dual-trace-e2e-services.cjs b/scripts/e2e/dual-trace-e2e-services.cjs index 800568080..68fb08060 100644 --- a/scripts/e2e/dual-trace-e2e-services.cjs +++ b/scripts/e2e/dual-trace-e2e-services.cjs @@ -190,7 +190,7 @@ function childEnvironments(workspace, services, providerBaseUrl) { SMARTPERFETTO_E2E_PROVIDER_URL: providerBaseUrl, SMARTPERFETTO_E2E_STUB_STATE_URL: `${providerBaseUrl}/__state`, SMARTPERFETTO_E2E_ARTIFACT_DIR: workspace.artifactDir, - SMARTPERFETTO_E2E_FIXTURE_DIR: PATHS.fixtures, + SMARTPERFETTO_E2E_FIXTURE_DIR: workspace.fixtureDir, SMARTPERFETTO_E2E_TP_PORT_MIN: String(services.traceProcessorPortMin), SMARTPERFETTO_E2E_TP_PORT_MAX: String(services.traceProcessorPortMax), SMARTPERFETTO_E2E_CHROME_CHANNEL: diff --git a/scripts/verify-public-skill-export.sh b/scripts/verify-public-skill-export.sh index 6dc7559b9..1f8ddc904 100755 --- a/scripts/verify-public-skill-export.sh +++ b/scripts/verify-public-skill-export.sh @@ -11,14 +11,17 @@ if [[ ! -d "$PERFETTO_SKILLS_DIR/.git" && ! -f "$PERFETTO_SKILLS_DIR/.git" ]]; t exit 2 fi -EXPORTER="$PERFETTO_SKILLS_DIR/tools/export_from_smartperfetto.py" +SYNC_CHECKER="$PERFETTO_SKILLS_DIR/tools/sync_smartperfetto.py" VALIDATOR="$PERFETTO_SKILLS_DIR/tools/validate_catalog.py" -if [[ ! -f "$EXPORTER" || ! -f "$VALIDATOR" ]]; then +if [[ ! -f "$SYNC_CHECKER" || ! -f "$VALIDATOR" ]]; then echo "Perfetto-Skills checkout is missing export verification tools: $PERFETTO_SKILLS_DIR" >&2 exit 2 fi -python3 "$EXPORTER" --source "$SMARTPERFETTO_DIR" --check +python3 "$SYNC_CHECKER" \ + --source "$SMARTPERFETTO_DIR" \ + --report-dir "$PERFETTO_SKILLS_DIR/test-output/verify-smartperfetto" \ + --check python3 "$VALIDATOR" \ --catalog "$PERFETTO_SKILLS_DIR/catalog/smartperfetto-export.json" \ --skill-root "$PERFETTO_SKILLS_DIR/skills/perfetto-performance-analysis"