@electron-memory/monitor 0.2.3 → 0.2.5

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
@@ -654,10 +684,38 @@ import * as os2 from "os";
654
684
  var Analyzer = class {
655
685
  /** 生成会话报告 */
656
686
  generateReport(sessionId, label, description, startTime, endTime, snapshots, anomalies, dataFile) {
687
+ const environment = this.collectEnvironment();
688
+ const eventMarks = this.collectEventMarks(snapshots);
657
689
  if (snapshots.length === 0) {
658
- throw new Error("No snapshots to analyze");
690
+ const summary2 = this.emptySummary();
691
+ const suggestions2 = [
692
+ {
693
+ id: "no-snapshots",
694
+ severity: "info",
695
+ category: "optimization",
696
+ title: "\u4F1A\u8BDD\u5185\u6CA1\u6709\u53EF\u7528\u7684\u5185\u5B58\u5FEB\u7167",
697
+ 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",
698
+ suggestions: [
699
+ "\u4FDD\u6301\u4F1A\u8BDD\u5F00\u542F\u81F3\u5C11\u4E00\u4E2A\u91C7\u96C6\u5468\u671F\u540E\u518D\u70B9\u300C\u7ED3\u675F\u4F1A\u8BDD\u300D",
700
+ "\u5728\u914D\u7F6E\u4E2D\u9002\u5F53\u7F29\u77ED\u91C7\u96C6\u95F4\u9694\u4EE5\u4FBF\u66F4\u5FEB\u5F97\u5230\u6570\u636E"
701
+ ]
702
+ }
703
+ ];
704
+ return {
705
+ sessionId,
706
+ label,
707
+ description,
708
+ startTime,
709
+ endTime,
710
+ duration: endTime - startTime,
711
+ environment,
712
+ summary: summary2,
713
+ anomalies,
714
+ suggestions: suggestions2,
715
+ eventMarks,
716
+ dataFile
717
+ };
659
718
  }
660
- const environment = this.collectEnvironment();
661
719
  const summary = this.computeSummary(snapshots);
662
720
  const suggestions = this.generateSuggestions(snapshots, summary, anomalies);
663
721
  return {
@@ -671,6 +729,7 @@ var Analyzer = class {
671
729
  summary,
672
730
  anomalies,
673
731
  suggestions,
732
+ eventMarks,
674
733
  dataFile
675
734
  };
676
735
  }
@@ -707,6 +766,53 @@ var Analyzer = class {
707
766
  };
708
767
  }
709
768
  // ===== 私有方法 =====
769
+ emptySummary() {
770
+ const z = this.computeMetricSummary([]);
771
+ const stable = { slope: 0, r2: 0, direction: "stable", confidence: "low" };
772
+ return {
773
+ totalProcesses: { min: 0, max: 0, avg: 0 },
774
+ totalMemory: z,
775
+ byProcessType: {
776
+ browser: z,
777
+ renderer: [],
778
+ gpu: null,
779
+ utility: null
780
+ },
781
+ mainV8Heap: {
782
+ heapUsed: z,
783
+ heapTotal: z,
784
+ external: z,
785
+ arrayBuffers: z
786
+ },
787
+ trends: {
788
+ totalMemory: stable,
789
+ browserMemory: stable,
790
+ rendererMemory: stable
791
+ }
792
+ };
793
+ }
794
+ /** 从快照展平所有标记,并附上该采样点的分类内存(KB) */
795
+ collectEventMarks(snapshots) {
796
+ const out = [];
797
+ for (const s of snapshots) {
798
+ if (!s.marks?.length) continue;
799
+ const browserKB = s.processes.filter((p) => p.type === "Browser").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
800
+ const rendererKB = s.processes.filter((p) => p.type === "Tab" && !p.isMonitorProcess).reduce((sum, p) => sum + p.memory.workingSetSize, 0);
801
+ const gpuKB = s.processes.filter((p) => p.type === "GPU").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
802
+ for (const m of s.marks) {
803
+ out.push({
804
+ timestamp: m.timestamp,
805
+ label: m.label,
806
+ metadata: m.metadata,
807
+ totalWorkingSetKB: s.totalWorkingSetSize,
808
+ browserKB,
809
+ rendererKB,
810
+ gpuKB
811
+ });
812
+ }
813
+ }
814
+ return out;
815
+ }
710
816
  collectEnvironment() {
711
817
  const cpus2 = os2.cpus();
712
818
  return {
@@ -1396,10 +1502,28 @@ var IPCMainHandler = class {
1396
1502
  return this.monitor.startSession(args.label, args.description);
1397
1503
  });
1398
1504
  ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
1399
- return this.monitor.stopSession();
1505
+ try {
1506
+ const report = await this.monitor.stopSession();
1507
+ if (!report) {
1508
+ return { ok: false, reason: "no_active_session" };
1509
+ }
1510
+ return {
1511
+ ok: true,
1512
+ sessionId: report.sessionId,
1513
+ label: report.label,
1514
+ durationMs: report.duration
1515
+ };
1516
+ } catch (err) {
1517
+ console.error("[electron-memory-monitor] SESSION_STOP failed:", err);
1518
+ return {
1519
+ ok: false,
1520
+ reason: "error",
1521
+ message: err instanceof Error ? err.message : String(err)
1522
+ };
1523
+ }
1400
1524
  });
1401
1525
  ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
1402
- return this.monitor.getSessions();
1526
+ return this.monitor.getSessionsPayloadForIpc();
1403
1527
  });
1404
1528
  ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
1405
1529
  return this.monitor.getSessionReport(sessionId);
@@ -1423,7 +1547,7 @@ var IPCMainHandler = class {
1423
1547
  return this.monitor.getConfig();
1424
1548
  });
1425
1549
  ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
1426
- return this.monitor.getSessions();
1550
+ return this.monitor.getSessionsPayloadForIpc();
1427
1551
  });
1428
1552
  ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
1429
1553
  return this.monitor.exportSession(sessionId);
@@ -1434,8 +1558,11 @@ var IPCMainHandler = class {
1434
1558
  ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
1435
1559
  return this.monitor.deleteSession(sessionId);
1436
1560
  });
1437
- ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (_event, detail) => {
1438
- this.monitor.updateRendererDetail(detail);
1561
+ ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (event, detail) => {
1562
+ this.monitor.updateRendererDetail({
1563
+ ...detail,
1564
+ webContentsId: event.sender.id
1565
+ });
1439
1566
  });
1440
1567
  }
1441
1568
  /** 向监控面板推送快照数据 */
@@ -1465,6 +1592,10 @@ var DEFAULT_CONFIG = {
1465
1592
  enabled: true,
1466
1593
  autoStart: true,
1467
1594
  openDashboardOnStart: true,
1595
+ session: {
1596
+ autoStartOnLaunch: true,
1597
+ autoLabelPrefix: "\u81EA\u52A8\u4F1A\u8BDD"
1598
+ },
1468
1599
  collectInterval: 2e3,
1469
1600
  persistInterval: 60,
1470
1601
  enableRendererDetail: false,
@@ -1490,6 +1621,36 @@ var DEFAULT_CONFIG = {
1490
1621
  };
1491
1622
 
1492
1623
  // src/core/monitor.ts
1624
+ function mergeMarksFromExcludedSnapshots(full, sampled) {
1625
+ const marked = full.filter((s) => s.marks && s.marks.length > 0);
1626
+ if (marked.length === 0) return sampled;
1627
+ const sampledRefs = new Set(sampled);
1628
+ const result = sampled.map((s) => ({
1629
+ ...s,
1630
+ marks: s.marks?.length ? [...s.marks] : void 0
1631
+ }));
1632
+ for (const src of marked) {
1633
+ if (sampledRefs.has(src)) continue;
1634
+ let bestIdx = 0;
1635
+ let bestDiff = Infinity;
1636
+ for (let i = 0; i < result.length; i++) {
1637
+ const d = Math.abs(result[i].timestamp - src.timestamp);
1638
+ if (d < bestDiff) {
1639
+ bestDiff = d;
1640
+ bestIdx = i;
1641
+ }
1642
+ }
1643
+ const target = result[bestIdx];
1644
+ target.marks = [...target.marks || [], ...src.marks];
1645
+ }
1646
+ return result;
1647
+ }
1648
+ function formatAutoSessionLabel(prefix) {
1649
+ const d = /* @__PURE__ */ new Date();
1650
+ const p2 = (n) => String(n).padStart(2, "0");
1651
+ const stamp = `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
1652
+ return `${prefix || "\u81EA\u52A8\u4F1A\u8BDD"} ${stamp}`;
1653
+ }
1493
1654
  var ElectronMemoryMonitor = class extends EventEmitter3 {
1494
1655
  constructor(config) {
1495
1656
  super();
@@ -1533,11 +1694,20 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1533
1694
  });
1534
1695
  this.collector.start();
1535
1696
  this.anomalyDetector.start();
1697
+ this.persister.cleanOldSessions();
1698
+ this.sessionManager.reconcileStaleRunningInIndex();
1699
+ this.started = true;
1700
+ if (this.config.session.autoStartOnLaunch) {
1701
+ try {
1702
+ const label = formatAutoSessionLabel(this.config.session.autoLabelPrefix);
1703
+ this.startSession(label, this.config.session.autoDescription);
1704
+ } catch (err) {
1705
+ console.error("[@electron-memory/monitor] autoStartOnLaunch failed:", err);
1706
+ }
1707
+ }
1536
1708
  if (this.config.openDashboardOnStart) {
1537
1709
  this.openDashboard();
1538
1710
  }
1539
- this.persister.cleanOldSessions();
1540
- this.started = true;
1541
1711
  }
1542
1712
  /** 停止监控 */
1543
1713
  async stop() {
@@ -1566,9 +1736,16 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1566
1736
  if (!this.started) {
1567
1737
  throw new Error("Monitor is not started");
1568
1738
  }
1569
- const session = this.sessionManager.startSession(label, description);
1739
+ const anomaliesForReplaced = this.anomalyDetector.getAnomalies();
1740
+ const { session, replaced } = this.sessionManager.startSession(label, description);
1741
+ if (replaced) {
1742
+ void this.persistCompletedSessionReport(replaced, anomaliesForReplaced).catch((err) => {
1743
+ console.error("[@electron-memory/monitor] \u88AB\u9876\u66FF\u4F1A\u8BDD\u7684\u62A5\u544A\u5199\u5165\u5931\u8D25:", err);
1744
+ });
1745
+ }
1570
1746
  this.collector.setSessionId(session.id);
1571
1747
  this.anomalyDetector.clearAnomalies();
1748
+ this.emit("session-start", session);
1572
1749
  return session.id;
1573
1750
  }
1574
1751
  /** 结束当前会话 */
@@ -1579,8 +1756,26 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1579
1756
  const completedSession = this.sessionManager.endSession();
1580
1757
  if (!completedSession) return null;
1581
1758
  this.collector.setSessionId(null);
1582
- const snapshots = this.persister.readSessionSnapshots(completedSession.id);
1583
1759
  const anomalies = this.anomalyDetector.getAnomalies();
1760
+ const report = await this.persistCompletedSessionReport(completedSession, anomalies);
1761
+ return report;
1762
+ }
1763
+ /** 为已落盘元数据的会话生成并写入 report.json(显式结束或被新会话顶替) */
1764
+ async persistCompletedSessionReport(completedSession, anomalies) {
1765
+ const fs2 = await import("fs/promises");
1766
+ const snapshotsPath = path4.join(
1767
+ this.persister.getStorageDir(),
1768
+ completedSession.id,
1769
+ "snapshots.jsonl"
1770
+ );
1771
+ let snapshots = [];
1772
+ try {
1773
+ const content = await fs2.readFile(snapshotsPath, "utf-8");
1774
+ const lines = content.trim().split("\n").filter(Boolean);
1775
+ snapshots = lines.map((line) => JSON.parse(line));
1776
+ } catch {
1777
+ snapshots = this.persister.readSessionSnapshots(completedSession.id);
1778
+ }
1584
1779
  const report = this.analyzer.generateReport(
1585
1780
  completedSession.id,
1586
1781
  completedSession.label,
@@ -1592,8 +1787,7 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1592
1787
  completedSession.dataFile
1593
1788
  );
1594
1789
  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");
1790
+ await fs2.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
1597
1791
  this.emit("session-end", report);
1598
1792
  return report;
1599
1793
  }
@@ -1618,6 +1812,18 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1618
1812
  async getSessions() {
1619
1813
  return this.sessionManager.getSessions();
1620
1814
  }
1815
+ /**
1816
+ * 供监控面板 IPC:列表来自磁盘索引,是否在录制以内存中的 currentSession 为准(避免索引僵尸 running)
1817
+ */
1818
+ getSessionsPayloadForIpc() {
1819
+ if (!this.started) {
1820
+ return { sessions: [], activeSessionId: null };
1821
+ }
1822
+ return {
1823
+ sessions: this.sessionManager.getSessions(),
1824
+ activeSessionId: this.sessionManager.getCurrentSession()?.id ?? null
1825
+ };
1826
+ }
1621
1827
  /** 获取指定会话报告 */
1622
1828
  async getSessionReport(sessionId) {
1623
1829
  const fs2 = await import("fs");
@@ -1629,7 +1835,6 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1629
1835
  const session = this.sessionManager.getSession(sessionId);
1630
1836
  if (!session || !session.endTime) return null;
1631
1837
  const snapshots = this.persister.readSessionSnapshots(sessionId);
1632
- if (snapshots.length === 0) return null;
1633
1838
  return this.analyzer.generateReport(
1634
1839
  session.id,
1635
1840
  session.label,
@@ -1651,17 +1856,18 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1651
1856
  if (endTime != null) {
1652
1857
  snapshots = snapshots.filter((s) => s.timestamp <= endTime);
1653
1858
  }
1859
+ const beforeDownsample = snapshots;
1654
1860
  const limit = maxPoints ?? 600;
1655
- if (snapshots.length > limit) {
1656
- const step = snapshots.length / limit;
1861
+ if (beforeDownsample.length > limit) {
1862
+ const step = beforeDownsample.length / limit;
1657
1863
  const sampled = [];
1658
1864
  for (let i = 0; i < limit; i++) {
1659
- sampled.push(snapshots[Math.round(i * step)]);
1865
+ sampled.push(beforeDownsample[Math.round(i * step)]);
1660
1866
  }
1661
- if (sampled[sampled.length - 1] !== snapshots[snapshots.length - 1]) {
1662
- sampled[sampled.length - 1] = snapshots[snapshots.length - 1];
1867
+ if (sampled[sampled.length - 1] !== beforeDownsample[beforeDownsample.length - 1]) {
1868
+ sampled[sampled.length - 1] = beforeDownsample[beforeDownsample.length - 1];
1663
1869
  }
1664
- snapshots = sampled;
1870
+ snapshots = mergeMarksFromExcludedSnapshots(beforeDownsample, sampled);
1665
1871
  }
1666
1872
  return snapshots;
1667
1873
  }
@@ -1812,6 +2018,10 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1812
2018
  return {
1813
2019
  ...DEFAULT_CONFIG,
1814
2020
  ...userConfig,
2021
+ session: {
2022
+ ...DEFAULT_CONFIG.session,
2023
+ ...userConfig.session || {}
2024
+ },
1815
2025
  anomaly: {
1816
2026
  ...DEFAULT_CONFIG.anomaly,
1817
2027
  ...userConfig.anomaly || {}