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,697 @@
1
+ import { mkdir, readFile, writeFile } from "fs/promises";
2
+ import { join } from "path";
3
+ import { isAppLibFile, isFrameworkOrSdkFile, isProjectSourceFile, parsePubspecPackageName, resolveSessionOutputDir, JANK_TARGET_FPS, } from "./session-types.js";
4
+ import { parseDartTimelineCpu, parseProfilerHotspots, parsePhaseCpuFallback, } from "./timeline-cpu-parser.js";
5
+ import { parseGcStats, parseImageDecodeStats, parseScrollFpsSegments, pickWorstScrollSegments, formatSlowImageDecodeLine, } from "./timeline-extras.js";
6
+ import { generateAiAnalysis } from "./ai-analysis.js";
7
+ import { aggregateNetworkRequests, buildDioJsonDecodeInsight, } from "./network-aggregate.js";
8
+ import { enrichRebuildsWithBusiness } from "./rebuild-business-resolver.js";
9
+ import { aggregateParseCostByPath } from "./json-parse-aggregate.js";
10
+ import { buildThresholdAlerts, } from "./threshold-alerts.js";
11
+ import { DEFAULT_PROGRESS, formatCollectEta, sleepWithProgress, } from "./collect-progress.js";
12
+ import { isBusinessCpuSymbol, isVmInternalCpuSymbol, humanizeVmCpuSymbol, isMisattributedVmCpuEntry, } from "./cpu-symbol-filter.js";
13
+ const VM_INTERNAL_CLASSES = new Set([
14
+ "_OneByteString",
15
+ "_TwoByteString",
16
+ "String",
17
+ "_List",
18
+ "_GrowableList",
19
+ "_Mint",
20
+ "_Uint8List",
21
+ "StreamSubscription",
22
+ ]);
23
+ function isVmInternal(name) {
24
+ return (VM_INTERNAL_CLASSES.has(name) ||
25
+ (name.startsWith("_") &&
26
+ name.length < 20 &&
27
+ !name.includes("State") &&
28
+ !name.includes("Controller")));
29
+ }
30
+ function sleep(ms) {
31
+ return new Promise((resolve) => setTimeout(resolve, ms));
32
+ }
33
+ function isKnownBusinessSymbol(name) {
34
+ const n = name ?? "";
35
+ return (n.includes("businessHotMethod") ||
36
+ n.includes("OrderCard") ||
37
+ n.includes("HomePage") ||
38
+ n.includes("_HomePageState") ||
39
+ n.includes("_OrderCard") ||
40
+ n.includes("FeedItem"));
41
+ }
42
+ function isBusinessCpuEntry(entry, opts) {
43
+ if (!isBusinessCpuSymbol(entry.name, entry.file))
44
+ return false;
45
+ const packageName = opts?.packageName;
46
+ const projectRoot = opts?.projectRoot;
47
+ const url = entry.sourceUrl ?? "";
48
+ const file = entry.file ?? "";
49
+ if (isFrameworkOrSdkFile(file) || isFrameworkOrSdkFile(url))
50
+ return false;
51
+ if (isKnownBusinessSymbol(entry.name))
52
+ return true;
53
+ // 强信号:package:<app>/ 或工程绝对路径
54
+ if (packageName) {
55
+ const pkgPrefix = `package:${packageName.toLowerCase()}/`;
56
+ if (url.toLowerCase().includes(pkgPrefix) || file.toLowerCase().includes(pkgPrefix)) {
57
+ return true;
58
+ }
59
+ }
60
+ if (url.startsWith("file://") && projectRoot) {
61
+ const sourcePath = url.replace(/^file:\/\//, "");
62
+ const root = projectRoot.replace(/\/$/, "");
63
+ if (sourcePath.startsWith(root + "/lib/"))
64
+ return true;
65
+ if (sourcePath.includes("/flutter/") ||
66
+ sourcePath.includes("/.pub-cache/") ||
67
+ sourcePath.includes("/dart-sdk/")) {
68
+ return false;
69
+ }
70
+ }
71
+ // 归一化后的业务相对路径:仅接受「看起来像 App 源码」的 lib/ 文件
72
+ // 排除 lib/ui、lib/src/widgets 等 SDK 形态
73
+ if (isAppLibFile(file, packageName)) {
74
+ const f = file.toLowerCase();
75
+ if (f.startsWith("lib/") && !f.startsWith("lib/src/") && !f.startsWith("lib/ui/")) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+ async function resolvePackageName(projectRoot) {
82
+ if (!projectRoot)
83
+ return undefined;
84
+ try {
85
+ const text = await readFile(join(projectRoot, "pubspec.yaml"), "utf-8");
86
+ return parsePubspecPackageName(text);
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ }
92
+ export class PerformanceSession {
93
+ client;
94
+ profiler;
95
+ rebuildTracker;
96
+ networkCapture;
97
+ cpuProfiler;
98
+ collecting = false;
99
+ constructor(client, profiler, rebuildTracker, networkCapture, cpuProfiler) {
100
+ this.client = client;
101
+ this.profiler = profiler;
102
+ this.rebuildTracker = rebuildTracker;
103
+ this.networkCapture = networkCapture;
104
+ this.cpuProfiler = cpuProfiler;
105
+ }
106
+ get isCollecting() {
107
+ return this.collecting;
108
+ }
109
+ async collect(options = {}) {
110
+ if (!this.client.connected) {
111
+ throw new Error("未连接 Flutter App,请先使用 discover_apps 或 connect");
112
+ }
113
+ if (this.collecting) {
114
+ throw new Error("已有采集正在进行中");
115
+ }
116
+ if (this.profiler.isActive) {
117
+ throw new Error("Profiler 已在运行,请先 stop_profiling");
118
+ }
119
+ const scenario = options.scenario ?? "manual-session";
120
+ const durationSec = options.durationSec ?? 30;
121
+ const warmupSec = options.warmupSec ?? 2;
122
+ const enableNetwork = options.enableNetwork ?? true;
123
+ const enableCpuProfile = options.enableCpuProfile ?? true;
124
+ const enableMemory = options.enableMemory ?? false;
125
+ const topN = options.topN ?? 15;
126
+ const generateAiReport = options.generateAiReport ?? false;
127
+ const onProgress = options.onProgress ?? DEFAULT_PROGRESS;
128
+ const recordingWindowSec = warmupSec + durationSec;
129
+ const progressLog = [];
130
+ const say = (msg) => {
131
+ onProgress(msg);
132
+ progressLog.push(msg);
133
+ };
134
+ this.collecting = true;
135
+ const startedAt = Date.now();
136
+ let rebuildTrackingEnabled = false;
137
+ const packageName = await resolvePackageName(options.projectRoot);
138
+ if (packageName) {
139
+ this.cpuProfiler.setPackageName(packageName);
140
+ }
141
+ say(formatCollectEta(recordingWindowSec, enableMemory));
142
+ try {
143
+ say("启动 Profiler / 重建追踪 / CPU / 网络…");
144
+ await this.profiler.start();
145
+ try {
146
+ await this.rebuildTracker.start();
147
+ rebuildTrackingEnabled = true;
148
+ }
149
+ catch {
150
+ // profile 模式可能不支持 ext.flutter.inspector.trackRebuildDirtyWidgets
151
+ }
152
+ if (enableCpuProfile)
153
+ await this.cpuProfiler.start();
154
+ if (enableNetwork)
155
+ await this.networkCapture.start();
156
+ if (warmupSec > 0) {
157
+ await sleepWithProgress(warmupSec, "预热", say, Math.min(5, warmupSec));
158
+ }
159
+ await sleepWithProgress(durationSec, "录制", say, 10);
160
+ say("录制结束,正在拉取 Timeline…");
161
+ const profiling = await this.profiler.stop();
162
+ say(`Timeline 就绪(${profiling.traceEvents?.length ?? 0} 事件),统计重建…`);
163
+ const rawRebuilds = rebuildTrackingEnabled
164
+ ? await this.rebuildTracker.stop(topN)
165
+ : [];
166
+ let widgetTree;
167
+ if (rebuildTrackingEnabled) {
168
+ try {
169
+ widgetTree = await this.client.getWidgetTree();
170
+ }
171
+ catch {
172
+ // inspector 不可用时不做运行时上级追溯
173
+ }
174
+ }
175
+ const rawRebuildsForBiz = rawRebuilds;
176
+ let isolateCpu = [];
177
+ if (enableCpuProfile && this.cpuProfiler.isActive) {
178
+ try {
179
+ say("采样各 isolate CPU(已限制数量,避免 Android 过慢)…");
180
+ isolateCpu = await this.cpuProfiler.sampleAllIsolates(5);
181
+ say(`isolate 采样完成(${isolateCpu.length} 个)`);
182
+ }
183
+ catch {
184
+ isolateCpu = [];
185
+ }
186
+ }
187
+ say("汇总 CPU Top 函数…");
188
+ let topFunctions = enableCpuProfile
189
+ ? await this.cpuProfiler.stop(topN)
190
+ : [];
191
+ let cpuProfileSource = topFunctions.length > 0 ? "vm-samples" : "none";
192
+ if (topFunctions.length === 0 && enableCpuProfile) {
193
+ const fromDart = parseDartTimelineCpu(profiling.traceEvents ?? [], topN);
194
+ if (fromDart.length > 0) {
195
+ topFunctions = fromDart.map(({ source: _, ...rest }) => rest);
196
+ cpuProfileSource = "timeline-dart";
197
+ }
198
+ else {
199
+ const fromHot = parseProfilerHotspots(profiling.cpuHotspots, topN);
200
+ if (fromHot.length > 0) {
201
+ topFunctions = fromHot.map(({ source: _, ...rest }) => rest);
202
+ cpuProfileSource = "timeline-hotspot";
203
+ }
204
+ else {
205
+ const fromPhase = parsePhaseCpuFallback({
206
+ buildCount: profiling.buildPhaseAnalysis.buildCount,
207
+ totalBuildTimeMs: profiling.buildPhaseAnalysis.totalBuildTimeMs,
208
+ layoutCount: profiling.layoutPhaseAnalysis.layoutCount,
209
+ totalLayoutTimeMs: profiling.layoutPhaseAnalysis.totalLayoutTimeMs,
210
+ paintCount: profiling.paintPhaseAnalysis.paintCount,
211
+ totalPaintTimeMs: profiling.paintPhaseAnalysis.totalPaintTimeMs,
212
+ });
213
+ if (fromPhase.length > 0) {
214
+ topFunctions = fromPhase.map(({ source: _, ...rest }) => rest);
215
+ cpuProfileSource = "timeline-hotspot";
216
+ }
217
+ }
218
+ }
219
+ }
220
+ // 即便 VM 全局 Top 有数据,若业务 Top 仍空,用 Timeline 业务事件补全
221
+ if (enableCpuProfile && packageName) {
222
+ const hasBusiness = topFunctions.some((f) => isBusinessCpuEntry(f, {
223
+ projectRoot: options.projectRoot,
224
+ packageName,
225
+ }));
226
+ if (!hasBusiness) {
227
+ const fromDart = parseDartTimelineCpu(profiling.traceEvents ?? [], topN).filter((f) => isAppLibFile(f.file, packageName) ||
228
+ f.name.toLowerCase().includes("business") ||
229
+ f.name.toLowerCase().includes("hotmethod"));
230
+ if (fromDart.length > 0) {
231
+ topFunctions = [
232
+ ...topFunctions,
233
+ ...fromDart.map(({ source: _, ...rest }) => rest),
234
+ ];
235
+ if (cpuProfileSource === "none") {
236
+ cpuProfileSource = "timeline-dart";
237
+ }
238
+ }
239
+ }
240
+ }
241
+ say("解析网络请求(复用 Timeline,不再二次拉取)…");
242
+ const network = enableNetwork
243
+ ? await this.networkCapture.stop({
244
+ traceEvents: profiling.traceEvents ?? [],
245
+ })
246
+ : { total: 0, errors: 0, slow: [], requests: [] };
247
+ let profile;
248
+ if (enableMemory) {
249
+ say("采集内存快照(getAllocationProfile gc:true)…");
250
+ profile = await this.client.getAllocationProfile(undefined, true);
251
+ }
252
+ else {
253
+ say("跳过内存快照(enableMemory=false,可用 get_memory_snapshot 单独采集)");
254
+ }
255
+ const wallClockSec = Math.round(((Date.now() - startedAt) / 1000) * 10) / 10;
256
+ const result = this.buildResult({
257
+ scenario,
258
+ recordingWindowSec,
259
+ wallClockSec,
260
+ progressLog,
261
+ profiling,
262
+ topRebuilds: rawRebuildsForBiz,
263
+ widgetTree,
264
+ topFunctions,
265
+ network,
266
+ profile,
267
+ memoryCollected: enableMemory,
268
+ topN,
269
+ rebuildTrackingEnabled,
270
+ cpuProfileSource,
271
+ projectRoot: options.projectRoot,
272
+ packageName,
273
+ isolateCpu,
274
+ });
275
+ if (generateAiReport) {
276
+ say("生成 AI 规则报告…");
277
+ result.aiAnalysis = await generateAiAnalysis(result, {
278
+ projectRoot: options.projectRoot,
279
+ });
280
+ }
281
+ say(`完成:录制 ${recordingWindowSec}s,总耗时 ${wallClockSec}s`);
282
+ let savedPath;
283
+ if (options.saveToFile) {
284
+ const outputDir = resolveSessionOutputDir({
285
+ outputDir: options.outputDir,
286
+ projectRoot: options.projectRoot,
287
+ });
288
+ savedPath = await this.saveResult(result, outputDir);
289
+ if (result.aiAnalysis && savedPath) {
290
+ const mdPath = savedPath.replace(/\.json$/, ".ai.md");
291
+ const { writeFile } = await import("fs/promises");
292
+ await writeFile(mdPath, result.aiAnalysis, "utf-8");
293
+ }
294
+ }
295
+ return { result, savedPath };
296
+ }
297
+ finally {
298
+ this.collecting = false;
299
+ }
300
+ }
301
+ buildResult(input) {
302
+ const { profiling, profile, memoryCollected } = input;
303
+ const frames = profiling.frameAnalysis;
304
+ const heapUsage = profile?.memoryUsage.heapUsage ?? 0;
305
+ const heapCapacity = profile?.memoryUsage.heapCapacity ?? 0;
306
+ const utilizationPct = memoryCollected && heapCapacity > 0
307
+ ? (heapUsage / heapCapacity) * 100
308
+ : 0;
309
+ const bizOpts = {
310
+ projectRoot: input.projectRoot,
311
+ packageName: input.packageName,
312
+ };
313
+ const traceEvents = profiling.traceEvents ?? [];
314
+ const targetFps = JANK_TARGET_FPS;
315
+ const gc = parseGcStats(traceEvents);
316
+ const imageDecode = parseImageDecodeStats(traceEvents);
317
+ const scrollSegments = parseScrollFpsSegments(traceEvents, targetFps, 2);
318
+ let worstScroll = pickWorstScrollSegments(scrollSegments, 3);
319
+ let overallFps = scrollSegments.length > 0
320
+ ? Math.round((scrollSegments.reduce((s, x) => s + x.fps, 0) /
321
+ scrollSegments.length) *
322
+ 10) / 10
323
+ : 0;
324
+ // Timeline 分段为空时,用 Profiler 帧统计回退整体 FPS
325
+ if (overallFps === 0 &&
326
+ frames.totalFrames > 5 &&
327
+ input.recordingWindowSec > 0) {
328
+ overallFps =
329
+ Math.round((frames.totalFrames / input.recordingWindowSec) * 10) / 10;
330
+ worstScroll = [
331
+ {
332
+ startSec: 0,
333
+ endSec: Math.round(input.recordingWindowSec * 10) / 10,
334
+ frames: frames.totalFrames,
335
+ fps: overallFps,
336
+ jankCount: frames.jankFrames,
337
+ jankPct: Math.round(frames.jankPercentage * 10) / 10,
338
+ avgMs: Math.round(frames.averageFrameTimeMs * 100) / 100,
339
+ maxMs: Math.round(frames.maxFrameTimeMs * 100) / 100,
340
+ },
341
+ ];
342
+ }
343
+ const validMembers = (profile?.members ?? []).filter((m) => m.class?.name);
344
+ const sortedBySize = [...validMembers]
345
+ .sort((a, b) => b.bytesCurrent - a.bytesCurrent)
346
+ .filter((m) => m.bytesCurrent > 0)
347
+ .slice(0, input.topN);
348
+ const suspicious = validMembers
349
+ .filter((m) => !isVmInternal(m.class.name) && m.instancesCurrent > 500)
350
+ .slice(0, 5)
351
+ .map((m) => `${m.class.name}: ${m.instancesCurrent} instances`);
352
+ const filesToInspect = [
353
+ ...new Set([
354
+ ...input.topRebuilds.map((r) => r.file),
355
+ ...input.topFunctions
356
+ .filter((f) => isBusinessCpuEntry(f, bizOpts))
357
+ .map((f) => f.file),
358
+ "lib/main.dart",
359
+ ].filter((f) => f &&
360
+ f !== "unknown" &&
361
+ (isProjectSourceFile(f) || isAppLibFile(f, input.packageName)))),
362
+ ].slice(0, 10);
363
+ const topRebuilds = input.projectRoot && input.topRebuilds.length > 0
364
+ ? enrichRebuildsWithBusiness(input.topRebuilds, {
365
+ projectRoot: input.projectRoot,
366
+ widgetTree: input.widgetTree,
367
+ hintPaths: filesToInspect,
368
+ })
369
+ : input.topRebuilds;
370
+ const projectTopFunctions = input.topFunctions
371
+ .filter((f) => isBusinessCpuEntry(f, bizOpts))
372
+ .sort((a, b) => b.selfMs - a.selfMs)
373
+ .slice(0, 10)
374
+ .map(({ sourceUrl: _, ...rest }) => rest);
375
+ // Timeline 仅合并「明确业务埋点」事件,避免 BUILD/Rasterizer 等被默认落到 lib/main.dart
376
+ {
377
+ const FRAMEWORK_TIMELINE_NAMES = /^(build|paint|layout|rasterizer|composit|surfaceframe|vsync|frame|pipeline|gpu)/i;
378
+ const timelineProjectFunctions = parseDartTimelineCpu(profiling.traceEvents ?? [], 20).filter((f) => {
379
+ if (FRAMEWORK_TIMELINE_NAMES.test(f.name))
380
+ return false;
381
+ if (isFrameworkOrSdkFile(f.file))
382
+ return false;
383
+ return (isKnownBusinessSymbol(f.name) ||
384
+ f.name.toLowerCase().includes("business") ||
385
+ f.name.toLowerCase().includes("hotmethod"));
386
+ });
387
+ const existing = new Set(projectTopFunctions.map((f) => `${f.file}::${f.name}`));
388
+ for (const f of timelineProjectFunctions) {
389
+ const key = `${f.file}::${f.name}`;
390
+ if (existing.has(key))
391
+ continue;
392
+ projectTopFunctions.push({
393
+ name: f.name,
394
+ file: f.file.startsWith("lib/") ? f.file : "lib/main.dart",
395
+ selfMs: f.selfMs,
396
+ pct: f.pct,
397
+ severity: f.severity,
398
+ });
399
+ existing.add(key);
400
+ }
401
+ // 去掉误入的 SDK / 框架路径 / VM 符号
402
+ for (let i = projectTopFunctions.length - 1; i >= 0; i--) {
403
+ const f = projectTopFunctions[i];
404
+ if (isFrameworkOrSdkFile(f.file) ||
405
+ isVmInternalCpuSymbol(f.name) ||
406
+ isMisattributedVmCpuEntry(f.name, f.file) ||
407
+ FRAMEWORK_TIMELINE_NAMES.test(f.name) ||
408
+ f.file.includes("lib/_http/") ||
409
+ f.file.includes("lib/ui/") ||
410
+ f.file.includes("lib/internal/")) {
411
+ projectTopFunctions.splice(i, 1);
412
+ }
413
+ }
414
+ projectTopFunctions.sort((a, b) => b.selfMs - a.selfMs);
415
+ projectTopFunctions.splice(10);
416
+ }
417
+ const vmTopFunctions = input.topFunctions
418
+ .filter((f) => isVmInternalCpuSymbol(f.name) ||
419
+ isMisattributedVmCpuEntry(f.name, f.file))
420
+ .sort((a, b) => b.selfMs - a.selfMs)
421
+ .slice(0, 5)
422
+ .map(({ sourceUrl: _, name, ...rest }) => ({
423
+ ...rest,
424
+ rawName: name,
425
+ name: humanizeVmCpuSymbol(name),
426
+ }));
427
+ // 将 lib/main.dart 的 <anonymous closure> 标注为更可读名称
428
+ const hasHot = projectTopFunctions.some((f) => f.name.includes("businessHotMethod"));
429
+ if (hasHot) {
430
+ for (const f of projectTopFunctions) {
431
+ if (f.name.includes("anonymous") && f.file.includes("main.dart")) {
432
+ f.name = "businessHotMethod.<anonymous>";
433
+ }
434
+ }
435
+ }
436
+ const topFunctions = input.topFunctions
437
+ .map(({ sourceUrl: _, inclusiveMs: _i, ...rest }) => rest)
438
+ .slice(0, input.topN);
439
+ const hintsForAnalysis = [];
440
+ if (!input.rebuildTrackingEnabled) {
441
+ hintsForAnalysis.push("未启用 Widget 重建追踪(profile 模式不支持 inspector 扩展,可改用 debug 模式采集重建数据)");
442
+ }
443
+ if (frames.jankPercentage > 5) {
444
+ hintsForAnalysis.push(`掉帧率偏高: ${frames.jankPercentage.toFixed(1)}%(${frames.jankFrames}/${frames.totalFrames} 帧)`);
445
+ }
446
+ for (const r of topRebuilds.filter((x) => x.count > 50).slice(0, 3)) {
447
+ const loc = r.bizFile
448
+ ? `${r.bizWidget ?? r.widget} @ ${r.bizFile}:${r.bizLine ?? r.line}`
449
+ : `${r.widget} @ ${r.file}:${r.line}`;
450
+ hintsForAnalysis.push(`过度重建: ${loc}(${r.count} 次)`);
451
+ }
452
+ // 不把框架 drawFrame 链路写入线索:对业务定位无帮助,改看 projectTopFunctions
453
+ if (input.cpuProfileSource &&
454
+ input.cpuProfileSource !== "vm-samples" &&
455
+ input.topFunctions.length > 0) {
456
+ const reason = input.rebuildTrackingEnabled
457
+ ? "debug 模式 VM getCpuSamples 常为空"
458
+ : "profile 模式 VM 采样未命中项目符号或 Timeline 更完整";
459
+ hintsForAnalysis.push(`CPU 数据来自 ${input.cpuProfileSource} 回退(${reason})`);
460
+ }
461
+ if (memoryCollected && utilizationPct > 85) {
462
+ hintsForAnalysis.push(`内存利用率偏高: ${utilizationPct.toFixed(1)}%`);
463
+ }
464
+ if (gc.count > 0) {
465
+ const top = gc.topPauses && gc.topPauses.length > 0
466
+ ? `;Top5: ${gc.topPauses
467
+ .map((p) => `${p.name} ${p.ms}ms`)
468
+ .join(", ")}`
469
+ : "";
470
+ hintsForAnalysis.push(`GC: ${gc.count} 次, 总停顿 ${gc.totalPauseMs}ms, 最长 ${gc.maxPauseMs}ms` +
471
+ (gc.longPauseCount > 0
472
+ ? `(>${8}ms 停顿 ${gc.longPauseCount} 次)`
473
+ : "") +
474
+ top);
475
+ }
476
+ if (gc.longPauseCount >= 3 || gc.maxPauseMs > 32) {
477
+ hintsForAnalysis.push("GC 停顿偏多/偏长,可能造成卡顿尖刺;检查每帧短生命周期对象分配");
478
+ }
479
+ if (worstScroll.length > 0) {
480
+ const w = worstScroll[0];
481
+ hintsForAnalysis.push(`滚动最差段 ${w.startSec}-${w.endSec}s: ${w.fps} FPS, jank ${w.jankPct}% (max ${w.maxMs}ms)`);
482
+ }
483
+ if (imageDecode.count > 0) {
484
+ const top = imageDecode.slow[0];
485
+ const detail = top ? `;最慢 ${formatSlowImageDecodeLine(top)}` : "";
486
+ hintsForAnalysis.push(`图片解码: ${imageDecode.count} 次, 合计 ${imageDecode.totalMs}ms, 最长 ${imageDecode.maxMs}ms${detail}`);
487
+ }
488
+ else if ((imageDecode.untimedSignals ?? 0) > 0) {
489
+ hintsForAnalysis.push(`图片解码: 检测到 ${imageDecode.untimedSignals} 次信号但无时长(请确认 AppImageLoader 写入 args.ms)`);
490
+ }
491
+ if (imageDecode.maxMs > 32) {
492
+ const top = imageDecode.slow[0];
493
+ const where = top?.url ? `(${top.url})` : "";
494
+ hintsForAnalysis.push(`存在较慢图片解码${where},建议预解码/缓存/缩小分辨率或使用 cached_network_image`);
495
+ }
496
+ const networkAggregate = aggregateNetworkRequests(input.network.requests ?? [], input.recordingWindowSec);
497
+ const dioJsonDecode = buildDioJsonDecodeInsight({
498
+ isolateCpu: input.isolateCpu ?? [],
499
+ networkTotal: input.network.total,
500
+ durationSec: input.recordingWindowSec,
501
+ aggregate: networkAggregate,
502
+ });
503
+ const topParsePaths = aggregateParseCostByPath(input.network.requests ?? [], dioJsonDecode?.totalWorkerCpuMs ?? 0);
504
+ const thresholdAlerts = buildThresholdAlerts({
505
+ requests: input.network.requests ?? [],
506
+ imageDecode,
507
+ parsePaths: topParsePaths,
508
+ dioWorkerCpuMs: dioJsonDecode?.totalWorkerCpuMs,
509
+ });
510
+ if (thresholdAlerts.summary.largeImageHits > 0 || thresholdAlerts.summary.slowDecodeHits > 0) {
511
+ hintsForAnalysis.push(`大图片超阈: 体积 ${thresholdAlerts.summary.largeImageHits} 次, 慢解码 ${thresholdAlerts.summary.slowDecodeHits} 次(${thresholdAlerts.config.largeImageBytes / 1024}KB / ${thresholdAlerts.config.slowImageDecodeMs}ms)`);
512
+ }
513
+ if (thresholdAlerts.summary.largeApiHits > 0 || thresholdAlerts.summary.slowParseHits > 0) {
514
+ hintsForAnalysis.push(`大接口超阈: 体积 ${thresholdAlerts.summary.largeApiHits} 个 path, 慢解析 ${thresholdAlerts.summary.slowParseHits} 个 path(${thresholdAlerts.config.largeApiBytes / 1024}KB / ${thresholdAlerts.config.slowApiParseMs}ms)`);
515
+ }
516
+ if (dioJsonDecode) {
517
+ hintsForAnalysis.push(`Dio 后台 JSON 解析: ${dioJsonDecode.workerCount} worker, CPU ~${dioJsonDecode.totalWorkerCpuMs}ms, HTTP ${dioJsonDecode.networkTotal} 次 (${dioJsonDecode.networkQps} req/s), ≥${dioJsonDecode.isolateThresholdKb}KB 响应 ${dioJsonDecode.largeJsonResponseCount} 次`);
518
+ }
519
+ else {
520
+ const bgIsolates = (input.isolateCpu ?? []).filter((i) => !i.isMain);
521
+ if (bgIsolates.length > 0) {
522
+ const busy = bgIsolates.filter((i) => i.topSelfMs > 50);
523
+ hintsForAnalysis.push(`后台 isolate ${bgIsolates.length} 个` +
524
+ (busy.length
525
+ ? `,较忙: ${busy.map((i) => `${i.name}(${i.topName} ${i.topSelfMs}ms)`).join(", ")}`
526
+ : "(负载较低)"));
527
+ }
528
+ else if ((input.isolateCpu ?? []).length <= 1) {
529
+ hintsForAnalysis.push("仅主 isolate 有 CPU 采样;重计算可考虑 compute()/Isolate 下沉");
530
+ }
531
+ }
532
+ for (const p of topParsePaths.slice(0, 3)) {
533
+ hintsForAnalysis.push(`JSON 解析 Top: ${p.method ?? "GET"} ${p.path} ×${p.count}` +
534
+ ` avg ${p.avgParseMs}ms 合计 ${p.totalParseMs}ms` +
535
+ (p.totalBytes ? ` ${Math.round(p.totalBytes / 1024)}KB` : ""));
536
+ }
537
+ if (networkAggregate.topPaths.length > 0 && topParsePaths.length === 0) {
538
+ for (const p of networkAggregate.topPaths.slice(0, 3)) {
539
+ hintsForAnalysis.push(`HTTP Top: ${p.path} ×${p.count}` +
540
+ (p.avgMs != null ? ` avg ${p.avgMs}ms` : ""));
541
+ }
542
+ }
543
+ if (hintsForAnalysis.length === 0) {
544
+ hintsForAnalysis.push("未发现明显性能问题,可结合代码做进一步确认。");
545
+ }
546
+ return {
547
+ scenario: input.scenario,
548
+ durationSec: input.wallClockSec,
549
+ recordingWindowSec: input.recordingWindowSec,
550
+ wallClockSec: input.wallClockSec,
551
+ progressLog: input.progressLog,
552
+ profileModeHint: `录制窗口 ${input.recordingWindowSec}s;总耗时含收尾分析(Android 真机常见 +30~120s)。建议使用 flutter run --profile 以获得更准确的性能数据。`,
553
+ frames: {
554
+ total: frames.totalFrames,
555
+ jankCount: frames.jankFrames,
556
+ jankPct: Math.round(frames.jankPercentage * 10) / 10,
557
+ avgMs: Math.round(frames.averageFrameTimeMs * 100) / 100,
558
+ p99Ms: Math.round(frames.p99FrameTimeMs * 100) / 100,
559
+ buildMaxMs: profiling.buildPhaseAnalysis.maxBuildTimeMs,
560
+ layoutMaxMs: profiling.layoutPhaseAnalysis.maxLayoutTimeMs,
561
+ paintMaxMs: profiling.paintPhaseAnalysis.maxPaintTimeMs,
562
+ },
563
+ topFunctions,
564
+ projectTopFunctions,
565
+ vmTopFunctions,
566
+ topRebuilds,
567
+ memory: {
568
+ collected: memoryCollected,
569
+ heapMb: memoryCollected
570
+ ? Math.round((heapUsage / 1024 / 1024) * 100) / 100
571
+ : 0,
572
+ utilizationPct: memoryCollected
573
+ ? Math.round(utilizationPct * 10) / 10
574
+ : 0,
575
+ topClasses: memoryCollected
576
+ ? sortedBySize.map((m) => ({
577
+ name: m.class.name,
578
+ instances: m.instancesCurrent,
579
+ bytesMb: Math.round((m.bytesCurrent / 1024 / 1024) * 100) / 100,
580
+ }))
581
+ : [],
582
+ suspicious: memoryCollected ? suspicious : [],
583
+ },
584
+ network: {
585
+ total: input.network.total,
586
+ errors: input.network.errors,
587
+ slow: input.network.slow,
588
+ qps: networkAggregate.qps,
589
+ avgMs: networkAggregate.avgMs,
590
+ largeJsonResponseCount: networkAggregate.largeJsonResponseCount,
591
+ topPaths: networkAggregate.topPaths,
592
+ topParsePaths,
593
+ },
594
+ thresholdAlerts,
595
+ dioJsonDecode,
596
+ gc,
597
+ scrollFps: {
598
+ segmentSec: 2,
599
+ overallFps,
600
+ worstSegments: worstScroll,
601
+ },
602
+ imageDecode,
603
+ isolateCpu: input.isolateCpu ?? [],
604
+ filesToInspect,
605
+ hintsForAnalysis,
606
+ cpuProfileSource: input.cpuProfileSource,
607
+ aiNextStep: "请读取 filesToInspect 中的 Dart 源码,按 P0/P1/P2 输出优化方案(数据依据 + 代码原因 + 改法)。",
608
+ };
609
+ }
610
+ async saveResult(result, outputDir) {
611
+ await mkdir(outputDir, { recursive: true });
612
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
613
+ const safeName = result.scenario.replace(/[^a-zA-Z0-9_-]/g, "_");
614
+ const filePath = join(outputDir, `${safeName}-${ts}.json`);
615
+ await writeFile(filePath, JSON.stringify(result, null, 2), "utf-8");
616
+ return filePath;
617
+ }
618
+ }
619
+ export function formatSessionSummary(result) {
620
+ const rec = result.recordingWindowSec ?? result.durationSec;
621
+ const wall = result.wallClockSec ?? result.durationSec;
622
+ const lines = [
623
+ `场景: ${result.scenario} | 录制: ${rec}s | 总耗时: ${wall}s | 掉帧: ${result.frames.jankPct}%`,
624
+ `帧: 平均 ${result.frames.avgMs}ms, P99 ${result.frames.p99Ms}ms, 共 ${result.frames.total} 帧`,
625
+ ];
626
+ if (result.topRebuilds.length > 0) {
627
+ const shown = result.topRebuilds.slice(0, 3);
628
+ if (shown.length === 1) {
629
+ const r = shown[0];
630
+ lines.push(`重建最多: ${r.widget} ${r.count} 次 @ ${r.file}:${r.line}`);
631
+ }
632
+ else {
633
+ lines.push("重建最多(Top3):");
634
+ for (const r of shown) {
635
+ lines.push(` - ${r.widget} ${r.count} 次 @ ${r.file}:${r.line}`);
636
+ }
637
+ }
638
+ }
639
+ if (result.projectTopFunctions[0]) {
640
+ const f = result.projectTopFunctions[0];
641
+ lines.push(`耗时函数最热: ${f.name} Self ${f.selfMs}ms @ ${f.file}`);
642
+ }
643
+ else if (result.vmTopFunctions?.[0]) {
644
+ const v = result.vmTopFunctions[0];
645
+ lines.push(`耗时函数最热: ${v.name} Self ${v.selfMs}ms(VM 符号,非业务;见报告 VM 热点表)`);
646
+ }
647
+ else {
648
+ lines.push("耗时函数最热: 未命中(请检查 package URI 归因或延长采集)");
649
+ }
650
+ if (result.scrollFps?.worstSegments?.[0]) {
651
+ const s = result.scrollFps.worstSegments[0];
652
+ lines.push(`滚动最差段: ${s.startSec}-${s.endSec}s ${s.fps}FPS jank ${s.jankPct}%`);
653
+ }
654
+ if (result.gc && result.gc.count > 0) {
655
+ lines.push(`GC: ${result.gc.count} 次, 最长停顿 ${result.gc.maxPauseMs}ms`);
656
+ }
657
+ if (result.imageDecode && result.imageDecode.count > 0) {
658
+ const top = result.imageDecode.slow[0];
659
+ lines.push(`图片解码: ${result.imageDecode.count} 次, 最长 ${result.imageDecode.maxMs}ms` +
660
+ (top?.url ? ` — ${top.url}` : ""));
661
+ }
662
+ if (result.isolateCpu && result.isolateCpu.length > 0) {
663
+ const main = result.isolateCpu.find((i) => i.isMain);
664
+ const bg = result.isolateCpu.filter((i) => !i.isMain).length;
665
+ lines.push(`Isolate: 主 top=${main?.topName ?? "-"} ${main?.topSelfMs ?? 0}ms, 后台 ${bg} 个`);
666
+ }
667
+ if (result.cpuProfileSource && result.cpuProfileSource !== "none") {
668
+ lines.push(`CPU 来源: ${result.cpuProfileSource}`);
669
+ }
670
+ if (result.network.total > 0) {
671
+ const netParts = [
672
+ `${result.network.total} 请求`,
673
+ `${result.network.errors} 错误`,
674
+ `${result.network.slow.length} 个慢请求`,
675
+ ];
676
+ if (result.network.qps != null)
677
+ netParts.push(`${result.network.qps} req/s`);
678
+ if (result.dioJsonDecode) {
679
+ netParts.push(`Dio worker ${result.dioJsonDecode.workerCount}, ~${result.dioJsonDecode.totalWorkerCpuMs}ms`);
680
+ }
681
+ lines.push(`网络: ${netParts.join(", ")}`);
682
+ }
683
+ lines.push(`待分析文件: ${result.filesToInspect.join(", ") || "无"}`);
684
+ if (result.progressLog?.length) {
685
+ lines.push("", "采集进度:");
686
+ for (const p of result.progressLog)
687
+ lines.push(`- ${p}`);
688
+ }
689
+ lines.push("");
690
+ lines.push("分析线索:");
691
+ for (const h of result.hintsForAnalysis)
692
+ lines.push(`- ${h}`);
693
+ lines.push("");
694
+ lines.push(result.aiNextStep);
695
+ return lines.join("\n");
696
+ }
697
+ //# sourceMappingURL=performance-session.js.map