@electron-memory/monitor 0.2.3 → 0.2.6

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.
package/dist/index.mjs CHANGED
@@ -261,13 +261,13 @@ var DataPersister = class {
261
261
  return null;
262
262
  }
263
263
  }
264
- /** 获取所有会话列表 */
264
+ /** 获取所有会话列表(按 startTime 降序,最新在前;与索引写入顺序无关) */
265
265
  getSessions() {
266
266
  const indexFile = path.join(this.storageDir, "sessions.json");
267
267
  try {
268
268
  const content = fs.readFileSync(indexFile, "utf-8");
269
269
  const index = JSON.parse(content);
270
- return index.sessions;
270
+ return this.sortSessionsByStartDesc(index.sessions);
271
271
  } catch {
272
272
  return [];
273
273
  }
@@ -370,11 +370,19 @@ var DataPersister = class {
370
370
  saveSessionIndex(sessions) {
371
371
  const indexFile = path.join(this.storageDir, "sessions.json");
372
372
  const index = {
373
- sessions,
373
+ sessions: this.sortSessionsByStartDesc(sessions),
374
374
  lastUpdated: Date.now()
375
375
  };
376
376
  fs.writeFileSync(indexFile, JSON.stringify(index, null, 2), "utf-8");
377
377
  }
378
+ /** 统一排序:startTime 新 → 旧(同毫秒时按 id 稳定排序) */
379
+ sortSessionsByStartDesc(sessions) {
380
+ return [...sessions].sort((a, b) => {
381
+ const t = b.startTime - a.startTime;
382
+ if (t !== 0) return t;
383
+ return String(b.id).localeCompare(String(a.id));
384
+ });
385
+ }
378
386
  ensureDirectory(dir) {
379
387
  if (!fs.existsSync(dir)) {
380
388
  fs.mkdirSync(dir, { recursive: true });
@@ -437,10 +445,14 @@ var SessionManager = class {
437
445
  getCurrentSession() {
438
446
  return this.currentSession;
439
447
  }
440
- /** 开始新会话 */
448
+ /** 开始新会话;若顶替了上一条进行中的会话,通过 `replaced` 返回以便主进程补写 report.json */
441
449
  startSession(label, description) {
450
+ if (!this.currentSession) {
451
+ this.reconcileStaleRunningInIndex();
452
+ }
453
+ let replaced = null;
442
454
  if (this.currentSession && this.currentSession.status === "running") {
443
- this.endSession();
455
+ replaced = this.endSession();
444
456
  }
445
457
  const sessionId = v4();
446
458
  const { dataFile, metaFile } = this.persister.createSessionFiles(sessionId);
@@ -456,7 +468,7 @@ var SessionManager = class {
456
468
  };
457
469
  this.currentSession = session;
458
470
  this.persister.saveSessionMeta(session);
459
- return session;
471
+ return { session, replaced };
460
472
  }
461
473
  /** 结束当前会话 */
462
474
  endSession() {
@@ -484,6 +496,24 @@ var SessionManager = class {
484
496
  getSession(sessionId) {
485
497
  return this.persister.readSessionMeta(sessionId);
486
498
  }
499
+ /**
500
+ * 当前内存无活动会话时,将索引中仍为 running 的条目标为 aborted(进程异常退出后残留)
501
+ */
502
+ reconcileStaleRunningInIndex() {
503
+ if (this.currentSession) return;
504
+ const sessions = this.persister.getSessions();
505
+ const now = Date.now();
506
+ for (const s of sessions) {
507
+ if (s.status !== "running") continue;
508
+ const fixed = {
509
+ ...s,
510
+ status: "aborted",
511
+ endTime: now,
512
+ duration: now - s.startTime
513
+ };
514
+ this.persister.saveSessionMeta(fixed);
515
+ }
516
+ }
487
517
  };
488
518
 
489
519
  // src/core/anomaly.ts
@@ -567,7 +597,18 @@ var AnomalyDetector = class extends EventEmitter2 {
567
597
  title: "\u603B\u5185\u5B58\u6301\u7EED\u589E\u957F",
568
598
  description: `\u5185\u5B58\u4EE5 ${slope.toFixed(2)} KB/s \u7684\u901F\u7387\u6301\u7EED\u589E\u957F (R\xB2=${r2.toFixed(3)})`,
569
599
  value: slope,
570
- threshold: 10
600
+ threshold: 10,
601
+ suggestions: [
602
+ "\u5BFC\u51FA\u5806\u5FEB\u7167 (Heap Snapshot)\uFF0C\u4F7F\u7528 Chrome DevTools \u7684 Memory \u9762\u677F\u52A0\u8F7D\u5206\u6790",
603
+ '\u5BF9\u6BD4\u4E24\u4E2A\u65F6\u95F4\u70B9\u7684\u5806\u5FEB\u7167\uFF0C\u67E5\u627E "Allocated between snapshots" \u4E2D\u65B0\u589E\u7684\u5927\u5BF9\u8C61',
604
+ "\u68C0\u67E5\u4E3B\u8FDB\u7A0B\u4E2D\u662F\u5426\u6709\u672A\u6E05\u7406\u7684 setInterval / setTimeout \u56DE\u8C03",
605
+ "\u68C0\u67E5 ipcMain.on \u662F\u5426\u5B58\u5728\u91CD\u590D\u6CE8\u518C\uFF08\u6BCF\u6B21\u7A97\u53E3\u521B\u5EFA\u90FD\u6CE8\u518C\u4F46\u4E0D\u79FB\u9664\uFF09",
606
+ "\u68C0\u67E5\u662F\u5426\u6709\u6301\u7EED\u589E\u957F\u7684 Map / Set / Array \u7F13\u5B58\u672A\u8BBE\u7F6E\u4E0A\u9650\u6216\u8FC7\u671F\u7B56\u7565"
607
+ ],
608
+ actions: [
609
+ { id: "take-heap-snapshot", label: "\u{1F4F8} \u5BFC\u51FA\u5806\u5FEB\u7167", type: "heap-snapshot" },
610
+ { id: "trigger-gc", label: "\u{1F5D1}\uFE0F \u89E6\u53D1 GC", type: "trigger-gc" }
611
+ ]
571
612
  };
572
613
  }
573
614
  return null;
@@ -592,7 +633,18 @@ var AnomalyDetector = class extends EventEmitter2 {
592
633
  title: "\u5185\u5B58\u7A81\u589E",
593
634
  description: `\u603B\u5185\u5B58\u4ECE ${Math.round(avg)} KB \u7A81\u589E\u5230 ${current} KB (+${((current - avg) / avg * 100).toFixed(1)}%)`,
594
635
  value: current,
595
- threshold: avg * 1.5
636
+ threshold: avg * 1.5,
637
+ suggestions: [
638
+ "\u7ACB\u5373\u5BFC\u51FA\u5806\u5FEB\u7167\uFF0C\u4E0E\u7A81\u589E\u524D\u7684\u5FEB\u7167\u5BF9\u6BD4\uFF0C\u5B9A\u4F4D\u65B0\u589E\u7684\u5927\u5BF9\u8C61",
639
+ "\u68C0\u67E5\u662F\u5426\u6709\u5927\u91CF\u65B0\u7A97\u53E3/\u6807\u7B7E\u9875\u540C\u65F6\u521B\u5EFA",
640
+ "\u68C0\u67E5\u662F\u5426\u52A0\u8F7D\u4E86\u5927\u6587\u4EF6\u6216\u5927\u91CF\u56FE\u7247\u8D44\u6E90",
641
+ "\u68C0\u67E5 IPC \u901A\u4FE1\u662F\u5426\u4F20\u8F93\u4E86\u8D85\u5927\u6570\u636E\uFF08\u5EFA\u8BAE\u5206\u7247\u6216\u4F7F\u7528 MessagePort\uFF09",
642
+ "\u89E6\u53D1 GC \u540E\u89C2\u5BDF\u5185\u5B58\u662F\u5426\u56DE\u843D\uFF0C\u5982\u4E0D\u56DE\u843D\u5219\u4E3A\u771F\u5B9E\u6CC4\u6F0F"
643
+ ],
644
+ actions: [
645
+ { id: "take-heap-snapshot", label: "\u{1F4F8} \u5BFC\u51FA\u5806\u5FEB\u7167", type: "heap-snapshot" },
646
+ { id: "trigger-gc", label: "\u{1F5D1}\uFE0F \u89E6\u53D1 GC", type: "trigger-gc" }
647
+ ]
596
648
  };
597
649
  }
598
650
  return null;
@@ -614,7 +666,17 @@ var AnomalyDetector = class extends EventEmitter2 {
614
666
  title: `\u68C0\u6D4B\u5230 ${detached} \u4E2A\u5206\u79BB\u7684 V8 \u4E0A\u4E0B\u6587`,
615
667
  description: "\u5B58\u5728\u672A\u6B63\u786E\u9500\u6BC1\u7684 BrowserWindow \u6216 WebContents\uFF0C\u53EF\u80FD\u5BFC\u81F4\u5185\u5B58\u6CC4\u6F0F",
616
668
  value: detached,
617
- threshold: 0
669
+ threshold: 0,
670
+ suggestions: [
671
+ '\u5728 Chrome DevTools Memory \u9762\u677F\u5BFC\u51FA\u5806\u5FEB\u7167\uFF0C\u641C\u7D22 "Detached" \u67E5\u627E\u6B8B\u7559\u7684 DOM \u6811\u548C JS \u4E0A\u4E0B\u6587',
672
+ "\u68C0\u67E5\u6240\u6709 BrowserWindow \u662F\u5426\u5728\u5173\u95ED\u65F6\u8C03\u7528\u4E86 destroy()\uFF08\u800C\u975E\u4EC5 close()\uFF09",
673
+ '\u68C0\u67E5 BrowserWindow.on("closed", ...) \u56DE\u8C03\u4E2D\u662F\u5426\u5C06\u7A97\u53E3\u5F15\u7528\u7F6E\u4E3A null',
674
+ "\u68C0\u67E5\u662F\u5426\u6709\u95ED\u5305\uFF08\u5982 ipcMain.on \u56DE\u8C03\uFF09\u6301\u6709\u5DF2\u5173\u95ED\u7A97\u53E3\u7684 webContents \u5F15\u7528",
675
+ "\u68C0\u67E5 ipcMain.on / ipcMain.handle \u662F\u5426\u5728\u7A97\u53E3\u5173\u95ED\u540E\u6B63\u786E\u79FB\u9664\u76D1\u542C"
676
+ ],
677
+ actions: [
678
+ { id: "take-heap-snapshot", label: "\u{1F4F8} \u5BFC\u51FA\u5806\u5FEB\u7167", type: "heap-snapshot" }
679
+ ]
618
680
  };
619
681
  }
620
682
  return null;
@@ -638,7 +700,18 @@ var AnomalyDetector = class extends EventEmitter2 {
638
700
  title: `V8 \u5806\u4F7F\u7528\u7387 ${(usagePercent * 100).toFixed(1)}%`,
639
701
  description: `\u4E3B\u8FDB\u7A0B V8 \u5806\u4F7F\u7528 ${Math.round(heapUsed / 1024 / 1024)} MB / ${Math.round(heapTotal / 1024 / 1024)} MB`,
640
702
  value: usagePercent * 100,
641
- threshold: 85
703
+ threshold: 85,
704
+ suggestions: [
705
+ "\u5BFC\u51FA\u5806\u5FEB\u7167 (Heap Snapshot)\uFF0C\u4F7F\u7528 Chrome DevTools \u7684 Memory \u9762\u677F\u5206\u6790\u5BF9\u8C61\u7559\u5B58",
706
+ '\u5BF9\u6BD4\u4E24\u4E2A\u65F6\u95F4\u70B9\u7684\u5806\u5FEB\u7167\uFF0C\u67E5\u627E "Allocated between snapshots" \u4E2D\u7684\u6CC4\u6F0F\u5BF9\u8C61',
707
+ "\u68C0\u67E5 Event Listeners \u662F\u5426\u6B63\u786E\u6E05\u7406\uFF08\u7279\u522B\u662F ipcMain / EventEmitter \u4E0A\u7684\u76D1\u542C\u5668\uFF09",
708
+ "\u68C0\u67E5 Promise \u94FE\u662F\u5426\u6709\u672A\u5904\u7406\u7684 rejection \u5BFC\u81F4\u5F15\u7528\u672A\u91CA\u653E",
709
+ "\u89E6\u53D1 GC \u540E\u89C2\u5BDF\u4F7F\u7528\u7387\u662F\u5426\u4E0B\u964D\uFF0C\u82E5\u4E0D\u964D\u5219\u786E\u8BA4\u4E3A\u6CC4\u6F0F"
710
+ ],
711
+ actions: [
712
+ { id: "take-heap-snapshot", label: "\u{1F4F8} \u5BFC\u51FA\u5806\u5FEB\u7167", type: "heap-snapshot" },
713
+ { id: "trigger-gc", label: "\u{1F5D1}\uFE0F \u89E6\u53D1 GC", type: "trigger-gc" }
714
+ ]
642
715
  };
643
716
  }
644
717
  }
@@ -654,10 +727,38 @@ import * as os2 from "os";
654
727
  var Analyzer = class {
655
728
  /** 生成会话报告 */
656
729
  generateReport(sessionId, label, description, startTime, endTime, snapshots, anomalies, dataFile) {
730
+ const environment = this.collectEnvironment();
731
+ const eventMarks = this.collectEventMarks(snapshots);
657
732
  if (snapshots.length === 0) {
658
- throw new Error("No snapshots to analyze");
733
+ const summary2 = this.emptySummary();
734
+ const suggestions2 = [
735
+ {
736
+ id: "no-snapshots",
737
+ severity: "info",
738
+ category: "optimization",
739
+ title: "\u4F1A\u8BDD\u5185\u6CA1\u6709\u53EF\u7528\u7684\u5185\u5B58\u5FEB\u7167",
740
+ description: "\u7ED3\u675F\u4F1A\u8BDD\u65F6\u78C1\u76D8\u4E0A\u5C1A\u672A\u5199\u5165\u4EFB\u4F55\u91C7\u6837\u70B9\uFF08\u4F8B\u5982\u521A\u542F\u52A8\u5C31\u7ACB\u523B\u7ED3\u675F\uFF0C\u6216\u91C7\u96C6\u95F4\u9694\u5C1A\u672A\u89E6\u53D1\uFF09\u3002\u62A5\u544A\u4E2D\u7684\u7EDF\u8BA1\u4E0E\u8D8B\u52BF\u65E0\u6CD5\u8BA1\u7B97\u3002",
741
+ suggestions: [
742
+ "\u4FDD\u6301\u4F1A\u8BDD\u5F00\u542F\u81F3\u5C11\u4E00\u4E2A\u91C7\u96C6\u5468\u671F\u540E\u518D\u70B9\u300C\u7ED3\u675F\u4F1A\u8BDD\u300D",
743
+ "\u5728\u914D\u7F6E\u4E2D\u9002\u5F53\u7F29\u77ED\u91C7\u96C6\u95F4\u9694\u4EE5\u4FBF\u66F4\u5FEB\u5F97\u5230\u6570\u636E"
744
+ ]
745
+ }
746
+ ];
747
+ return {
748
+ sessionId,
749
+ label,
750
+ description,
751
+ startTime,
752
+ endTime,
753
+ duration: endTime - startTime,
754
+ environment,
755
+ summary: summary2,
756
+ anomalies,
757
+ suggestions: suggestions2,
758
+ eventMarks,
759
+ dataFile
760
+ };
659
761
  }
660
- const environment = this.collectEnvironment();
661
762
  const summary = this.computeSummary(snapshots);
662
763
  const suggestions = this.generateSuggestions(snapshots, summary, anomalies);
663
764
  return {
@@ -671,6 +772,7 @@ var Analyzer = class {
671
772
  summary,
672
773
  anomalies,
673
774
  suggestions,
775
+ eventMarks,
674
776
  dataFile
675
777
  };
676
778
  }
@@ -707,6 +809,53 @@ var Analyzer = class {
707
809
  };
708
810
  }
709
811
  // ===== 私有方法 =====
812
+ emptySummary() {
813
+ const z = this.computeMetricSummary([]);
814
+ const stable = { slope: 0, r2: 0, direction: "stable", confidence: "low" };
815
+ return {
816
+ totalProcesses: { min: 0, max: 0, avg: 0 },
817
+ totalMemory: z,
818
+ byProcessType: {
819
+ browser: z,
820
+ renderer: [],
821
+ gpu: null,
822
+ utility: null
823
+ },
824
+ mainV8Heap: {
825
+ heapUsed: z,
826
+ heapTotal: z,
827
+ external: z,
828
+ arrayBuffers: z
829
+ },
830
+ trends: {
831
+ totalMemory: stable,
832
+ browserMemory: stable,
833
+ rendererMemory: stable
834
+ }
835
+ };
836
+ }
837
+ /** 从快照展平所有标记,并附上该采样点的分类内存(KB) */
838
+ collectEventMarks(snapshots) {
839
+ const out = [];
840
+ for (const s of snapshots) {
841
+ if (!s.marks?.length) continue;
842
+ const browserKB = s.processes.filter((p) => p.type === "Browser").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
843
+ const rendererKB = s.processes.filter((p) => p.type === "Tab" && !p.isMonitorProcess).reduce((sum, p) => sum + p.memory.workingSetSize, 0);
844
+ const gpuKB = s.processes.filter((p) => p.type === "GPU").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
845
+ for (const m of s.marks) {
846
+ out.push({
847
+ timestamp: m.timestamp,
848
+ label: m.label,
849
+ metadata: m.metadata,
850
+ totalWorkingSetKB: s.totalWorkingSetSize,
851
+ browserKB,
852
+ rendererKB,
853
+ gpuKB
854
+ });
855
+ }
856
+ }
857
+ return out;
858
+ }
710
859
  collectEnvironment() {
711
860
  const cpus2 = os2.cpus();
712
861
  return {
@@ -1396,10 +1545,28 @@ var IPCMainHandler = class {
1396
1545
  return this.monitor.startSession(args.label, args.description);
1397
1546
  });
1398
1547
  ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
1399
- return this.monitor.stopSession();
1548
+ try {
1549
+ const report = await this.monitor.stopSession();
1550
+ if (!report) {
1551
+ return { ok: false, reason: "no_active_session" };
1552
+ }
1553
+ return {
1554
+ ok: true,
1555
+ sessionId: report.sessionId,
1556
+ label: report.label,
1557
+ durationMs: report.duration
1558
+ };
1559
+ } catch (err) {
1560
+ console.error("[electron-memory-monitor] SESSION_STOP failed:", err);
1561
+ return {
1562
+ ok: false,
1563
+ reason: "error",
1564
+ message: err instanceof Error ? err.message : String(err)
1565
+ };
1566
+ }
1400
1567
  });
1401
1568
  ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
1402
- return this.monitor.getSessions();
1569
+ return this.monitor.getSessionsPayloadForIpc();
1403
1570
  });
1404
1571
  ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
1405
1572
  return this.monitor.getSessionReport(sessionId);
@@ -1423,7 +1590,7 @@ var IPCMainHandler = class {
1423
1590
  return this.monitor.getConfig();
1424
1591
  });
1425
1592
  ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
1426
- return this.monitor.getSessions();
1593
+ return this.monitor.getSessionsPayloadForIpc();
1427
1594
  });
1428
1595
  ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
1429
1596
  return this.monitor.exportSession(sessionId);
@@ -1434,8 +1601,11 @@ var IPCMainHandler = class {
1434
1601
  ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
1435
1602
  return this.monitor.deleteSession(sessionId);
1436
1603
  });
1437
- ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (_event, detail) => {
1438
- this.monitor.updateRendererDetail(detail);
1604
+ ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (event, detail) => {
1605
+ this.monitor.updateRendererDetail({
1606
+ ...detail,
1607
+ webContentsId: event.sender.id
1608
+ });
1439
1609
  });
1440
1610
  }
1441
1611
  /** 向监控面板推送快照数据 */
@@ -1465,6 +1635,10 @@ var DEFAULT_CONFIG = {
1465
1635
  enabled: true,
1466
1636
  autoStart: true,
1467
1637
  openDashboardOnStart: true,
1638
+ session: {
1639
+ autoStartOnLaunch: true,
1640
+ autoLabelPrefix: "\u81EA\u52A8\u4F1A\u8BDD"
1641
+ },
1468
1642
  collectInterval: 2e3,
1469
1643
  persistInterval: 60,
1470
1644
  enableRendererDetail: false,
@@ -1490,6 +1664,36 @@ var DEFAULT_CONFIG = {
1490
1664
  };
1491
1665
 
1492
1666
  // src/core/monitor.ts
1667
+ function mergeMarksFromExcludedSnapshots(full, sampled) {
1668
+ const marked = full.filter((s) => s.marks && s.marks.length > 0);
1669
+ if (marked.length === 0) return sampled;
1670
+ const sampledRefs = new Set(sampled);
1671
+ const result = sampled.map((s) => ({
1672
+ ...s,
1673
+ marks: s.marks?.length ? [...s.marks] : void 0
1674
+ }));
1675
+ for (const src of marked) {
1676
+ if (sampledRefs.has(src)) continue;
1677
+ let bestIdx = 0;
1678
+ let bestDiff = Infinity;
1679
+ for (let i = 0; i < result.length; i++) {
1680
+ const d = Math.abs(result[i].timestamp - src.timestamp);
1681
+ if (d < bestDiff) {
1682
+ bestDiff = d;
1683
+ bestIdx = i;
1684
+ }
1685
+ }
1686
+ const target = result[bestIdx];
1687
+ target.marks = [...target.marks || [], ...src.marks];
1688
+ }
1689
+ return result;
1690
+ }
1691
+ function formatAutoSessionLabel(prefix) {
1692
+ const d = /* @__PURE__ */ new Date();
1693
+ const p2 = (n) => String(n).padStart(2, "0");
1694
+ const stamp = `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
1695
+ return `${prefix || "\u81EA\u52A8\u4F1A\u8BDD"} ${stamp}`;
1696
+ }
1493
1697
  var ElectronMemoryMonitor = class extends EventEmitter3 {
1494
1698
  constructor(config) {
1495
1699
  super();
@@ -1533,11 +1737,20 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1533
1737
  });
1534
1738
  this.collector.start();
1535
1739
  this.anomalyDetector.start();
1740
+ this.persister.cleanOldSessions();
1741
+ this.sessionManager.reconcileStaleRunningInIndex();
1742
+ this.started = true;
1743
+ if (this.config.session.autoStartOnLaunch) {
1744
+ try {
1745
+ const label = formatAutoSessionLabel(this.config.session.autoLabelPrefix);
1746
+ this.startSession(label, this.config.session.autoDescription);
1747
+ } catch (err) {
1748
+ console.error("[@electron-memory/monitor] autoStartOnLaunch failed:", err);
1749
+ }
1750
+ }
1536
1751
  if (this.config.openDashboardOnStart) {
1537
1752
  this.openDashboard();
1538
1753
  }
1539
- this.persister.cleanOldSessions();
1540
- this.started = true;
1541
1754
  }
1542
1755
  /** 停止监控 */
1543
1756
  async stop() {
@@ -1566,9 +1779,16 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1566
1779
  if (!this.started) {
1567
1780
  throw new Error("Monitor is not started");
1568
1781
  }
1569
- const session = this.sessionManager.startSession(label, description);
1782
+ const anomaliesForReplaced = this.anomalyDetector.getAnomalies();
1783
+ const { session, replaced } = this.sessionManager.startSession(label, description);
1784
+ if (replaced) {
1785
+ void this.persistCompletedSessionReport(replaced, anomaliesForReplaced).catch((err) => {
1786
+ console.error("[@electron-memory/monitor] \u88AB\u9876\u66FF\u4F1A\u8BDD\u7684\u62A5\u544A\u5199\u5165\u5931\u8D25:", err);
1787
+ });
1788
+ }
1570
1789
  this.collector.setSessionId(session.id);
1571
1790
  this.anomalyDetector.clearAnomalies();
1791
+ this.emit("session-start", session);
1572
1792
  return session.id;
1573
1793
  }
1574
1794
  /** 结束当前会话 */
@@ -1579,8 +1799,26 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1579
1799
  const completedSession = this.sessionManager.endSession();
1580
1800
  if (!completedSession) return null;
1581
1801
  this.collector.setSessionId(null);
1582
- const snapshots = this.persister.readSessionSnapshots(completedSession.id);
1583
1802
  const anomalies = this.anomalyDetector.getAnomalies();
1803
+ const report = await this.persistCompletedSessionReport(completedSession, anomalies);
1804
+ return report;
1805
+ }
1806
+ /** 为已落盘元数据的会话生成并写入 report.json(显式结束或被新会话顶替) */
1807
+ async persistCompletedSessionReport(completedSession, anomalies) {
1808
+ const fs2 = await import("fs/promises");
1809
+ const snapshotsPath = path4.join(
1810
+ this.persister.getStorageDir(),
1811
+ completedSession.id,
1812
+ "snapshots.jsonl"
1813
+ );
1814
+ let snapshots = [];
1815
+ try {
1816
+ const content = await fs2.readFile(snapshotsPath, "utf-8");
1817
+ const lines = content.trim().split("\n").filter(Boolean);
1818
+ snapshots = lines.map((line) => JSON.parse(line));
1819
+ } catch {
1820
+ snapshots = this.persister.readSessionSnapshots(completedSession.id);
1821
+ }
1584
1822
  const report = this.analyzer.generateReport(
1585
1823
  completedSession.id,
1586
1824
  completedSession.label,
@@ -1592,8 +1830,7 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1592
1830
  completedSession.dataFile
1593
1831
  );
1594
1832
  const reportPath = path4.join(this.persister.getStorageDir(), completedSession.id, "report.json");
1595
- const fs2 = await import("fs");
1596
- fs2.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
1833
+ await fs2.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
1597
1834
  this.emit("session-end", report);
1598
1835
  return report;
1599
1836
  }
@@ -1618,6 +1855,18 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1618
1855
  async getSessions() {
1619
1856
  return this.sessionManager.getSessions();
1620
1857
  }
1858
+ /**
1859
+ * 供监控面板 IPC:列表来自磁盘索引,是否在录制以内存中的 currentSession 为准(避免索引僵尸 running)
1860
+ */
1861
+ getSessionsPayloadForIpc() {
1862
+ if (!this.started) {
1863
+ return { sessions: [], activeSessionId: null };
1864
+ }
1865
+ return {
1866
+ sessions: this.sessionManager.getSessions(),
1867
+ activeSessionId: this.sessionManager.getCurrentSession()?.id ?? null
1868
+ };
1869
+ }
1621
1870
  /** 获取指定会话报告 */
1622
1871
  async getSessionReport(sessionId) {
1623
1872
  const fs2 = await import("fs");
@@ -1629,7 +1878,6 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1629
1878
  const session = this.sessionManager.getSession(sessionId);
1630
1879
  if (!session || !session.endTime) return null;
1631
1880
  const snapshots = this.persister.readSessionSnapshots(sessionId);
1632
- if (snapshots.length === 0) return null;
1633
1881
  return this.analyzer.generateReport(
1634
1882
  session.id,
1635
1883
  session.label,
@@ -1651,17 +1899,18 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1651
1899
  if (endTime != null) {
1652
1900
  snapshots = snapshots.filter((s) => s.timestamp <= endTime);
1653
1901
  }
1902
+ const beforeDownsample = snapshots;
1654
1903
  const limit = maxPoints ?? 600;
1655
- if (snapshots.length > limit) {
1656
- const step = snapshots.length / limit;
1904
+ if (beforeDownsample.length > limit) {
1905
+ const step = beforeDownsample.length / limit;
1657
1906
  const sampled = [];
1658
1907
  for (let i = 0; i < limit; i++) {
1659
- sampled.push(snapshots[Math.round(i * step)]);
1908
+ sampled.push(beforeDownsample[Math.round(i * step)]);
1660
1909
  }
1661
- if (sampled[sampled.length - 1] !== snapshots[snapshots.length - 1]) {
1662
- sampled[sampled.length - 1] = snapshots[snapshots.length - 1];
1910
+ if (sampled[sampled.length - 1] !== beforeDownsample[beforeDownsample.length - 1]) {
1911
+ sampled[sampled.length - 1] = beforeDownsample[beforeDownsample.length - 1];
1663
1912
  }
1664
- snapshots = sampled;
1913
+ snapshots = mergeMarksFromExcludedSnapshots(beforeDownsample, sampled);
1665
1914
  }
1666
1915
  return snapshots;
1667
1916
  }
@@ -1812,6 +2061,10 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1812
2061
  return {
1813
2062
  ...DEFAULT_CONFIG,
1814
2063
  ...userConfig,
2064
+ session: {
2065
+ ...DEFAULT_CONFIG.session,
2066
+ ...userConfig.session || {}
2067
+ },
1815
2068
  anomaly: {
1816
2069
  ...DEFAULT_CONFIG.anomaly,
1817
2070
  ...userConfig.anomaly || {}