@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.js CHANGED
@@ -299,13 +299,13 @@ var DataPersister = class {
299
299
  return null;
300
300
  }
301
301
  }
302
- /** 获取所有会话列表 */
302
+ /** 获取所有会话列表(按 startTime 降序,最新在前;与索引写入顺序无关) */
303
303
  getSessions() {
304
304
  const indexFile = path.join(this.storageDir, "sessions.json");
305
305
  try {
306
306
  const content = fs.readFileSync(indexFile, "utf-8");
307
307
  const index = JSON.parse(content);
308
- return index.sessions;
308
+ return this.sortSessionsByStartDesc(index.sessions);
309
309
  } catch {
310
310
  return [];
311
311
  }
@@ -408,11 +408,19 @@ var DataPersister = class {
408
408
  saveSessionIndex(sessions) {
409
409
  const indexFile = path.join(this.storageDir, "sessions.json");
410
410
  const index = {
411
- sessions,
411
+ sessions: this.sortSessionsByStartDesc(sessions),
412
412
  lastUpdated: Date.now()
413
413
  };
414
414
  fs.writeFileSync(indexFile, JSON.stringify(index, null, 2), "utf-8");
415
415
  }
416
+ /** 统一排序:startTime 新 → 旧(同毫秒时按 id 稳定排序) */
417
+ sortSessionsByStartDesc(sessions) {
418
+ return [...sessions].sort((a, b) => {
419
+ const t = b.startTime - a.startTime;
420
+ if (t !== 0) return t;
421
+ return String(b.id).localeCompare(String(a.id));
422
+ });
423
+ }
416
424
  ensureDirectory(dir) {
417
425
  if (!fs.existsSync(dir)) {
418
426
  fs.mkdirSync(dir, { recursive: true });
@@ -475,10 +483,14 @@ var SessionManager = class {
475
483
  getCurrentSession() {
476
484
  return this.currentSession;
477
485
  }
478
- /** 开始新会话 */
486
+ /** 开始新会话;若顶替了上一条进行中的会话,通过 `replaced` 返回以便主进程补写 report.json */
479
487
  startSession(label, description) {
488
+ if (!this.currentSession) {
489
+ this.reconcileStaleRunningInIndex();
490
+ }
491
+ let replaced = null;
480
492
  if (this.currentSession && this.currentSession.status === "running") {
481
- this.endSession();
493
+ replaced = this.endSession();
482
494
  }
483
495
  const sessionId = v4();
484
496
  const { dataFile, metaFile } = this.persister.createSessionFiles(sessionId);
@@ -494,7 +506,7 @@ var SessionManager = class {
494
506
  };
495
507
  this.currentSession = session;
496
508
  this.persister.saveSessionMeta(session);
497
- return session;
509
+ return { session, replaced };
498
510
  }
499
511
  /** 结束当前会话 */
500
512
  endSession() {
@@ -522,6 +534,24 @@ var SessionManager = class {
522
534
  getSession(sessionId) {
523
535
  return this.persister.readSessionMeta(sessionId);
524
536
  }
537
+ /**
538
+ * 当前内存无活动会话时,将索引中仍为 running 的条目标为 aborted(进程异常退出后残留)
539
+ */
540
+ reconcileStaleRunningInIndex() {
541
+ if (this.currentSession) return;
542
+ const sessions = this.persister.getSessions();
543
+ const now = Date.now();
544
+ for (const s of sessions) {
545
+ if (s.status !== "running") continue;
546
+ const fixed = {
547
+ ...s,
548
+ status: "aborted",
549
+ endTime: now,
550
+ duration: now - s.startTime
551
+ };
552
+ this.persister.saveSessionMeta(fixed);
553
+ }
554
+ }
525
555
  };
526
556
 
527
557
  // src/core/anomaly.ts
@@ -605,7 +635,18 @@ var AnomalyDetector = class extends import_events2.EventEmitter {
605
635
  title: "\u603B\u5185\u5B58\u6301\u7EED\u589E\u957F",
606
636
  description: `\u5185\u5B58\u4EE5 ${slope.toFixed(2)} KB/s \u7684\u901F\u7387\u6301\u7EED\u589E\u957F (R\xB2=${r2.toFixed(3)})`,
607
637
  value: slope,
608
- threshold: 10
638
+ threshold: 10,
639
+ suggestions: [
640
+ "\u5BFC\u51FA\u5806\u5FEB\u7167 (Heap Snapshot)\uFF0C\u4F7F\u7528 Chrome DevTools \u7684 Memory \u9762\u677F\u52A0\u8F7D\u5206\u6790",
641
+ '\u5BF9\u6BD4\u4E24\u4E2A\u65F6\u95F4\u70B9\u7684\u5806\u5FEB\u7167\uFF0C\u67E5\u627E "Allocated between snapshots" \u4E2D\u65B0\u589E\u7684\u5927\u5BF9\u8C61',
642
+ "\u68C0\u67E5\u4E3B\u8FDB\u7A0B\u4E2D\u662F\u5426\u6709\u672A\u6E05\u7406\u7684 setInterval / setTimeout \u56DE\u8C03",
643
+ "\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",
644
+ "\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"
645
+ ],
646
+ actions: [
647
+ { id: "take-heap-snapshot", label: "\u{1F4F8} \u5BFC\u51FA\u5806\u5FEB\u7167", type: "heap-snapshot" },
648
+ { id: "trigger-gc", label: "\u{1F5D1}\uFE0F \u89E6\u53D1 GC", type: "trigger-gc" }
649
+ ]
609
650
  };
610
651
  }
611
652
  return null;
@@ -630,7 +671,18 @@ var AnomalyDetector = class extends import_events2.EventEmitter {
630
671
  title: "\u5185\u5B58\u7A81\u589E",
631
672
  description: `\u603B\u5185\u5B58\u4ECE ${Math.round(avg)} KB \u7A81\u589E\u5230 ${current} KB (+${((current - avg) / avg * 100).toFixed(1)}%)`,
632
673
  value: current,
633
- threshold: avg * 1.5
674
+ threshold: avg * 1.5,
675
+ suggestions: [
676
+ "\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",
677
+ "\u68C0\u67E5\u662F\u5426\u6709\u5927\u91CF\u65B0\u7A97\u53E3/\u6807\u7B7E\u9875\u540C\u65F6\u521B\u5EFA",
678
+ "\u68C0\u67E5\u662F\u5426\u52A0\u8F7D\u4E86\u5927\u6587\u4EF6\u6216\u5927\u91CF\u56FE\u7247\u8D44\u6E90",
679
+ "\u68C0\u67E5 IPC \u901A\u4FE1\u662F\u5426\u4F20\u8F93\u4E86\u8D85\u5927\u6570\u636E\uFF08\u5EFA\u8BAE\u5206\u7247\u6216\u4F7F\u7528 MessagePort\uFF09",
680
+ "\u89E6\u53D1 GC \u540E\u89C2\u5BDF\u5185\u5B58\u662F\u5426\u56DE\u843D\uFF0C\u5982\u4E0D\u56DE\u843D\u5219\u4E3A\u771F\u5B9E\u6CC4\u6F0F"
681
+ ],
682
+ actions: [
683
+ { id: "take-heap-snapshot", label: "\u{1F4F8} \u5BFC\u51FA\u5806\u5FEB\u7167", type: "heap-snapshot" },
684
+ { id: "trigger-gc", label: "\u{1F5D1}\uFE0F \u89E6\u53D1 GC", type: "trigger-gc" }
685
+ ]
634
686
  };
635
687
  }
636
688
  return null;
@@ -652,7 +704,17 @@ var AnomalyDetector = class extends import_events2.EventEmitter {
652
704
  title: `\u68C0\u6D4B\u5230 ${detached} \u4E2A\u5206\u79BB\u7684 V8 \u4E0A\u4E0B\u6587`,
653
705
  description: "\u5B58\u5728\u672A\u6B63\u786E\u9500\u6BC1\u7684 BrowserWindow \u6216 WebContents\uFF0C\u53EF\u80FD\u5BFC\u81F4\u5185\u5B58\u6CC4\u6F0F",
654
706
  value: detached,
655
- threshold: 0
707
+ threshold: 0,
708
+ suggestions: [
709
+ '\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',
710
+ "\u68C0\u67E5\u6240\u6709 BrowserWindow \u662F\u5426\u5728\u5173\u95ED\u65F6\u8C03\u7528\u4E86 destroy()\uFF08\u800C\u975E\u4EC5 close()\uFF09",
711
+ '\u68C0\u67E5 BrowserWindow.on("closed", ...) \u56DE\u8C03\u4E2D\u662F\u5426\u5C06\u7A97\u53E3\u5F15\u7528\u7F6E\u4E3A null',
712
+ "\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",
713
+ "\u68C0\u67E5 ipcMain.on / ipcMain.handle \u662F\u5426\u5728\u7A97\u53E3\u5173\u95ED\u540E\u6B63\u786E\u79FB\u9664\u76D1\u542C"
714
+ ],
715
+ actions: [
716
+ { id: "take-heap-snapshot", label: "\u{1F4F8} \u5BFC\u51FA\u5806\u5FEB\u7167", type: "heap-snapshot" }
717
+ ]
656
718
  };
657
719
  }
658
720
  return null;
@@ -676,7 +738,18 @@ var AnomalyDetector = class extends import_events2.EventEmitter {
676
738
  title: `V8 \u5806\u4F7F\u7528\u7387 ${(usagePercent * 100).toFixed(1)}%`,
677
739
  description: `\u4E3B\u8FDB\u7A0B V8 \u5806\u4F7F\u7528 ${Math.round(heapUsed / 1024 / 1024)} MB / ${Math.round(heapTotal / 1024 / 1024)} MB`,
678
740
  value: usagePercent * 100,
679
- threshold: 85
741
+ threshold: 85,
742
+ suggestions: [
743
+ "\u5BFC\u51FA\u5806\u5FEB\u7167 (Heap Snapshot)\uFF0C\u4F7F\u7528 Chrome DevTools \u7684 Memory \u9762\u677F\u5206\u6790\u5BF9\u8C61\u7559\u5B58",
744
+ '\u5BF9\u6BD4\u4E24\u4E2A\u65F6\u95F4\u70B9\u7684\u5806\u5FEB\u7167\uFF0C\u67E5\u627E "Allocated between snapshots" \u4E2D\u7684\u6CC4\u6F0F\u5BF9\u8C61',
745
+ "\u68C0\u67E5 Event Listeners \u662F\u5426\u6B63\u786E\u6E05\u7406\uFF08\u7279\u522B\u662F ipcMain / EventEmitter \u4E0A\u7684\u76D1\u542C\u5668\uFF09",
746
+ "\u68C0\u67E5 Promise \u94FE\u662F\u5426\u6709\u672A\u5904\u7406\u7684 rejection \u5BFC\u81F4\u5F15\u7528\u672A\u91CA\u653E",
747
+ "\u89E6\u53D1 GC \u540E\u89C2\u5BDF\u4F7F\u7528\u7387\u662F\u5426\u4E0B\u964D\uFF0C\u82E5\u4E0D\u964D\u5219\u786E\u8BA4\u4E3A\u6CC4\u6F0F"
748
+ ],
749
+ actions: [
750
+ { id: "take-heap-snapshot", label: "\u{1F4F8} \u5BFC\u51FA\u5806\u5FEB\u7167", type: "heap-snapshot" },
751
+ { id: "trigger-gc", label: "\u{1F5D1}\uFE0F \u89E6\u53D1 GC", type: "trigger-gc" }
752
+ ]
680
753
  };
681
754
  }
682
755
  }
@@ -692,10 +765,38 @@ var os2 = __toESM(require("os"));
692
765
  var Analyzer = class {
693
766
  /** 生成会话报告 */
694
767
  generateReport(sessionId, label, description, startTime, endTime, snapshots, anomalies, dataFile) {
768
+ const environment = this.collectEnvironment();
769
+ const eventMarks = this.collectEventMarks(snapshots);
695
770
  if (snapshots.length === 0) {
696
- throw new Error("No snapshots to analyze");
771
+ const summary2 = this.emptySummary();
772
+ const suggestions2 = [
773
+ {
774
+ id: "no-snapshots",
775
+ severity: "info",
776
+ category: "optimization",
777
+ title: "\u4F1A\u8BDD\u5185\u6CA1\u6709\u53EF\u7528\u7684\u5185\u5B58\u5FEB\u7167",
778
+ 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",
779
+ suggestions: [
780
+ "\u4FDD\u6301\u4F1A\u8BDD\u5F00\u542F\u81F3\u5C11\u4E00\u4E2A\u91C7\u96C6\u5468\u671F\u540E\u518D\u70B9\u300C\u7ED3\u675F\u4F1A\u8BDD\u300D",
781
+ "\u5728\u914D\u7F6E\u4E2D\u9002\u5F53\u7F29\u77ED\u91C7\u96C6\u95F4\u9694\u4EE5\u4FBF\u66F4\u5FEB\u5F97\u5230\u6570\u636E"
782
+ ]
783
+ }
784
+ ];
785
+ return {
786
+ sessionId,
787
+ label,
788
+ description,
789
+ startTime,
790
+ endTime,
791
+ duration: endTime - startTime,
792
+ environment,
793
+ summary: summary2,
794
+ anomalies,
795
+ suggestions: suggestions2,
796
+ eventMarks,
797
+ dataFile
798
+ };
697
799
  }
698
- const environment = this.collectEnvironment();
699
800
  const summary = this.computeSummary(snapshots);
700
801
  const suggestions = this.generateSuggestions(snapshots, summary, anomalies);
701
802
  return {
@@ -709,6 +810,7 @@ var Analyzer = class {
709
810
  summary,
710
811
  anomalies,
711
812
  suggestions,
813
+ eventMarks,
712
814
  dataFile
713
815
  };
714
816
  }
@@ -745,6 +847,53 @@ var Analyzer = class {
745
847
  };
746
848
  }
747
849
  // ===== 私有方法 =====
850
+ emptySummary() {
851
+ const z = this.computeMetricSummary([]);
852
+ const stable = { slope: 0, r2: 0, direction: "stable", confidence: "low" };
853
+ return {
854
+ totalProcesses: { min: 0, max: 0, avg: 0 },
855
+ totalMemory: z,
856
+ byProcessType: {
857
+ browser: z,
858
+ renderer: [],
859
+ gpu: null,
860
+ utility: null
861
+ },
862
+ mainV8Heap: {
863
+ heapUsed: z,
864
+ heapTotal: z,
865
+ external: z,
866
+ arrayBuffers: z
867
+ },
868
+ trends: {
869
+ totalMemory: stable,
870
+ browserMemory: stable,
871
+ rendererMemory: stable
872
+ }
873
+ };
874
+ }
875
+ /** 从快照展平所有标记,并附上该采样点的分类内存(KB) */
876
+ collectEventMarks(snapshots) {
877
+ const out = [];
878
+ for (const s of snapshots) {
879
+ if (!s.marks?.length) continue;
880
+ const browserKB = s.processes.filter((p) => p.type === "Browser").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
881
+ const rendererKB = s.processes.filter((p) => p.type === "Tab" && !p.isMonitorProcess).reduce((sum, p) => sum + p.memory.workingSetSize, 0);
882
+ const gpuKB = s.processes.filter((p) => p.type === "GPU").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
883
+ for (const m of s.marks) {
884
+ out.push({
885
+ timestamp: m.timestamp,
886
+ label: m.label,
887
+ metadata: m.metadata,
888
+ totalWorkingSetKB: s.totalWorkingSetSize,
889
+ browserKB,
890
+ rendererKB,
891
+ gpuKB
892
+ });
893
+ }
894
+ }
895
+ return out;
896
+ }
748
897
  collectEnvironment() {
749
898
  const cpus2 = os2.cpus();
750
899
  return {
@@ -1434,10 +1583,28 @@ var IPCMainHandler = class {
1434
1583
  return this.monitor.startSession(args.label, args.description);
1435
1584
  });
1436
1585
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
1437
- return this.monitor.stopSession();
1586
+ try {
1587
+ const report = await this.monitor.stopSession();
1588
+ if (!report) {
1589
+ return { ok: false, reason: "no_active_session" };
1590
+ }
1591
+ return {
1592
+ ok: true,
1593
+ sessionId: report.sessionId,
1594
+ label: report.label,
1595
+ durationMs: report.duration
1596
+ };
1597
+ } catch (err) {
1598
+ console.error("[electron-memory-monitor] SESSION_STOP failed:", err);
1599
+ return {
1600
+ ok: false,
1601
+ reason: "error",
1602
+ message: err instanceof Error ? err.message : String(err)
1603
+ };
1604
+ }
1438
1605
  });
1439
1606
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
1440
- return this.monitor.getSessions();
1607
+ return this.monitor.getSessionsPayloadForIpc();
1441
1608
  });
1442
1609
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
1443
1610
  return this.monitor.getSessionReport(sessionId);
@@ -1461,7 +1628,7 @@ var IPCMainHandler = class {
1461
1628
  return this.monitor.getConfig();
1462
1629
  });
1463
1630
  import_electron4.ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
1464
- return this.monitor.getSessions();
1631
+ return this.monitor.getSessionsPayloadForIpc();
1465
1632
  });
1466
1633
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
1467
1634
  return this.monitor.exportSession(sessionId);
@@ -1472,8 +1639,11 @@ var IPCMainHandler = class {
1472
1639
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
1473
1640
  return this.monitor.deleteSession(sessionId);
1474
1641
  });
1475
- import_electron4.ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (_event, detail) => {
1476
- this.monitor.updateRendererDetail(detail);
1642
+ import_electron4.ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (event, detail) => {
1643
+ this.monitor.updateRendererDetail({
1644
+ ...detail,
1645
+ webContentsId: event.sender.id
1646
+ });
1477
1647
  });
1478
1648
  }
1479
1649
  /** 向监控面板推送快照数据 */
@@ -1503,6 +1673,10 @@ var DEFAULT_CONFIG = {
1503
1673
  enabled: true,
1504
1674
  autoStart: true,
1505
1675
  openDashboardOnStart: true,
1676
+ session: {
1677
+ autoStartOnLaunch: true,
1678
+ autoLabelPrefix: "\u81EA\u52A8\u4F1A\u8BDD"
1679
+ },
1506
1680
  collectInterval: 2e3,
1507
1681
  persistInterval: 60,
1508
1682
  enableRendererDetail: false,
@@ -1528,6 +1702,36 @@ var DEFAULT_CONFIG = {
1528
1702
  };
1529
1703
 
1530
1704
  // src/core/monitor.ts
1705
+ function mergeMarksFromExcludedSnapshots(full, sampled) {
1706
+ const marked = full.filter((s) => s.marks && s.marks.length > 0);
1707
+ if (marked.length === 0) return sampled;
1708
+ const sampledRefs = new Set(sampled);
1709
+ const result = sampled.map((s) => ({
1710
+ ...s,
1711
+ marks: s.marks?.length ? [...s.marks] : void 0
1712
+ }));
1713
+ for (const src of marked) {
1714
+ if (sampledRefs.has(src)) continue;
1715
+ let bestIdx = 0;
1716
+ let bestDiff = Infinity;
1717
+ for (let i = 0; i < result.length; i++) {
1718
+ const d = Math.abs(result[i].timestamp - src.timestamp);
1719
+ if (d < bestDiff) {
1720
+ bestDiff = d;
1721
+ bestIdx = i;
1722
+ }
1723
+ }
1724
+ const target = result[bestIdx];
1725
+ target.marks = [...target.marks || [], ...src.marks];
1726
+ }
1727
+ return result;
1728
+ }
1729
+ function formatAutoSessionLabel(prefix) {
1730
+ const d = /* @__PURE__ */ new Date();
1731
+ const p2 = (n) => String(n).padStart(2, "0");
1732
+ const stamp = `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
1733
+ return `${prefix || "\u81EA\u52A8\u4F1A\u8BDD"} ${stamp}`;
1734
+ }
1531
1735
  var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1532
1736
  constructor(config) {
1533
1737
  super();
@@ -1571,11 +1775,20 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1571
1775
  });
1572
1776
  this.collector.start();
1573
1777
  this.anomalyDetector.start();
1778
+ this.persister.cleanOldSessions();
1779
+ this.sessionManager.reconcileStaleRunningInIndex();
1780
+ this.started = true;
1781
+ if (this.config.session.autoStartOnLaunch) {
1782
+ try {
1783
+ const label = formatAutoSessionLabel(this.config.session.autoLabelPrefix);
1784
+ this.startSession(label, this.config.session.autoDescription);
1785
+ } catch (err) {
1786
+ console.error("[@electron-memory/monitor] autoStartOnLaunch failed:", err);
1787
+ }
1788
+ }
1574
1789
  if (this.config.openDashboardOnStart) {
1575
1790
  this.openDashboard();
1576
1791
  }
1577
- this.persister.cleanOldSessions();
1578
- this.started = true;
1579
1792
  }
1580
1793
  /** 停止监控 */
1581
1794
  async stop() {
@@ -1604,9 +1817,16 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1604
1817
  if (!this.started) {
1605
1818
  throw new Error("Monitor is not started");
1606
1819
  }
1607
- const session = this.sessionManager.startSession(label, description);
1820
+ const anomaliesForReplaced = this.anomalyDetector.getAnomalies();
1821
+ const { session, replaced } = this.sessionManager.startSession(label, description);
1822
+ if (replaced) {
1823
+ void this.persistCompletedSessionReport(replaced, anomaliesForReplaced).catch((err) => {
1824
+ console.error("[@electron-memory/monitor] \u88AB\u9876\u66FF\u4F1A\u8BDD\u7684\u62A5\u544A\u5199\u5165\u5931\u8D25:", err);
1825
+ });
1826
+ }
1608
1827
  this.collector.setSessionId(session.id);
1609
1828
  this.anomalyDetector.clearAnomalies();
1829
+ this.emit("session-start", session);
1610
1830
  return session.id;
1611
1831
  }
1612
1832
  /** 结束当前会话 */
@@ -1617,8 +1837,26 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1617
1837
  const completedSession = this.sessionManager.endSession();
1618
1838
  if (!completedSession) return null;
1619
1839
  this.collector.setSessionId(null);
1620
- const snapshots = this.persister.readSessionSnapshots(completedSession.id);
1621
1840
  const anomalies = this.anomalyDetector.getAnomalies();
1841
+ const report = await this.persistCompletedSessionReport(completedSession, anomalies);
1842
+ return report;
1843
+ }
1844
+ /** 为已落盘元数据的会话生成并写入 report.json(显式结束或被新会话顶替) */
1845
+ async persistCompletedSessionReport(completedSession, anomalies) {
1846
+ const fs2 = await import("fs/promises");
1847
+ const snapshotsPath = path4.join(
1848
+ this.persister.getStorageDir(),
1849
+ completedSession.id,
1850
+ "snapshots.jsonl"
1851
+ );
1852
+ let snapshots = [];
1853
+ try {
1854
+ const content = await fs2.readFile(snapshotsPath, "utf-8");
1855
+ const lines = content.trim().split("\n").filter(Boolean);
1856
+ snapshots = lines.map((line) => JSON.parse(line));
1857
+ } catch {
1858
+ snapshots = this.persister.readSessionSnapshots(completedSession.id);
1859
+ }
1622
1860
  const report = this.analyzer.generateReport(
1623
1861
  completedSession.id,
1624
1862
  completedSession.label,
@@ -1630,8 +1868,7 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1630
1868
  completedSession.dataFile
1631
1869
  );
1632
1870
  const reportPath = path4.join(this.persister.getStorageDir(), completedSession.id, "report.json");
1633
- const fs2 = await import("fs");
1634
- fs2.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
1871
+ await fs2.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
1635
1872
  this.emit("session-end", report);
1636
1873
  return report;
1637
1874
  }
@@ -1656,6 +1893,18 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1656
1893
  async getSessions() {
1657
1894
  return this.sessionManager.getSessions();
1658
1895
  }
1896
+ /**
1897
+ * 供监控面板 IPC:列表来自磁盘索引,是否在录制以内存中的 currentSession 为准(避免索引僵尸 running)
1898
+ */
1899
+ getSessionsPayloadForIpc() {
1900
+ if (!this.started) {
1901
+ return { sessions: [], activeSessionId: null };
1902
+ }
1903
+ return {
1904
+ sessions: this.sessionManager.getSessions(),
1905
+ activeSessionId: this.sessionManager.getCurrentSession()?.id ?? null
1906
+ };
1907
+ }
1659
1908
  /** 获取指定会话报告 */
1660
1909
  async getSessionReport(sessionId) {
1661
1910
  const fs2 = await import("fs");
@@ -1667,7 +1916,6 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1667
1916
  const session = this.sessionManager.getSession(sessionId);
1668
1917
  if (!session || !session.endTime) return null;
1669
1918
  const snapshots = this.persister.readSessionSnapshots(sessionId);
1670
- if (snapshots.length === 0) return null;
1671
1919
  return this.analyzer.generateReport(
1672
1920
  session.id,
1673
1921
  session.label,
@@ -1689,17 +1937,18 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1689
1937
  if (endTime != null) {
1690
1938
  snapshots = snapshots.filter((s) => s.timestamp <= endTime);
1691
1939
  }
1940
+ const beforeDownsample = snapshots;
1692
1941
  const limit = maxPoints ?? 600;
1693
- if (snapshots.length > limit) {
1694
- const step = snapshots.length / limit;
1942
+ if (beforeDownsample.length > limit) {
1943
+ const step = beforeDownsample.length / limit;
1695
1944
  const sampled = [];
1696
1945
  for (let i = 0; i < limit; i++) {
1697
- sampled.push(snapshots[Math.round(i * step)]);
1946
+ sampled.push(beforeDownsample[Math.round(i * step)]);
1698
1947
  }
1699
- if (sampled[sampled.length - 1] !== snapshots[snapshots.length - 1]) {
1700
- sampled[sampled.length - 1] = snapshots[snapshots.length - 1];
1948
+ if (sampled[sampled.length - 1] !== beforeDownsample[beforeDownsample.length - 1]) {
1949
+ sampled[sampled.length - 1] = beforeDownsample[beforeDownsample.length - 1];
1701
1950
  }
1702
- snapshots = sampled;
1951
+ snapshots = mergeMarksFromExcludedSnapshots(beforeDownsample, sampled);
1703
1952
  }
1704
1953
  return snapshots;
1705
1954
  }
@@ -1850,6 +2099,10 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1850
2099
  return {
1851
2100
  ...DEFAULT_CONFIG,
1852
2101
  ...userConfig,
2102
+ session: {
2103
+ ...DEFAULT_CONFIG.session,
2104
+ ...userConfig.session || {}
2105
+ },
1853
2106
  anomaly: {
1854
2107
  ...DEFAULT_CONFIG.anomaly,
1855
2108
  ...userConfig.anomaly || {}