@electron-memory/monitor 0.2.2 → 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.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
@@ -692,10 +722,38 @@ var os2 = __toESM(require("os"));
692
722
  var Analyzer = class {
693
723
  /** 生成会话报告 */
694
724
  generateReport(sessionId, label, description, startTime, endTime, snapshots, anomalies, dataFile) {
725
+ const environment = this.collectEnvironment();
726
+ const eventMarks = this.collectEventMarks(snapshots);
695
727
  if (snapshots.length === 0) {
696
- throw new Error("No snapshots to analyze");
728
+ const summary2 = this.emptySummary();
729
+ const suggestions2 = [
730
+ {
731
+ id: "no-snapshots",
732
+ severity: "info",
733
+ category: "optimization",
734
+ title: "\u4F1A\u8BDD\u5185\u6CA1\u6709\u53EF\u7528\u7684\u5185\u5B58\u5FEB\u7167",
735
+ 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",
736
+ suggestions: [
737
+ "\u4FDD\u6301\u4F1A\u8BDD\u5F00\u542F\u81F3\u5C11\u4E00\u4E2A\u91C7\u96C6\u5468\u671F\u540E\u518D\u70B9\u300C\u7ED3\u675F\u4F1A\u8BDD\u300D",
738
+ "\u5728\u914D\u7F6E\u4E2D\u9002\u5F53\u7F29\u77ED\u91C7\u96C6\u95F4\u9694\u4EE5\u4FBF\u66F4\u5FEB\u5F97\u5230\u6570\u636E"
739
+ ]
740
+ }
741
+ ];
742
+ return {
743
+ sessionId,
744
+ label,
745
+ description,
746
+ startTime,
747
+ endTime,
748
+ duration: endTime - startTime,
749
+ environment,
750
+ summary: summary2,
751
+ anomalies,
752
+ suggestions: suggestions2,
753
+ eventMarks,
754
+ dataFile
755
+ };
697
756
  }
698
- const environment = this.collectEnvironment();
699
757
  const summary = this.computeSummary(snapshots);
700
758
  const suggestions = this.generateSuggestions(snapshots, summary, anomalies);
701
759
  return {
@@ -709,6 +767,7 @@ var Analyzer = class {
709
767
  summary,
710
768
  anomalies,
711
769
  suggestions,
770
+ eventMarks,
712
771
  dataFile
713
772
  };
714
773
  }
@@ -745,6 +804,53 @@ var Analyzer = class {
745
804
  };
746
805
  }
747
806
  // ===== 私有方法 =====
807
+ emptySummary() {
808
+ const z = this.computeMetricSummary([]);
809
+ const stable = { slope: 0, r2: 0, direction: "stable", confidence: "low" };
810
+ return {
811
+ totalProcesses: { min: 0, max: 0, avg: 0 },
812
+ totalMemory: z,
813
+ byProcessType: {
814
+ browser: z,
815
+ renderer: [],
816
+ gpu: null,
817
+ utility: null
818
+ },
819
+ mainV8Heap: {
820
+ heapUsed: z,
821
+ heapTotal: z,
822
+ external: z,
823
+ arrayBuffers: z
824
+ },
825
+ trends: {
826
+ totalMemory: stable,
827
+ browserMemory: stable,
828
+ rendererMemory: stable
829
+ }
830
+ };
831
+ }
832
+ /** 从快照展平所有标记,并附上该采样点的分类内存(KB) */
833
+ collectEventMarks(snapshots) {
834
+ const out = [];
835
+ for (const s of snapshots) {
836
+ if (!s.marks?.length) continue;
837
+ const browserKB = s.processes.filter((p) => p.type === "Browser").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
838
+ const rendererKB = s.processes.filter((p) => p.type === "Tab" && !p.isMonitorProcess).reduce((sum, p) => sum + p.memory.workingSetSize, 0);
839
+ const gpuKB = s.processes.filter((p) => p.type === "GPU").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
840
+ for (const m of s.marks) {
841
+ out.push({
842
+ timestamp: m.timestamp,
843
+ label: m.label,
844
+ metadata: m.metadata,
845
+ totalWorkingSetKB: s.totalWorkingSetSize,
846
+ browserKB,
847
+ rendererKB,
848
+ gpuKB
849
+ });
850
+ }
851
+ }
852
+ return out;
853
+ }
748
854
  collectEnvironment() {
749
855
  const cpus2 = os2.cpus();
750
856
  return {
@@ -1114,6 +1220,7 @@ var path3 = __toESM(require("path"));
1114
1220
  var import_electron2 = require("electron");
1115
1221
  var import_promises = require("fs/promises");
1116
1222
  var path2 = __toESM(require("path"));
1223
+ var import_node_url = require("url");
1117
1224
  var SCHEME = "emm-dashboard";
1118
1225
  var privilegedRegistered = false;
1119
1226
  var handlerRegistered = false;
@@ -1148,6 +1255,48 @@ function isPathInsideRoot(filePath, root) {
1148
1255
  }
1149
1256
  return true;
1150
1257
  }
1258
+ function urlToUiRelativePath(requestUrl) {
1259
+ let u;
1260
+ try {
1261
+ u = new URL(requestUrl);
1262
+ } catch {
1263
+ return "";
1264
+ }
1265
+ let p = "";
1266
+ try {
1267
+ p = decodeURIComponent(u.pathname || "");
1268
+ } catch {
1269
+ p = u.pathname || "";
1270
+ }
1271
+ p = p.replace(/^\/+/, "");
1272
+ const host = (u.hostname || "").toLowerCase();
1273
+ if (p.includes("..")) {
1274
+ return "";
1275
+ }
1276
+ if (host === "electron" || host === "") {
1277
+ return p || "index.html";
1278
+ }
1279
+ if (p) {
1280
+ return `${host}/${p}`.replace(/\\/g, "/");
1281
+ }
1282
+ if (host.includes(".")) {
1283
+ return host;
1284
+ }
1285
+ return "index.html";
1286
+ }
1287
+ function bufferToResponseBody(buf) {
1288
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
1289
+ }
1290
+ function looksLikeHtml(buf) {
1291
+ let i = 0;
1292
+ if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
1293
+ i = 3;
1294
+ }
1295
+ while (i < buf.length && (buf[i] === 32 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13)) {
1296
+ i += 1;
1297
+ }
1298
+ return i < buf.length && buf[i] === 60;
1299
+ }
1151
1300
  function corsHeaders() {
1152
1301
  return {
1153
1302
  "Access-Control-Allow-Origin": "*",
@@ -1189,24 +1338,40 @@ function ensureDashboardProtocolHandler(uiRoot) {
1189
1338
  return new Response(null, { status: 204, headers: corsHeaders() });
1190
1339
  }
1191
1340
  try {
1192
- let pathname;
1193
- try {
1194
- pathname = decodeURIComponent(new URL(request.url).pathname);
1195
- } catch {
1196
- return new Response("Bad URL", { status: 400, headers: corsHeaders() });
1341
+ const rel = urlToUiRelativePath(request.url);
1342
+ if (!rel || rel.includes("..")) {
1343
+ return new Response("Bad path", { status: 400, headers: corsHeaders() });
1197
1344
  }
1198
- let rel = pathname.replace(/^\/+/, "");
1199
- if (!rel) {
1200
- rel = "index.html";
1201
- }
1202
- const filePath = path2.resolve(path2.join(base, rel));
1345
+ const filePath = path2.resolve(path2.join(base, ...rel.split("/")));
1203
1346
  if (!isPathInsideRoot(filePath, base)) {
1204
1347
  return new Response("Forbidden", { status: 403, headers: corsHeaders() });
1205
1348
  }
1206
- const body = await (0, import_promises.readFile)(filePath);
1349
+ const fileUrl = (0, import_node_url.pathToFileURL)(filePath).href;
1350
+ let upstream;
1351
+ try {
1352
+ upstream = await import_electron2.net.fetch(fileUrl);
1353
+ } catch {
1354
+ upstream = new Response(null, { status: 599 });
1355
+ }
1356
+ let buf;
1357
+ if (!upstream.ok) {
1358
+ buf = await (0, import_promises.readFile)(filePath);
1359
+ } else {
1360
+ const ab = await upstream.arrayBuffer();
1361
+ buf = Buffer.from(ab);
1362
+ }
1207
1363
  const ext = path2.extname(filePath).toLowerCase();
1364
+ if (ext === ".html" && !looksLikeHtml(buf)) {
1365
+ console.error(
1366
+ "[@electron-memory/monitor] emm-dashboard: not HTML at",
1367
+ filePath,
1368
+ "url=",
1369
+ request.url
1370
+ );
1371
+ return new Response("Invalid dashboard HTML", { status: 500, headers: corsHeaders() });
1372
+ }
1208
1373
  const mime = MIME_BY_EXT[ext] || "application/octet-stream";
1209
- return new Response(body, {
1374
+ return new Response(bufferToResponseBody(buf), {
1210
1375
  status: 200,
1211
1376
  headers: {
1212
1377
  "Content-Type": mime,
@@ -1216,6 +1381,7 @@ function ensureDashboardProtocolHandler(uiRoot) {
1216
1381
  });
1217
1382
  } catch (err) {
1218
1383
  const msg = err instanceof Error ? err.message : String(err);
1384
+ console.error("[@electron-memory/monitor] emm-dashboard:", request.url, err);
1219
1385
  return new Response(msg, { status: 404, headers: corsHeaders() });
1220
1386
  }
1221
1387
  });
@@ -1374,10 +1540,28 @@ var IPCMainHandler = class {
1374
1540
  return this.monitor.startSession(args.label, args.description);
1375
1541
  });
1376
1542
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
1377
- return this.monitor.stopSession();
1543
+ try {
1544
+ const report = await this.monitor.stopSession();
1545
+ if (!report) {
1546
+ return { ok: false, reason: "no_active_session" };
1547
+ }
1548
+ return {
1549
+ ok: true,
1550
+ sessionId: report.sessionId,
1551
+ label: report.label,
1552
+ durationMs: report.duration
1553
+ };
1554
+ } catch (err) {
1555
+ console.error("[electron-memory-monitor] SESSION_STOP failed:", err);
1556
+ return {
1557
+ ok: false,
1558
+ reason: "error",
1559
+ message: err instanceof Error ? err.message : String(err)
1560
+ };
1561
+ }
1378
1562
  });
1379
1563
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
1380
- return this.monitor.getSessions();
1564
+ return this.monitor.getSessionsPayloadForIpc();
1381
1565
  });
1382
1566
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
1383
1567
  return this.monitor.getSessionReport(sessionId);
@@ -1401,7 +1585,7 @@ var IPCMainHandler = class {
1401
1585
  return this.monitor.getConfig();
1402
1586
  });
1403
1587
  import_electron4.ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
1404
- return this.monitor.getSessions();
1588
+ return this.monitor.getSessionsPayloadForIpc();
1405
1589
  });
1406
1590
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
1407
1591
  return this.monitor.exportSession(sessionId);
@@ -1412,8 +1596,11 @@ var IPCMainHandler = class {
1412
1596
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
1413
1597
  return this.monitor.deleteSession(sessionId);
1414
1598
  });
1415
- import_electron4.ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (_event, detail) => {
1416
- this.monitor.updateRendererDetail(detail);
1599
+ import_electron4.ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (event, detail) => {
1600
+ this.monitor.updateRendererDetail({
1601
+ ...detail,
1602
+ webContentsId: event.sender.id
1603
+ });
1417
1604
  });
1418
1605
  }
1419
1606
  /** 向监控面板推送快照数据 */
@@ -1443,6 +1630,10 @@ var DEFAULT_CONFIG = {
1443
1630
  enabled: true,
1444
1631
  autoStart: true,
1445
1632
  openDashboardOnStart: true,
1633
+ session: {
1634
+ autoStartOnLaunch: true,
1635
+ autoLabelPrefix: "\u81EA\u52A8\u4F1A\u8BDD"
1636
+ },
1446
1637
  collectInterval: 2e3,
1447
1638
  persistInterval: 60,
1448
1639
  enableRendererDetail: false,
@@ -1468,6 +1659,36 @@ var DEFAULT_CONFIG = {
1468
1659
  };
1469
1660
 
1470
1661
  // src/core/monitor.ts
1662
+ function mergeMarksFromExcludedSnapshots(full, sampled) {
1663
+ const marked = full.filter((s) => s.marks && s.marks.length > 0);
1664
+ if (marked.length === 0) return sampled;
1665
+ const sampledRefs = new Set(sampled);
1666
+ const result = sampled.map((s) => ({
1667
+ ...s,
1668
+ marks: s.marks?.length ? [...s.marks] : void 0
1669
+ }));
1670
+ for (const src of marked) {
1671
+ if (sampledRefs.has(src)) continue;
1672
+ let bestIdx = 0;
1673
+ let bestDiff = Infinity;
1674
+ for (let i = 0; i < result.length; i++) {
1675
+ const d = Math.abs(result[i].timestamp - src.timestamp);
1676
+ if (d < bestDiff) {
1677
+ bestDiff = d;
1678
+ bestIdx = i;
1679
+ }
1680
+ }
1681
+ const target = result[bestIdx];
1682
+ target.marks = [...target.marks || [], ...src.marks];
1683
+ }
1684
+ return result;
1685
+ }
1686
+ function formatAutoSessionLabel(prefix) {
1687
+ const d = /* @__PURE__ */ new Date();
1688
+ const p2 = (n) => String(n).padStart(2, "0");
1689
+ const stamp = `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
1690
+ return `${prefix || "\u81EA\u52A8\u4F1A\u8BDD"} ${stamp}`;
1691
+ }
1471
1692
  var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1472
1693
  constructor(config) {
1473
1694
  super();
@@ -1511,11 +1732,20 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1511
1732
  });
1512
1733
  this.collector.start();
1513
1734
  this.anomalyDetector.start();
1735
+ this.persister.cleanOldSessions();
1736
+ this.sessionManager.reconcileStaleRunningInIndex();
1737
+ this.started = true;
1738
+ if (this.config.session.autoStartOnLaunch) {
1739
+ try {
1740
+ const label = formatAutoSessionLabel(this.config.session.autoLabelPrefix);
1741
+ this.startSession(label, this.config.session.autoDescription);
1742
+ } catch (err) {
1743
+ console.error("[@electron-memory/monitor] autoStartOnLaunch failed:", err);
1744
+ }
1745
+ }
1514
1746
  if (this.config.openDashboardOnStart) {
1515
1747
  this.openDashboard();
1516
1748
  }
1517
- this.persister.cleanOldSessions();
1518
- this.started = true;
1519
1749
  }
1520
1750
  /** 停止监控 */
1521
1751
  async stop() {
@@ -1544,9 +1774,16 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1544
1774
  if (!this.started) {
1545
1775
  throw new Error("Monitor is not started");
1546
1776
  }
1547
- const session = this.sessionManager.startSession(label, description);
1777
+ const anomaliesForReplaced = this.anomalyDetector.getAnomalies();
1778
+ const { session, replaced } = this.sessionManager.startSession(label, description);
1779
+ if (replaced) {
1780
+ void this.persistCompletedSessionReport(replaced, anomaliesForReplaced).catch((err) => {
1781
+ console.error("[@electron-memory/monitor] \u88AB\u9876\u66FF\u4F1A\u8BDD\u7684\u62A5\u544A\u5199\u5165\u5931\u8D25:", err);
1782
+ });
1783
+ }
1548
1784
  this.collector.setSessionId(session.id);
1549
1785
  this.anomalyDetector.clearAnomalies();
1786
+ this.emit("session-start", session);
1550
1787
  return session.id;
1551
1788
  }
1552
1789
  /** 结束当前会话 */
@@ -1557,8 +1794,26 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1557
1794
  const completedSession = this.sessionManager.endSession();
1558
1795
  if (!completedSession) return null;
1559
1796
  this.collector.setSessionId(null);
1560
- const snapshots = this.persister.readSessionSnapshots(completedSession.id);
1561
1797
  const anomalies = this.anomalyDetector.getAnomalies();
1798
+ const report = await this.persistCompletedSessionReport(completedSession, anomalies);
1799
+ return report;
1800
+ }
1801
+ /** 为已落盘元数据的会话生成并写入 report.json(显式结束或被新会话顶替) */
1802
+ async persistCompletedSessionReport(completedSession, anomalies) {
1803
+ const fs2 = await import("fs/promises");
1804
+ const snapshotsPath = path4.join(
1805
+ this.persister.getStorageDir(),
1806
+ completedSession.id,
1807
+ "snapshots.jsonl"
1808
+ );
1809
+ let snapshots = [];
1810
+ try {
1811
+ const content = await fs2.readFile(snapshotsPath, "utf-8");
1812
+ const lines = content.trim().split("\n").filter(Boolean);
1813
+ snapshots = lines.map((line) => JSON.parse(line));
1814
+ } catch {
1815
+ snapshots = this.persister.readSessionSnapshots(completedSession.id);
1816
+ }
1562
1817
  const report = this.analyzer.generateReport(
1563
1818
  completedSession.id,
1564
1819
  completedSession.label,
@@ -1570,8 +1825,7 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1570
1825
  completedSession.dataFile
1571
1826
  );
1572
1827
  const reportPath = path4.join(this.persister.getStorageDir(), completedSession.id, "report.json");
1573
- const fs2 = await import("fs");
1574
- fs2.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
1828
+ await fs2.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
1575
1829
  this.emit("session-end", report);
1576
1830
  return report;
1577
1831
  }
@@ -1596,6 +1850,18 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1596
1850
  async getSessions() {
1597
1851
  return this.sessionManager.getSessions();
1598
1852
  }
1853
+ /**
1854
+ * 供监控面板 IPC:列表来自磁盘索引,是否在录制以内存中的 currentSession 为准(避免索引僵尸 running)
1855
+ */
1856
+ getSessionsPayloadForIpc() {
1857
+ if (!this.started) {
1858
+ return { sessions: [], activeSessionId: null };
1859
+ }
1860
+ return {
1861
+ sessions: this.sessionManager.getSessions(),
1862
+ activeSessionId: this.sessionManager.getCurrentSession()?.id ?? null
1863
+ };
1864
+ }
1599
1865
  /** 获取指定会话报告 */
1600
1866
  async getSessionReport(sessionId) {
1601
1867
  const fs2 = await import("fs");
@@ -1607,7 +1873,6 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1607
1873
  const session = this.sessionManager.getSession(sessionId);
1608
1874
  if (!session || !session.endTime) return null;
1609
1875
  const snapshots = this.persister.readSessionSnapshots(sessionId);
1610
- if (snapshots.length === 0) return null;
1611
1876
  return this.analyzer.generateReport(
1612
1877
  session.id,
1613
1878
  session.label,
@@ -1629,17 +1894,18 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1629
1894
  if (endTime != null) {
1630
1895
  snapshots = snapshots.filter((s) => s.timestamp <= endTime);
1631
1896
  }
1897
+ const beforeDownsample = snapshots;
1632
1898
  const limit = maxPoints ?? 600;
1633
- if (snapshots.length > limit) {
1634
- const step = snapshots.length / limit;
1899
+ if (beforeDownsample.length > limit) {
1900
+ const step = beforeDownsample.length / limit;
1635
1901
  const sampled = [];
1636
1902
  for (let i = 0; i < limit; i++) {
1637
- sampled.push(snapshots[Math.round(i * step)]);
1903
+ sampled.push(beforeDownsample[Math.round(i * step)]);
1638
1904
  }
1639
- if (sampled[sampled.length - 1] !== snapshots[snapshots.length - 1]) {
1640
- sampled[sampled.length - 1] = snapshots[snapshots.length - 1];
1905
+ if (sampled[sampled.length - 1] !== beforeDownsample[beforeDownsample.length - 1]) {
1906
+ sampled[sampled.length - 1] = beforeDownsample[beforeDownsample.length - 1];
1641
1907
  }
1642
- snapshots = sampled;
1908
+ snapshots = mergeMarksFromExcludedSnapshots(beforeDownsample, sampled);
1643
1909
  }
1644
1910
  return snapshots;
1645
1911
  }
@@ -1790,6 +2056,10 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1790
2056
  return {
1791
2057
  ...DEFAULT_CONFIG,
1792
2058
  ...userConfig,
2059
+ session: {
2060
+ ...DEFAULT_CONFIG.session,
2061
+ ...userConfig.session || {}
2062
+ },
1793
2063
  anomaly: {
1794
2064
  ...DEFAULT_CONFIG.anomaly,
1795
2065
  ...userConfig.anomaly || {}