flutter-devtools-mcp 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/README.zh-CN.md +292 -0
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +55 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/services/ai-analysis.d.ts +6 -0
  9. package/dist/services/ai-analysis.d.ts.map +1 -0
  10. package/dist/services/ai-analysis.js +728 -0
  11. package/dist/services/ai-analysis.js.map +1 -0
  12. package/dist/services/collect-progress.d.ts +6 -0
  13. package/dist/services/collect-progress.d.ts.map +1 -0
  14. package/dist/services/collect-progress.js +23 -0
  15. package/dist/services/collect-progress.js.map +1 -0
  16. package/dist/services/cpu-profiler-service.d.ts +42 -0
  17. package/dist/services/cpu-profiler-service.d.ts.map +1 -0
  18. package/dist/services/cpu-profiler-service.js +208 -0
  19. package/dist/services/cpu-profiler-service.js.map +1 -0
  20. package/dist/services/cpu-symbol-filter.d.ts +10 -0
  21. package/dist/services/cpu-symbol-filter.d.ts.map +1 -0
  22. package/dist/services/cpu-symbol-filter.js +90 -0
  23. package/dist/services/cpu-symbol-filter.js.map +1 -0
  24. package/dist/services/http-profile-parser.d.ts +4 -0
  25. package/dist/services/http-profile-parser.d.ts.map +1 -0
  26. package/dist/services/http-profile-parser.js +71 -0
  27. package/dist/services/http-profile-parser.js.map +1 -0
  28. package/dist/services/json-parse-aggregate.d.ts +30 -0
  29. package/dist/services/json-parse-aggregate.d.ts.map +1 -0
  30. package/dist/services/json-parse-aggregate.js +155 -0
  31. package/dist/services/json-parse-aggregate.js.map +1 -0
  32. package/dist/services/network-aggregate.d.ts +50 -0
  33. package/dist/services/network-aggregate.d.ts.map +1 -0
  34. package/dist/services/network-aggregate.js +185 -0
  35. package/dist/services/network-aggregate.js.map +1 -0
  36. package/dist/services/network-capture-service.d.ts +50 -0
  37. package/dist/services/network-capture-service.d.ts.map +1 -0
  38. package/dist/services/network-capture-service.js +187 -0
  39. package/dist/services/network-capture-service.js.map +1 -0
  40. package/dist/services/network-capture.d.ts +2 -0
  41. package/dist/services/network-capture.d.ts.map +1 -0
  42. package/dist/services/network-capture.js +2 -0
  43. package/dist/services/network-capture.js.map +1 -0
  44. package/dist/services/performance-session.d.ts +41 -0
  45. package/dist/services/performance-session.d.ts.map +1 -0
  46. package/dist/services/performance-session.js +697 -0
  47. package/dist/services/performance-session.js.map +1 -0
  48. package/dist/services/performance-thresholds.d.ts +17 -0
  49. package/dist/services/performance-thresholds.d.ts.map +1 -0
  50. package/dist/services/performance-thresholds.js +19 -0
  51. package/dist/services/performance-thresholds.js.map +1 -0
  52. package/dist/services/profiler.d.ts +71 -0
  53. package/dist/services/profiler.d.ts.map +1 -0
  54. package/dist/services/profiler.js +314 -0
  55. package/dist/services/profiler.js.map +1 -0
  56. package/dist/services/rebuild-business-resolver.d.ts +24 -0
  57. package/dist/services/rebuild-business-resolver.d.ts.map +1 -0
  58. package/dist/services/rebuild-business-resolver.js +599 -0
  59. package/dist/services/rebuild-business-resolver.js.map +1 -0
  60. package/dist/services/rebuild-tracker-service.d.ts +45 -0
  61. package/dist/services/rebuild-tracker-service.d.ts.map +1 -0
  62. package/dist/services/rebuild-tracker-service.js +196 -0
  63. package/dist/services/rebuild-tracker-service.js.map +1 -0
  64. package/dist/services/session-types.d.ts +240 -0
  65. package/dist/services/session-types.d.ts.map +1 -0
  66. package/dist/services/session-types.js +180 -0
  67. package/dist/services/session-types.js.map +1 -0
  68. package/dist/services/threshold-alerts.d.ts +49 -0
  69. package/dist/services/threshold-alerts.d.ts.map +1 -0
  70. package/dist/services/threshold-alerts.js +258 -0
  71. package/dist/services/threshold-alerts.js.map +1 -0
  72. package/dist/services/timeline-cpu-parser.d.ts +28 -0
  73. package/dist/services/timeline-cpu-parser.d.ts.map +1 -0
  74. package/dist/services/timeline-cpu-parser.js +174 -0
  75. package/dist/services/timeline-cpu-parser.js.map +1 -0
  76. package/dist/services/timeline-extras.d.ts +61 -0
  77. package/dist/services/timeline-extras.d.ts.map +1 -0
  78. package/dist/services/timeline-extras.js +449 -0
  79. package/dist/services/timeline-extras.js.map +1 -0
  80. package/dist/services/vm-service-client.d.ts +123 -0
  81. package/dist/services/vm-service-client.d.ts.map +1 -0
  82. package/dist/services/vm-service-client.js +418 -0
  83. package/dist/services/vm-service-client.js.map +1 -0
  84. package/dist/tools/connection.d.ts +4 -0
  85. package/dist/tools/connection.d.ts.map +1 -0
  86. package/dist/tools/connection.js +127 -0
  87. package/dist/tools/connection.js.map +1 -0
  88. package/dist/tools/debug-actions.d.ts +4 -0
  89. package/dist/tools/debug-actions.d.ts.map +1 -0
  90. package/dist/tools/debug-actions.js +210 -0
  91. package/dist/tools/debug-actions.js.map +1 -0
  92. package/dist/tools/discover.d.ts +4 -0
  93. package/dist/tools/discover.d.ts.map +1 -0
  94. package/dist/tools/discover.js +183 -0
  95. package/dist/tools/discover.js.map +1 -0
  96. package/dist/tools/memory.d.ts +4 -0
  97. package/dist/tools/memory.d.ts.map +1 -0
  98. package/dist/tools/memory.js +145 -0
  99. package/dist/tools/memory.js.map +1 -0
  100. package/dist/tools/network.d.ts +4 -0
  101. package/dist/tools/network.d.ts.map +1 -0
  102. package/dist/tools/network.js +265 -0
  103. package/dist/tools/network.js.map +1 -0
  104. package/dist/tools/performance-session.d.ts +4 -0
  105. package/dist/tools/performance-session.d.ts.map +1 -0
  106. package/dist/tools/performance-session.js +105 -0
  107. package/dist/tools/performance-session.js.map +1 -0
  108. package/dist/tools/profiling.d.ts +5 -0
  109. package/dist/tools/profiling.d.ts.map +1 -0
  110. package/dist/tools/profiling.js +142 -0
  111. package/dist/tools/profiling.js.map +1 -0
  112. package/dist/tools/rebuild-tracker.d.ts +4 -0
  113. package/dist/tools/rebuild-tracker.d.ts.map +1 -0
  114. package/dist/tools/rebuild-tracker.js +228 -0
  115. package/dist/tools/rebuild-tracker.js.map +1 -0
  116. package/dist/tools/snapshot-diff.d.ts +4 -0
  117. package/dist/tools/snapshot-diff.d.ts.map +1 -0
  118. package/dist/tools/snapshot-diff.js +235 -0
  119. package/dist/tools/snapshot-diff.js.map +1 -0
  120. package/dist/tools/widget-tree.d.ts +4 -0
  121. package/dist/tools/widget-tree.d.ts.map +1 -0
  122. package/dist/tools/widget-tree.js +156 -0
  123. package/dist/tools/widget-tree.js.map +1 -0
  124. package/docs/README.md +9 -0
  125. package/docs/performance-audit-requirements.md +403 -0
  126. package/docs/performance-metrics-guide.md +207 -0
  127. package/docs/performance-session-simple-design.md +399 -0
  128. package/examples/app_network_image.dart +242 -0
  129. package/examples/app_network_image_README.md +51 -0
  130. package/package.json +69 -0
@@ -0,0 +1,728 @@
1
+ import { readFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { HOT_FUNCTION_ABNORMAL_MS, } from "./session-types.js";
6
+ import { isDioJsonDecodeIsolate, enrichPerformanceSession, } from "./network-aggregate.js";
7
+ import { formatRebuildLocationCell, } from "./rebuild-business-resolver.js";
8
+ import { formatBytesKb as formatParseBytesKb, parseSourceLabel, } from "./json-parse-aggregate.js";
9
+ const MCP_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
10
+ const DEFAULT_FLUTTER_SIMPLE = join(MCP_ROOT, "..", "flutter-simple");
11
+ const REBUILD_ABNORMAL_COUNT = 100;
12
+ function isMemoryCollected(result) {
13
+ if (result.memory.collected === false)
14
+ return false;
15
+ if (result.memory.collected === true)
16
+ return true;
17
+ return result.memory.heapMb > 0 || result.memory.utilizationPct > 0;
18
+ }
19
+ const REBUILD_ATTENTION_COUNT = 30;
20
+ /** 摘要只展示重建 Top3 */
21
+ const REBUILD_SUMMARY_TOP_N = 3;
22
+ function mdEscapeCell(s) {
23
+ return s.replace(/\|/g, "\\|").replace(/\n/g, " ");
24
+ }
25
+ function mdTable(headers, rows) {
26
+ if (rows.length === 0)
27
+ return ["_(无数据)_"];
28
+ return [
29
+ `| ${headers.map(mdEscapeCell).join(" | ")} |`,
30
+ `| ${headers.map(() => "---").join(" | ")} |`,
31
+ ...rows.map((row) => `| ${row.map((c) => mdEscapeCell(String(c))).join(" | ")} |`),
32
+ ];
33
+ }
34
+ function formatBytesKb(bytes) {
35
+ if (bytes == null || bytes <= 0)
36
+ return "-";
37
+ if (bytes >= 1024 * 1024) {
38
+ return `${Math.round((bytes / 1024 / 1024) * 10) / 10}MB`;
39
+ }
40
+ return `${Math.round((bytes / 1024) * 10) / 10}KB`;
41
+ }
42
+ function formatRuntimeSummaryTable(rows) {
43
+ const tag = (v) => v === "异常"
44
+ ? "🔴异常"
45
+ : v === "注意"
46
+ ? "🟡注意"
47
+ : v === "无数据"
48
+ ? "⚪无数据"
49
+ : "🟢正常";
50
+ return [
51
+ ...mdTable(["维度", "状态", "数据", "详情"], rows.map((r) => [r.dim, tag(r.verdict), r.data, r.action || "-"])),
52
+ "",
53
+ ];
54
+ }
55
+ function formatRebuildTable(items, projectRoot) {
56
+ if (items.length === 0) {
57
+ return ["_无 rebuild 数据(profile 模式常见)_", ""];
58
+ }
59
+ const rows = items.slice(0, REBUILD_SUMMARY_TOP_N).map((r) => {
60
+ const { biz, component } = formatRebuildLocationCell(r, projectRoot);
61
+ return [r.widget, String(r.count), biz, component];
62
+ });
63
+ return [
64
+ ...mdTable(["Widget", "次数", "业务定位", "组件位置"], rows),
65
+ "",
66
+ ];
67
+ }
68
+ function formatLargeImageUrl(item) {
69
+ if (item.url?.startsWith("http"))
70
+ return item.url;
71
+ return item.path || item.url || "-";
72
+ }
73
+ function formatLargeImageTable(alerts, heading = "#### 大图片(体积 / 解码超阈)") {
74
+ if (alerts.largeImages.length === 0) {
75
+ return [];
76
+ }
77
+ const rows = alerts.largeImages.map((i) => [
78
+ formatLargeImageUrl(i),
79
+ formatParseBytesKb(i.bytes),
80
+ i.decodeMs != null ? `${i.decodeMs}ms` : "-",
81
+ i.httpMs != null ? `${i.httpMs}ms` : "-",
82
+ i.width && i.height ? `${i.width}x${i.height}` : "-",
83
+ i.triggers.join("; "),
84
+ i.source,
85
+ ]);
86
+ return [
87
+ heading,
88
+ ...mdTable(["Path / URL", "体积", "解码", "HTTP", "尺寸", "命中", "来源"], rows),
89
+ "",
90
+ ];
91
+ }
92
+ function formatLargeApiTable(alerts, heading = "#### 大接口(体积 / 解析超阈)") {
93
+ if (alerts.largeApis.length === 0) {
94
+ return [];
95
+ }
96
+ const rows = alerts.largeApis.map((a) => [
97
+ a.method ?? "-",
98
+ a.path,
99
+ String(a.count),
100
+ formatParseBytesKb(a.avgBytes ?? a.bytes),
101
+ `${a.avgParseMs}ms`,
102
+ `${a.totalParseMs}ms`,
103
+ a.triggers.join("; "),
104
+ a.parseSource ? parseSourceLabel(a.parseSource) : "-",
105
+ ]);
106
+ return [
107
+ heading,
108
+ "_解析含 HttpProfile 后置段 + Dio Worker 分摊_",
109
+ ...mdTable(["Method", "Path", "次数", "Avg体积", "Avg解析", "合计解析", "命中", "来源"], rows),
110
+ "",
111
+ ];
112
+ }
113
+ function formatHotFunctionsTable(funcs) {
114
+ if (funcs.length === 0) {
115
+ return ["_未命中业务 lib/ 方法_", ""];
116
+ }
117
+ const rows = funcs.slice(0, 10).map((f) => [
118
+ f.name,
119
+ f.file,
120
+ `${f.selfMs}ms`,
121
+ `${f.pct}%`,
122
+ f.selfMs > HOT_FUNCTION_ABNORMAL_MS ? "🔴" : "",
123
+ ]);
124
+ return [
125
+ ...mdTable(["函数", "文件", "Self", "占比", "标记"], rows),
126
+ "",
127
+ ];
128
+ }
129
+ function dioSuggestionsForTableView(suggestions) {
130
+ return suggestions.filter((s) => !s.startsWith("采集窗口内高频/大响应接口") &&
131
+ !s.startsWith("已统计到 HTTP 次数但未解析") &&
132
+ !s.startsWith("接口明细见报告"));
133
+ }
134
+ function selectRebuildsForSummary(topRebuilds) {
135
+ return topRebuilds.slice(0, REBUILD_SUMMARY_TOP_N);
136
+ }
137
+ function detectCodeFindings(source) {
138
+ const findings = [];
139
+ if (/Timer\.periodic/.test(source) && /setState/.test(source)) {
140
+ findings.push({
141
+ priority: "P0",
142
+ title: "高频全局 setState 导致整树重建",
143
+ evidence: "Timer.periodic + setState 同文件出现",
144
+ cause: "父组件周期性 setState,子树随 tick 被动重建。",
145
+ fix: "移除全局 tick 刷新;改为 ValueNotifier/Stream 仅更新必要子树,或对列表项使用 const + 独立状态。",
146
+ });
147
+ }
148
+ if (/class OrderCard/.test(source) && /required this\.tick/.test(source)) {
149
+ findings.push({
150
+ priority: "P0",
151
+ title: "OrderCard 依赖父级 tick 导致被动重建",
152
+ evidence: "OrderCard 构造函数含 tick 参数",
153
+ cause: "子组件 build 签名依赖父 setState 变量,无法局部隔离重建。",
154
+ fix: "将 tick 相关展示拆到独立 StatefulWidget,或用 ListenableBuilder 缩小重建范围。",
155
+ });
156
+ }
157
+ if (/for\s*\([^)]*<\s*200/.test(source) && /Widget build/.test(source)) {
158
+ findings.push({
159
+ priority: "P1",
160
+ title: "build 方法内同步循环计算",
161
+ evidence: "build 内 for 循环 checksum 计算",
162
+ cause: "每次重建都在 UI 线程做 O(n) 计算,放大 jank。",
163
+ fix: "将 checksum 移到 initState/compute 或缓存到 FeedItem 模型。",
164
+ });
165
+ }
166
+ if (/HttpClient/.test(source) && /setState/.test(source)) {
167
+ findings.push({
168
+ priority: "P1",
169
+ title: "网络回调触发额外 setState",
170
+ evidence: "HttpClient 与 setState 同文件",
171
+ cause: "请求开始/结束各触发 setState,叠加高频 tick 重建。",
172
+ fix: "用 Riverpod/Bloc 管理网络状态;请求进行中避免整页刷新。",
173
+ });
174
+ }
175
+ if (/ListView\.builder/.test(source) && !/itemExtent|cacheExtent/.test(source)) {
176
+ findings.push({
177
+ priority: "P2",
178
+ title: "长列表未做重建隔离优化",
179
+ evidence: "ListView.builder 无 itemExtent/cacheExtent",
180
+ cause: "频繁父级重建时列表项无法有效复用。",
181
+ fix: "为列表项添加 Key、const 子组件,或配合 AutomaticKeepAliveClientMixin。",
182
+ });
183
+ }
184
+ return findings;
185
+ }
186
+ function rebuildFixHint(topRebuilds) {
187
+ const targets = selectRebuildsForSummary(topRebuilds);
188
+ if (targets.length === 0) {
189
+ return "缩小 setState/Animation 触发范围,见 P0 代码项。";
190
+ }
191
+ const locs = targets
192
+ .map((r) => {
193
+ if (r.bizFile && r.bizWidget) {
194
+ const short = r.bizFile.split("/").slice(-2).join("/");
195
+ return `${r.bizWidget}(${short}:${r.bizLine ?? r.line})`;
196
+ }
197
+ return `${r.file.split("/").pop()}:${r.line}`;
198
+ })
199
+ .join("、");
200
+ return `按下方「重建」定位排查 ${locs} 的触发源,缩小 setState/Animation 范围。`;
201
+ }
202
+ function findingsFromRuntime(result) {
203
+ const findings = [];
204
+ const recSec = result.recordingWindowSec ?? result.durationSec;
205
+ const maxRebuild = result.topRebuilds[0]?.count ?? 0;
206
+ if (maxRebuild > 100) {
207
+ const top = result.topRebuilds[0];
208
+ findings.push({
209
+ priority: "P0",
210
+ title: "运行时检测到大量 Widget 重建",
211
+ evidence: `最高 ${top?.widget ?? "Unknown"} ${maxRebuild}x / 录制 ${recSec}s`,
212
+ cause: "高频 Animation/setState 或父级 tick 导致子树被动重建。",
213
+ fix: rebuildFixHint(result.topRebuilds),
214
+ });
215
+ }
216
+ const bizHot = result.projectTopFunctions[0];
217
+ if (bizHot && bizHot.selfMs > HOT_FUNCTION_ABNORMAL_MS) {
218
+ findings.push({
219
+ priority: bizHot.selfMs >= 50 || result.frames.jankPct > 5 ? "P0" : "P1",
220
+ title: "业务热点函数 Self 时间偏高",
221
+ evidence: `Top ${bizHot.name} Self ${bizHot.selfMs}ms / 录制 ${recSec}s`,
222
+ cause: "热路径在主 isolate 累计耗时过高,可能放大 rebuild/GC 影响。",
223
+ fix: "移出 build/回调热路径,或缓存/下沉 isolate。",
224
+ });
225
+ }
226
+ if (result.frames.jankPct > 5) {
227
+ findings.push({
228
+ priority: "P0",
229
+ title: "掉帧率偏高",
230
+ evidence: `掉帧 ${result.frames.jankPct}%(${result.frames.jankCount}/${result.frames.total})`,
231
+ cause: "主线程 build/布局压力过大。",
232
+ fix: "减少重建范围 + 移出 build 内计算 + profile 模式验证。",
233
+ });
234
+ }
235
+ const worstScroll = result.scrollFps?.worstSegments?.[0];
236
+ if (worstScroll && worstScroll.jankPct > 15 && worstScroll.frames >= 5) {
237
+ findings.push({
238
+ priority: "P0",
239
+ title: "滚动段掉帧明显",
240
+ evidence: `${worstScroll.startSec}-${worstScroll.endSec}s: ${worstScroll.fps} FPS, jank ${worstScroll.jankPct}%`,
241
+ cause: "列表滚动期间主线程过载(重建/布局/解码)。",
242
+ fix: "缩小重建范围、itemExtent、预解码图片、避免滚动中同步计算。",
243
+ });
244
+ }
245
+ if (result.gc &&
246
+ (result.gc.longPauseCount >= 3 || result.gc.maxPauseMs > 32)) {
247
+ findings.push({
248
+ priority: "P1",
249
+ title: "GC 停顿可能造成卡顿尖刺",
250
+ evidence: `GC ${result.gc.count} 次, 最长 ${result.gc.maxPauseMs}ms, >8ms ${result.gc.longPauseCount} 次`,
251
+ cause: "短生命周期对象分配过多触发频繁/长 GC。",
252
+ fix: "减少每帧临时 List/String/闭包;复用对象;大计算移出 UI isolate。",
253
+ });
254
+ }
255
+ if (result.imageDecode && result.imageDecode.maxMs > 32) {
256
+ const top = result.imageDecode.slow[0];
257
+ const where = top?.url
258
+ ? `,图: ${top.url}`
259
+ : top
260
+ ? `,事件: ${top.name}`
261
+ : "";
262
+ findings.push({
263
+ priority: "P1",
264
+ title: "图片解码偏慢",
265
+ evidence: `解码 ${result.imageDecode.count} 次, 最长 ${result.imageDecode.maxMs}ms, 合计 ${result.imageDecode.totalMs}ms${where}`,
266
+ cause: "大图在 UI 路径同步解码或缺少缓存。",
267
+ fix: top?.url
268
+ ? `针对 ${top.url}:缩小分辨率、ResizeImage/预缓存,或 cached_network_image。`
269
+ : "缩小分辨率、预缓存、使用 cached_network_image / ResizeImage。",
270
+ });
271
+ }
272
+ const dio = result.dioJsonDecode;
273
+ const bgBusy = (result.isolateCpu ?? []).filter((i) => !i.isMain && i.topSelfMs > 100 && !isDioJsonDecodeIsolate(i.name));
274
+ if (dio) {
275
+ findings.push({
276
+ priority: "P2",
277
+ title: "Dio 后台 JSON 解析占用 CPU",
278
+ evidence: `${dio.workerCount} 个 FusedTransformer worker, 累计 ~${dio.totalWorkerCpuMs}ms; ` +
279
+ `HTTP ${dio.networkTotal} 次 (${dio.networkQps} req/s), ≥${dio.isolateThresholdKb}KB 响应 ${dio.largeJsonResponseCount} 次,` +
280
+ `明细见「大接口」表`,
281
+ cause: "Dio 5.9 默认对 ≥50KB JSON 在后台 isolate 做 utf8+json 解析,高并发或大 payload 会增加 CPU/GC 压力。",
282
+ fix: dio.topPaths.length > 0
283
+ ? "见「大接口」表:优先排查超阈 path 的 payload 体积、缓存/去重与是否可 sync 解析。"
284
+ : (dio.suggestions.find((s) => !s.startsWith("采集窗口内高频")) ??
285
+ "检查高频接口缓存与 payload 体积。"),
286
+ });
287
+ }
288
+ else if (bgBusy.length > 0) {
289
+ findings.push({
290
+ priority: "P2",
291
+ title: "后台 isolate 有明显 CPU",
292
+ evidence: bgBusy
293
+ .map((i) => `${i.name}: ${i.topName} ${i.topSelfMs}ms`)
294
+ .join("; "),
295
+ cause: "后台任务占用 CPU,可能与主 isolate 争抢。",
296
+ fix: "确认后台任务优先级;避免与 UI 高峰重叠。",
297
+ });
298
+ }
299
+ else if ((result.isolateCpu ?? []).filter((i) => i.isMain).length === 1 &&
300
+ (result.isolateCpu ?? []).every((i) => i.isMain) &&
301
+ (result.projectTopFunctions[0]?.selfMs ?? 0) > 200) {
302
+ findings.push({
303
+ priority: "P2",
304
+ title: "重计算仍在主 isolate",
305
+ evidence: `业务热点 ${result.projectTopFunctions[0]?.name} ${result.projectTopFunctions[0]?.selfMs}ms,未见后台 isolate 分担`,
306
+ cause: "热路径同步计算阻塞 UI。",
307
+ fix: "将纯计算用 compute()/Isolate.run 下沉。",
308
+ });
309
+ }
310
+ if (isMemoryCollected(result) && result.memory.utilizationPct > 85) {
311
+ findings.push({
312
+ priority: "P1",
313
+ title: "堆内存利用率偏高",
314
+ evidence: `Heap ${result.memory.heapMb}MB, 利用率 ${result.memory.utilizationPct}%`,
315
+ cause: "debug 模式堆偏大;频繁重建产生短生命周期对象。",
316
+ fix: "修复重建后复测;发布前用 profile/release 对比内存。",
317
+ });
318
+ }
319
+ if (result.network.total > 0 && result.network.errors > 0) {
320
+ findings.push({
321
+ priority: "P1",
322
+ title: "网络请求存在失败",
323
+ evidence: `${result.network.errors}/${result.network.total} 失败`,
324
+ cause: "HttpClient 未复用或网络权限/证书问题。",
325
+ fix: "使用单例 HttpClient + 错误重试;检查 Android INTERNET 权限。",
326
+ });
327
+ }
328
+ const ta = result.thresholdAlerts;
329
+ if (ta && (ta.summary.largeImageHits > 0 || ta.summary.slowDecodeHits > 0)) {
330
+ findings.push({
331
+ priority: ta.summary.slowDecodeHits > 0 ? "P1" : "P2",
332
+ title: "大图片或慢解码超阈值",
333
+ evidence: `大图 ${ta.summary.largeImageHits}, 慢解码 ${ta.summary.slowDecodeHits}(阈值 ${Math.round(ta.config.largeImageBytes / 1024)}KB / ${ta.config.slowImageDecodeMs}ms)`,
334
+ cause: "大图同步解码或慢 HTTP 下载阻塞 UI/GC。",
335
+ fix: "缩小分辨率、ResizeImage/预缓存、cached_network_image。",
336
+ });
337
+ }
338
+ if (ta && (ta.summary.largeApiHits > 0 || ta.summary.slowParseHits > 0)) {
339
+ findings.push({
340
+ priority: ta.summary.slowParseHits > 0 ? "P1" : "P2",
341
+ title: "大接口或 JSON 解析超阈值",
342
+ evidence: `大接口 ${ta.summary.largeApiHits}, 慢解析 ${ta.summary.slowParseHits}(阈值 ${Math.round(ta.config.largeApiBytes / 1024)}KB / ${ta.config.slowApiParseMs}ms)`,
343
+ cause: "大 payload 或高频 JSON 解析增加 CPU/GC 压力。",
344
+ fix: "缩小 payload、缓存/去重、提高 contentLengthIsolateThreshold 或 sync 小响应。",
345
+ });
346
+ }
347
+ return findings;
348
+ }
349
+ function mergeFindings(findings) {
350
+ const seen = new Set();
351
+ const out = [];
352
+ for (const f of findings) {
353
+ const key = `${f.priority}:${f.title}`;
354
+ if (seen.has(key))
355
+ continue;
356
+ seen.add(key);
357
+ out.push(f);
358
+ }
359
+ const order = { P0: 0, P1: 1, P2: 2 };
360
+ return out.sort((a, b) => order[a.priority] - order[b.priority]);
361
+ }
362
+ async function readProjectSources(result, projectRoot) {
363
+ const paths = new Set(result.filesToInspect);
364
+ paths.add("lib/main.dart");
365
+ const chunks = [];
366
+ for (const rel of paths) {
367
+ const full = join(projectRoot, rel);
368
+ if (!existsSync(full))
369
+ continue;
370
+ try {
371
+ const content = await readFile(full, "utf-8");
372
+ chunks.push(`// ${rel}\n${content}`);
373
+ }
374
+ catch {
375
+ // skip
376
+ }
377
+ }
378
+ return chunks.join("\n\n");
379
+ }
380
+ export async function generateAiAnalysis(result, options = {}) {
381
+ const projectRoot = options.projectRoot ?? DEFAULT_FLUTTER_SIMPLE;
382
+ result = enrichPerformanceSession(result, { projectRoot: options.projectRoot });
383
+ const source = await readProjectSources(result, projectRoot);
384
+ const findings = mergeFindings([
385
+ ...findingsFromRuntime(result),
386
+ ...detectCodeFindings(source),
387
+ ]);
388
+ const byPriority = {
389
+ P0: findings.filter((f) => f.priority === "P0"),
390
+ P1: findings.filter((f) => f.priority === "P1"),
391
+ P2: findings.filter((f) => f.priority === "P2"),
392
+ };
393
+ const bizTop = result.projectTopFunctions
394
+ .slice(0, 3)
395
+ .map((f) => `${f.name} ${f.selfMs}ms`)
396
+ .join(", ");
397
+ const recSec = result.recordingWindowSec ?? result.durationSec;
398
+ const wallSec = result.wallClockSec ?? result.durationSec;
399
+ const parseTop = result.network.topParsePaths?.[0];
400
+ const dio = result.dioJsonDecode;
401
+ const dioWorkerMs = dio?.totalWorkerCpuMs ?? 0;
402
+ const thresholdAlerts = result.thresholdAlerts;
403
+ const frameVerdict = result.frames.total < 10
404
+ ? "无数据"
405
+ : result.frames.jankPct > 15
406
+ ? "异常"
407
+ : result.frames.jankPct > 5
408
+ ? "注意"
409
+ : "正常";
410
+ const frameData = `${result.frames.total} 帧, P99 ${result.frames.p99Ms}ms, jank ${result.frames.jankPct}%; B/L/P max ${result.frames.buildMaxMs}/${result.frames.layoutMaxMs}/${result.frames.paintMaxMs}ms`;
411
+ const frameAction = frameVerdict === "无数据"
412
+ ? "Timeline 帧偏少,以耗时函数/图片解码为准"
413
+ : frameVerdict !== "正常"
414
+ ? "见下方「帧」"
415
+ : "-";
416
+ const rebuildItems = selectRebuildsForSummary(result.topRebuilds);
417
+ const rebuildTop = rebuildItems[0];
418
+ const rebuildVerdict = !rebuildTop
419
+ ? "无数据"
420
+ : rebuildTop.count > REBUILD_ABNORMAL_COUNT
421
+ ? "异常"
422
+ : rebuildTop.count > REBUILD_ATTENTION_COUNT
423
+ ? "注意"
424
+ : "正常";
425
+ const rebuildData = !rebuildTop
426
+ ? "profile 下常无 rebuild 数据"
427
+ : `Top${REBUILD_SUMMARY_TOP_N},最高 ${rebuildTop.widget} ${rebuildTop.count}x`;
428
+ const rebuildAction = !rebuildTop
429
+ ? "需要时用 debug 补采"
430
+ : rebuildVerdict !== "正常"
431
+ ? "见下方「重建」"
432
+ : "-";
433
+ const bizHot = result.projectTopFunctions[0];
434
+ const bizVerdict = !bizHot
435
+ ? "无数据"
436
+ : bizHot.selfMs > HOT_FUNCTION_ABNORMAL_MS
437
+ ? "异常"
438
+ : "正常";
439
+ const bizData = bizHot
440
+ ? `${bizTop || bizHot.name} @ ${bizHot.file}`
441
+ : "未命中业务 lib/ 方法";
442
+ const bizAction = !bizHot
443
+ ? "延长采集并持续操作页面"
444
+ : bizVerdict !== "正常"
445
+ ? "见下方「耗时函数」"
446
+ : "-";
447
+ const worstScroll = result.scrollFps?.worstSegments?.[0];
448
+ const scrollVerdict = !worstScroll
449
+ ? "无数据"
450
+ : worstScroll.jankPct > 15
451
+ ? "异常"
452
+ : worstScroll.jankPct > 5 || (result.scrollFps?.overallFps ?? 60) < 50
453
+ ? "注意"
454
+ : "正常";
455
+ const scrollData = worstScroll
456
+ ? `~${result.scrollFps.overallFps} FPS, 段 ${worstScroll.startSec}-${worstScroll.endSec}s, jank ${worstScroll.jankPct}%`
457
+ : "无足够帧分段";
458
+ const scrollAction = scrollVerdict !== "正常" && scrollVerdict !== "无数据"
459
+ ? "见下方「滚动」"
460
+ : "-";
461
+ const img = result.imageDecode;
462
+ const imgThresholdHits = (thresholdAlerts?.summary.largeImageHits ?? 0) +
463
+ (thresholdAlerts?.summary.slowDecodeHits ?? 0);
464
+ let imgVerdict = thresholdAlerts && imgThresholdHits > 0
465
+ ? (thresholdAlerts.summary.slowDecodeHits ?? 0) > 0
466
+ ? "异常"
467
+ : "注意"
468
+ : !img || (img.count === 0 && !(img.untimedSignals && img.untimedSignals > 0))
469
+ ? "正常"
470
+ : img.count === 0 && (img.untimedSignals ?? 0) > 0
471
+ ? "无数据"
472
+ : img.maxMs > 100
473
+ ? "异常"
474
+ : img.maxMs > 32
475
+ ? "注意"
476
+ : "正常";
477
+ let imgData = thresholdAlerts && imgThresholdHits > 0
478
+ ? `大图 ${thresholdAlerts.summary.largeImageHits}, 慢解码 ${thresholdAlerts.summary.slowDecodeHits}`
479
+ : img && img.count > 0
480
+ ? `${img.count} 次, 最长 ${img.maxMs}ms` +
481
+ (img.slow[0]?.url
482
+ ? ` (${img.slow[0].url.length > 40 ? `${img.slow[0].url.slice(0, 37)}...` : img.slow[0].url})`
483
+ : "")
484
+ : img && (img.untimedSignals ?? 0) > 0
485
+ ? `${img.untimedSignals} 次信号无时长`
486
+ : "未检测到解码事件";
487
+ let imgAction = thresholdAlerts && imgThresholdHits > 0
488
+ ? "见下方「图片解码」"
489
+ : imgVerdict === "无数据"
490
+ ? "确认埋点写入 args.ms"
491
+ : imgVerdict !== "正常"
492
+ ? "见下方「图片解码」"
493
+ : "-";
494
+ const isoBg = (result.isolateCpu ?? []).filter((i) => !i.isMain);
495
+ const isoVerdict = dio
496
+ ? dio.workerCount >= 4 || dio.totalWorkerCpuMs > 2000
497
+ ? "注意"
498
+ : "正常"
499
+ : isoBg.some((i) => i.topSelfMs > 100)
500
+ ? "注意"
501
+ : (bizHot?.selfMs ?? 0) > 100 &&
502
+ isoBg.every((i) => i.topSelfMs < 50 || isDioJsonDecodeIsolate(i.name))
503
+ ? "注意"
504
+ : "正常";
505
+ const isoData = dio
506
+ ? `Dio worker ${dio.workerCount} 个, CPU ~${dio.totalWorkerCpuMs}ms`
507
+ : isoBg.length > 0
508
+ ? `主+后台 ${isoBg.length} 个` +
509
+ (isoBg[0] ? `, 后台 ${isoBg[0].topSelfMs}ms` : "")
510
+ : "仅主 isolate";
511
+ const isoAction = dio
512
+ ? "见下方「Isolate」"
513
+ : isoVerdict === "注意"
514
+ ? "见下方「Isolate」"
515
+ : "-";
516
+ const apiThresholdHits = (thresholdAlerts?.summary.largeApiHits ?? 0) +
517
+ (thresholdAlerts?.summary.slowParseHits ?? 0);
518
+ const netVerdict = result.network.errors > 0
519
+ ? "异常"
520
+ : apiThresholdHits > 0
521
+ ? "注意"
522
+ : dio && dio.networkQps >= 2
523
+ ? "注意"
524
+ : "正常";
525
+ const netParts = [`${result.network.total} 请求`];
526
+ if (result.network.errors)
527
+ netParts.push(`${result.network.errors} 失败`);
528
+ if (dioWorkerMs > 0)
529
+ netParts.push(`JSON 解析 CPU ~${dioWorkerMs}ms`);
530
+ if (thresholdAlerts && apiThresholdHits > 0) {
531
+ netParts.push(`超阈 大接口 ${thresholdAlerts.summary.largeApiHits} / 慢解析 ${thresholdAlerts.summary.slowParseHits}`);
532
+ }
533
+ else if (parseTop) {
534
+ netParts.push(`解析 Top ${parseTop.path} 合计 ${parseTop.totalParseMs}ms`);
535
+ }
536
+ if (result.network.qps != null && result.network.total > 0) {
537
+ netParts.push(`${result.network.qps} req/s`);
538
+ }
539
+ const netData = netParts.join(", ");
540
+ const netAction = thresholdAlerts && apiThresholdHits > 0
541
+ ? "见下方「网络」"
542
+ : netVerdict !== "正常"
543
+ ? "见下方「网络」"
544
+ : "-";
545
+ const memVerdict = !isMemoryCollected(result)
546
+ ? "无数据"
547
+ : result.memory.utilizationPct > 90
548
+ ? "异常"
549
+ : result.memory.utilizationPct > 80
550
+ ? "注意"
551
+ : "正常";
552
+ const memData = isMemoryCollected(result)
553
+ ? `${result.memory.heapMb}MB (${result.memory.utilizationPct}%)`
554
+ : "默认关闭 enableMemory";
555
+ const memAction = isMemoryCollected(result)
556
+ ? memVerdict !== "正常"
557
+ ? "见下方「内存」"
558
+ : "-"
559
+ : "需时用 get_memory_snapshot";
560
+ const gcVerdict = !result.gc
561
+ ? "无数据"
562
+ : result.gc.maxPauseMs > 80 || result.gc.longPauseCount >= 10
563
+ ? "异常"
564
+ : result.gc.longPauseCount >= 3 || result.gc.maxPauseMs > 32
565
+ ? "注意"
566
+ : "正常";
567
+ const gcData = result.gc
568
+ ? `${result.gc.count} 次, 最长 ${result.gc.maxPauseMs}ms, >8ms ${result.gc.longPauseCount} 次`
569
+ : "未采集";
570
+ const gcAction = gcVerdict !== "正常" && gcVerdict !== "无数据" ? "见下方「GC」" : "-";
571
+ const runtimeSummaryRows = [
572
+ { dim: "帧", verdict: frameVerdict, data: frameData, action: frameAction },
573
+ { dim: "重建", verdict: rebuildVerdict, data: rebuildData, action: rebuildAction },
574
+ { dim: "耗时函数", verdict: bizVerdict, data: bizData, action: bizAction },
575
+ { dim: "滚动", verdict: scrollVerdict, data: scrollData, action: scrollAction },
576
+ { dim: "图片解码", verdict: imgVerdict, data: imgData, action: imgAction },
577
+ { dim: "GC", verdict: gcVerdict, data: gcData, action: gcAction },
578
+ { dim: "Isolate", verdict: isoVerdict, data: isoData, action: isoAction },
579
+ { dim: "网络", verdict: netVerdict, data: netData, action: netAction },
580
+ { dim: "内存", verdict: memVerdict, data: memData, action: memAction },
581
+ ];
582
+ const lines = [
583
+ `# 性能优化方案(规则引擎 + 源码对照)`,
584
+ ``,
585
+ `**场景**: ${result.scenario} | **录制**: ${recSec}s | **总耗时**: ${wallSec}s | **掉帧**: ${result.frames.jankPct}%`,
586
+ ``,
587
+ `## 运行时摘要`,
588
+ ...formatRuntimeSummaryTable(runtimeSummaryRows),
589
+ `## 维度详情`,
590
+ ``,
591
+ `### 帧`,
592
+ `- ${frameData}`,
593
+ `- 平均 ${result.frames.avgMs}ms · jank ${result.frames.jankCount}/${result.frames.total}`,
594
+ ``,
595
+ `### 重建`,
596
+ ...formatRebuildTable(rebuildItems, projectRoot),
597
+ `### 耗时函数`,
598
+ ...formatHotFunctionsTable(result.projectTopFunctions),
599
+ `### 滚动`,
600
+ ];
601
+ if (result.scrollFps?.worstSegments?.length) {
602
+ const scrollRows = result.scrollFps.worstSegments.map((s) => [
603
+ `${s.startSec}-${s.endSec}s`,
604
+ `${s.fps}`,
605
+ `${s.jankPct}%`,
606
+ `${s.jankCount}/${s.frames}`,
607
+ `${s.maxMs}ms`,
608
+ ]);
609
+ lines.push(`- 段内整体 ~${result.scrollFps.overallFps} FPS`, ...mdTable(["时段", "FPS", "Jank", "帧", "Max"], scrollRows), ``);
610
+ }
611
+ else {
612
+ lines.push(`- _无足够帧分段_`, ``);
613
+ }
614
+ lines.push(`### 图片解码`);
615
+ lines.push(`- ${imgData}`);
616
+ if (result.imageDecode && result.imageDecode.count > 0) {
617
+ lines.push(`- 合计 ${result.imageDecode.totalMs}ms · 最长 ${result.imageDecode.maxMs}ms`);
618
+ for (const s of result.imageDecode.slow.slice(0, 5)) {
619
+ const parts = [`${s.name} ${s.ms}ms`];
620
+ if (s.url)
621
+ parts.push(s.url);
622
+ if (s.width && s.height)
623
+ parts.push(`${s.width}x${s.height}`);
624
+ if (s.bytes && s.bytes > 0)
625
+ parts.push(formatBytesKb(s.bytes));
626
+ lines.push(`- 慢解码: ${parts.join(" | ")}`);
627
+ }
628
+ }
629
+ if (thresholdAlerts) {
630
+ lines.push(...formatLargeImageTable(thresholdAlerts));
631
+ }
632
+ lines.push(``);
633
+ lines.push(`### GC`);
634
+ if (result.gc) {
635
+ lines.push(`- ${result.gc.count} 次 · 总停顿 ${result.gc.totalPauseMs}ms · 最长 ${result.gc.maxPauseMs}ms · >8ms ${result.gc.longPauseCount} 次`);
636
+ if (result.gc.topPauses?.length) {
637
+ lines.push(`- Top 停顿: ${result.gc.topPauses
638
+ .slice(0, 5)
639
+ .map((p) => `${p.name} ${p.ms}ms`)
640
+ .join(", ")}`);
641
+ }
642
+ }
643
+ else {
644
+ lines.push(`- _未采集_`);
645
+ }
646
+ lines.push(``);
647
+ lines.push(`### Isolate`);
648
+ const mainIso = result.isolateCpu?.find((i) => i.isMain);
649
+ if (dio) {
650
+ lines.push(`- Dio FusedTransformer: ${dio.workerCount} worker · CPU ~${dio.totalWorkerCpuMs}ms · ≥${dio.isolateThresholdKb}KB 响应 ${dio.largeJsonResponseCount} 次`);
651
+ lines.push(`- 关联 HTTP: ${dio.networkTotal} 次 · ${dio.networkQps} req/s`);
652
+ const sug = dioSuggestionsForTableView(dio.suggestions);
653
+ for (const s of sug)
654
+ lines.push(`- ${s}`);
655
+ }
656
+ if (result.isolateCpu && result.isolateCpu.length > 0) {
657
+ const isoRows = result.isolateCpu.map((i) => {
658
+ const topLabel = /drawframe|handledrawframe|invokeframe|persistentframe/i.test(i.topName)
659
+ ? `刷帧链路(${i.topName})`
660
+ : `${i.topName} ${i.topSelfMs}ms`;
661
+ return [
662
+ i.isMain ? "主" : "后台",
663
+ i.name.length > 48 ? `${i.name.slice(0, 45)}...` : i.name,
664
+ String(i.sampleCount),
665
+ topLabel,
666
+ ];
667
+ });
668
+ lines.push(...mdTable(["类型", "Isolate", "Samples", "Top"], isoRows), ``);
669
+ }
670
+ else if (!dio) {
671
+ lines.push(`- _未采样_`, ``);
672
+ }
673
+ else if (mainIso) {
674
+ lines.push(`- 主 isolate: samples=${mainIso.sampleCount}, top=${mainIso.topName} ${mainIso.topSelfMs}ms`, ``);
675
+ }
676
+ lines.push(`### 网络`);
677
+ lines.push(`- ${netData}`);
678
+ if (result.network.topPaths && result.network.topPaths.length > 0) {
679
+ const netRows = result.network.topPaths.slice(0, 10).map((p) => [
680
+ p.method ?? "-",
681
+ p.path,
682
+ String(p.count),
683
+ p.avgMs != null ? `${p.avgMs}ms` : "-",
684
+ p.totalBytes != null && p.totalBytes > 0
685
+ ? formatBytesKb(p.totalBytes)
686
+ : "-",
687
+ ]);
688
+ lines.push(`- Top 接口`, ...mdTable(["Method", "Path", "次数", "Avg", "体积"], netRows), ``);
689
+ }
690
+ if (parseTop) {
691
+ lines.push(`- 解析 Top: ${parseTop.path} 合计 ${parseTop.totalParseMs}ms (${parseSourceLabel(parseTop.source)})`);
692
+ }
693
+ if (thresholdAlerts) {
694
+ lines.push(...formatLargeApiTable(thresholdAlerts));
695
+ }
696
+ if (!result.network.topPaths?.length && !thresholdAlerts?.largeApis.length) {
697
+ lines.push(``);
698
+ }
699
+ lines.push(`### 内存`);
700
+ if (isMemoryCollected(result)) {
701
+ lines.push(`- Heap ${result.memory.heapMb}MB · 利用率 ${result.memory.utilizationPct}%`);
702
+ if (result.memory.topClasses?.length) {
703
+ lines.push(`- Top 类: ${result.memory.topClasses
704
+ .slice(0, 5)
705
+ .map((c) => `${c.name} ${c.bytesMb}MB`)
706
+ .join(", ")}`);
707
+ }
708
+ }
709
+ else {
710
+ lines.push(`- ${memData}`);
711
+ }
712
+ lines.push(``);
713
+ for (const priority of ["P0", "P1", "P2"]) {
714
+ const items = byPriority[priority];
715
+ lines.push(`## ${priority}${priority === "P0" ? "(必须修复)" : priority === "P1" ? "(建议修复)" : "(可选优化)"}`);
716
+ if (items.length === 0) {
717
+ lines.push(`- 无`, ``);
718
+ }
719
+ else {
720
+ for (const item of items) {
721
+ lines.push(`### ${item.title}`, `- **数据**: ${item.evidence}`, `- **原因**: ${item.cause}`, `- **改法**: ${item.fix}`, ``);
722
+ }
723
+ }
724
+ }
725
+ lines.push("---", "*由 flutter-devtools-mcp 规则引擎生成,非 LLM 调用。*");
726
+ return lines.join("\n");
727
+ }
728
+ //# sourceMappingURL=ai-analysis.js.map