@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.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 {
@@ -1073,9 +1179,10 @@ import { BrowserWindow as BrowserWindow2 } from "electron";
1073
1179
  import * as path3 from "path";
1074
1180
 
1075
1181
  // src/core/dashboard-protocol.ts
1076
- import { app as app2, protocol } from "electron";
1182
+ import { app as app2, net, protocol } from "electron";
1077
1183
  import { readFile } from "fs/promises";
1078
1184
  import * as path2 from "path";
1185
+ import { pathToFileURL } from "url";
1079
1186
  var SCHEME = "emm-dashboard";
1080
1187
  var privilegedRegistered = false;
1081
1188
  var handlerRegistered = false;
@@ -1110,6 +1217,48 @@ function isPathInsideRoot(filePath, root) {
1110
1217
  }
1111
1218
  return true;
1112
1219
  }
1220
+ function urlToUiRelativePath(requestUrl) {
1221
+ let u;
1222
+ try {
1223
+ u = new URL(requestUrl);
1224
+ } catch {
1225
+ return "";
1226
+ }
1227
+ let p = "";
1228
+ try {
1229
+ p = decodeURIComponent(u.pathname || "");
1230
+ } catch {
1231
+ p = u.pathname || "";
1232
+ }
1233
+ p = p.replace(/^\/+/, "");
1234
+ const host = (u.hostname || "").toLowerCase();
1235
+ if (p.includes("..")) {
1236
+ return "";
1237
+ }
1238
+ if (host === "electron" || host === "") {
1239
+ return p || "index.html";
1240
+ }
1241
+ if (p) {
1242
+ return `${host}/${p}`.replace(/\\/g, "/");
1243
+ }
1244
+ if (host.includes(".")) {
1245
+ return host;
1246
+ }
1247
+ return "index.html";
1248
+ }
1249
+ function bufferToResponseBody(buf) {
1250
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
1251
+ }
1252
+ function looksLikeHtml(buf) {
1253
+ let i = 0;
1254
+ if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
1255
+ i = 3;
1256
+ }
1257
+ while (i < buf.length && (buf[i] === 32 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13)) {
1258
+ i += 1;
1259
+ }
1260
+ return i < buf.length && buf[i] === 60;
1261
+ }
1113
1262
  function corsHeaders() {
1114
1263
  return {
1115
1264
  "Access-Control-Allow-Origin": "*",
@@ -1151,24 +1300,40 @@ function ensureDashboardProtocolHandler(uiRoot) {
1151
1300
  return new Response(null, { status: 204, headers: corsHeaders() });
1152
1301
  }
1153
1302
  try {
1154
- let pathname;
1155
- try {
1156
- pathname = decodeURIComponent(new URL(request.url).pathname);
1157
- } catch {
1158
- return new Response("Bad URL", { status: 400, headers: corsHeaders() });
1303
+ const rel = urlToUiRelativePath(request.url);
1304
+ if (!rel || rel.includes("..")) {
1305
+ return new Response("Bad path", { status: 400, headers: corsHeaders() });
1159
1306
  }
1160
- let rel = pathname.replace(/^\/+/, "");
1161
- if (!rel) {
1162
- rel = "index.html";
1163
- }
1164
- const filePath = path2.resolve(path2.join(base, rel));
1307
+ const filePath = path2.resolve(path2.join(base, ...rel.split("/")));
1165
1308
  if (!isPathInsideRoot(filePath, base)) {
1166
1309
  return new Response("Forbidden", { status: 403, headers: corsHeaders() });
1167
1310
  }
1168
- const body = await readFile(filePath);
1311
+ const fileUrl = pathToFileURL(filePath).href;
1312
+ let upstream;
1313
+ try {
1314
+ upstream = await net.fetch(fileUrl);
1315
+ } catch {
1316
+ upstream = new Response(null, { status: 599 });
1317
+ }
1318
+ let buf;
1319
+ if (!upstream.ok) {
1320
+ buf = await readFile(filePath);
1321
+ } else {
1322
+ const ab = await upstream.arrayBuffer();
1323
+ buf = Buffer.from(ab);
1324
+ }
1169
1325
  const ext = path2.extname(filePath).toLowerCase();
1326
+ if (ext === ".html" && !looksLikeHtml(buf)) {
1327
+ console.error(
1328
+ "[@electron-memory/monitor] emm-dashboard: not HTML at",
1329
+ filePath,
1330
+ "url=",
1331
+ request.url
1332
+ );
1333
+ return new Response("Invalid dashboard HTML", { status: 500, headers: corsHeaders() });
1334
+ }
1170
1335
  const mime = MIME_BY_EXT[ext] || "application/octet-stream";
1171
- return new Response(body, {
1336
+ return new Response(bufferToResponseBody(buf), {
1172
1337
  status: 200,
1173
1338
  headers: {
1174
1339
  "Content-Type": mime,
@@ -1178,6 +1343,7 @@ function ensureDashboardProtocolHandler(uiRoot) {
1178
1343
  });
1179
1344
  } catch (err) {
1180
1345
  const msg = err instanceof Error ? err.message : String(err);
1346
+ console.error("[@electron-memory/monitor] emm-dashboard:", request.url, err);
1181
1347
  return new Response(msg, { status: 404, headers: corsHeaders() });
1182
1348
  }
1183
1349
  });
@@ -1336,10 +1502,28 @@ var IPCMainHandler = class {
1336
1502
  return this.monitor.startSession(args.label, args.description);
1337
1503
  });
1338
1504
  ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
1339
- 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
+ }
1340
1524
  });
1341
1525
  ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
1342
- return this.monitor.getSessions();
1526
+ return this.monitor.getSessionsPayloadForIpc();
1343
1527
  });
1344
1528
  ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
1345
1529
  return this.monitor.getSessionReport(sessionId);
@@ -1363,7 +1547,7 @@ var IPCMainHandler = class {
1363
1547
  return this.monitor.getConfig();
1364
1548
  });
1365
1549
  ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
1366
- return this.monitor.getSessions();
1550
+ return this.monitor.getSessionsPayloadForIpc();
1367
1551
  });
1368
1552
  ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
1369
1553
  return this.monitor.exportSession(sessionId);
@@ -1374,8 +1558,11 @@ var IPCMainHandler = class {
1374
1558
  ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
1375
1559
  return this.monitor.deleteSession(sessionId);
1376
1560
  });
1377
- ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (_event, detail) => {
1378
- 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
+ });
1379
1566
  });
1380
1567
  }
1381
1568
  /** 向监控面板推送快照数据 */
@@ -1405,6 +1592,10 @@ var DEFAULT_CONFIG = {
1405
1592
  enabled: true,
1406
1593
  autoStart: true,
1407
1594
  openDashboardOnStart: true,
1595
+ session: {
1596
+ autoStartOnLaunch: true,
1597
+ autoLabelPrefix: "\u81EA\u52A8\u4F1A\u8BDD"
1598
+ },
1408
1599
  collectInterval: 2e3,
1409
1600
  persistInterval: 60,
1410
1601
  enableRendererDetail: false,
@@ -1430,6 +1621,36 @@ var DEFAULT_CONFIG = {
1430
1621
  };
1431
1622
 
1432
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
+ }
1433
1654
  var ElectronMemoryMonitor = class extends EventEmitter3 {
1434
1655
  constructor(config) {
1435
1656
  super();
@@ -1473,11 +1694,20 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1473
1694
  });
1474
1695
  this.collector.start();
1475
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
+ }
1476
1708
  if (this.config.openDashboardOnStart) {
1477
1709
  this.openDashboard();
1478
1710
  }
1479
- this.persister.cleanOldSessions();
1480
- this.started = true;
1481
1711
  }
1482
1712
  /** 停止监控 */
1483
1713
  async stop() {
@@ -1506,9 +1736,16 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1506
1736
  if (!this.started) {
1507
1737
  throw new Error("Monitor is not started");
1508
1738
  }
1509
- 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
+ }
1510
1746
  this.collector.setSessionId(session.id);
1511
1747
  this.anomalyDetector.clearAnomalies();
1748
+ this.emit("session-start", session);
1512
1749
  return session.id;
1513
1750
  }
1514
1751
  /** 结束当前会话 */
@@ -1519,8 +1756,26 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1519
1756
  const completedSession = this.sessionManager.endSession();
1520
1757
  if (!completedSession) return null;
1521
1758
  this.collector.setSessionId(null);
1522
- const snapshots = this.persister.readSessionSnapshots(completedSession.id);
1523
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
+ }
1524
1779
  const report = this.analyzer.generateReport(
1525
1780
  completedSession.id,
1526
1781
  completedSession.label,
@@ -1532,8 +1787,7 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1532
1787
  completedSession.dataFile
1533
1788
  );
1534
1789
  const reportPath = path4.join(this.persister.getStorageDir(), completedSession.id, "report.json");
1535
- const fs2 = await import("fs");
1536
- fs2.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
1790
+ await fs2.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
1537
1791
  this.emit("session-end", report);
1538
1792
  return report;
1539
1793
  }
@@ -1558,6 +1812,18 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1558
1812
  async getSessions() {
1559
1813
  return this.sessionManager.getSessions();
1560
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
+ }
1561
1827
  /** 获取指定会话报告 */
1562
1828
  async getSessionReport(sessionId) {
1563
1829
  const fs2 = await import("fs");
@@ -1569,7 +1835,6 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1569
1835
  const session = this.sessionManager.getSession(sessionId);
1570
1836
  if (!session || !session.endTime) return null;
1571
1837
  const snapshots = this.persister.readSessionSnapshots(sessionId);
1572
- if (snapshots.length === 0) return null;
1573
1838
  return this.analyzer.generateReport(
1574
1839
  session.id,
1575
1840
  session.label,
@@ -1591,17 +1856,18 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1591
1856
  if (endTime != null) {
1592
1857
  snapshots = snapshots.filter((s) => s.timestamp <= endTime);
1593
1858
  }
1859
+ const beforeDownsample = snapshots;
1594
1860
  const limit = maxPoints ?? 600;
1595
- if (snapshots.length > limit) {
1596
- const step = snapshots.length / limit;
1861
+ if (beforeDownsample.length > limit) {
1862
+ const step = beforeDownsample.length / limit;
1597
1863
  const sampled = [];
1598
1864
  for (let i = 0; i < limit; i++) {
1599
- sampled.push(snapshots[Math.round(i * step)]);
1865
+ sampled.push(beforeDownsample[Math.round(i * step)]);
1600
1866
  }
1601
- if (sampled[sampled.length - 1] !== snapshots[snapshots.length - 1]) {
1602
- 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];
1603
1869
  }
1604
- snapshots = sampled;
1870
+ snapshots = mergeMarksFromExcludedSnapshots(beforeDownsample, sampled);
1605
1871
  }
1606
1872
  return snapshots;
1607
1873
  }
@@ -1752,6 +2018,10 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
1752
2018
  return {
1753
2019
  ...DEFAULT_CONFIG,
1754
2020
  ...userConfig,
2021
+ session: {
2022
+ ...DEFAULT_CONFIG.session,
2023
+ ...userConfig.session || {}
2024
+ },
1755
2025
  anomaly: {
1756
2026
  ...DEFAULT_CONFIG.anomaly,
1757
2027
  ...userConfig.anomaly || {}