@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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ipc/preload-api.ts","../src/ipc/channels.ts","../src/core/dashboard-preload.ts"],"sourcesContent":["/**\r\n * Preload API 辅助\r\n * \r\n * 定义暴露给监控面板渲染进程的 API\r\n */\r\n\r\nimport { contextBridge, ipcRenderer } from 'electron'\r\nimport { IPC_CHANNELS } from './channels'\r\n\r\n/** 监控面板 preload 注入的 API 类型 */\r\nexport interface MonitorPanelAPI {\r\n // 会话控制\r\n startSession: (label: string, description?: string) => Promise<string>\r\n stopSession: () => Promise<unknown>\r\n getSessions: () => Promise<unknown[]>\r\n getSessionReport: (sessionId: string) => Promise<unknown>\r\n compareSessions: (baseId: string, targetId: string) => Promise<unknown>\r\n\r\n // 数据查询\r\n getSessionSnapshots: (sessionId: string, startTime?: number, endTime?: number, maxPoints?: number) => Promise<unknown[]>\r\n\r\n // 导入导出\r\n exportSession: (sessionId: string) => Promise<{ success: boolean; filePath?: string; error?: string }>\r\n importSession: () => Promise<{ success: boolean; session?: unknown; error?: string }>\r\n deleteSession: (sessionId: string) => Promise<boolean>\r\n\r\n // 工具\r\n triggerGC: () => Promise<unknown>\r\n takeHeapSnapshot: (filePath?: string) => Promise<string>\r\n addMark: (label: string, metadata?: Record<string, unknown>) => Promise<void>\r\n getConfig: () => Promise<unknown>\r\n\r\n // 数据订阅\r\n onSnapshot: (callback: (data: unknown) => void) => void\r\n onAnomaly: (callback: (data: unknown) => void) => void\r\n\r\n // 移除监听器\r\n removeSnapshotListener: () => void\r\n removeAnomalyListener: () => void\r\n}\r\n\r\n/** 在监控面板的 preload 中注入 API */\r\nexport function injectMonitorPanelAPI(): void {\r\n const api: MonitorPanelAPI = {\r\n // 会话控制\r\n startSession: (label: string, description?: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_START, { label, description }),\r\n stopSession: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_STOP),\r\n getSessions: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.GET_SESSIONS),\r\n getSessionReport: (sessionId: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_REPORT, sessionId),\r\n compareSessions: (baseId: string, targetId: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_COMPARE, { baseId, targetId }),\r\n\r\n // 数据查询\r\n getSessionSnapshots: (sessionId: string, startTime?: number, endTime?: number, maxPoints?: number) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_SNAPSHOTS, { sessionId, startTime, endTime, maxPoints }),\r\n\r\n // 导入导出\r\n exportSession: (sessionId: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_EXPORT, sessionId),\r\n importSession: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_IMPORT),\r\n deleteSession: (sessionId: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_DELETE, sessionId),\r\n\r\n // 工具\r\n triggerGC: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.TRIGGER_GC),\r\n takeHeapSnapshot: (filePath?: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.HEAP_SNAPSHOT, filePath),\r\n addMark: (label: string, metadata?: Record<string, unknown>) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.MARK, { label, metadata }),\r\n getConfig: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.GET_CONFIG),\r\n\r\n // 数据订阅\r\n onSnapshot: (callback: (data: unknown) => void) => {\r\n ipcRenderer.on(IPC_CHANNELS.SNAPSHOT, (_event, data) => callback(data))\r\n },\r\n onAnomaly: (callback: (data: unknown) => void) => {\r\n ipcRenderer.on(IPC_CHANNELS.ANOMALY, (_event, data) => callback(data))\r\n },\r\n\r\n // 移除监听器\r\n removeSnapshotListener: () => {\r\n ipcRenderer.removeAllListeners(IPC_CHANNELS.SNAPSHOT)\r\n },\r\n removeAnomalyListener: () => {\r\n ipcRenderer.removeAllListeners(IPC_CHANNELS.ANOMALY)\r\n },\r\n }\r\n\r\n contextBridge.exposeInMainWorld('monitorAPI', api)\r\n}\r\n","/**\r\n * IPC 通道常量定义\r\n * 所有通道以 'emm:' 为前缀,避免与业务 IPC 冲突\r\n */\r\n\r\nexport const IPC_CHANNELS = {\r\n // === 数据推送(主进程 → 监控面板)===\r\n SNAPSHOT: 'emm:snapshot',\r\n ANOMALY: 'emm:anomaly',\r\n\r\n // === 会话控制(面板 → 主进程)===\r\n SESSION_START: 'emm:session:start',\r\n SESSION_STOP: 'emm:session:stop',\r\n SESSION_LIST: 'emm:session:list',\r\n SESSION_REPORT: 'emm:session:report',\r\n SESSION_COMPARE: 'emm:session:compare',\r\n\r\n // === 数据查询(面板 → 主进程)===\r\n SESSION_SNAPSHOTS: 'emm:session:snapshots',\r\n\r\n // === 工具操作(面板 → 主进程)===\r\n TRIGGER_GC: 'emm:gc',\r\n HEAP_SNAPSHOT: 'emm:heap-snapshot',\r\n MARK: 'emm:mark',\r\n CONFIG_UPDATE: 'emm:config:update',\r\n GET_CONFIG: 'emm:config:get',\r\n GET_SESSIONS: 'emm:sessions:get',\r\n\r\n // === 导入导出(面板 → 主进程)===\r\n SESSION_EXPORT: 'emm:session:export',\r\n SESSION_IMPORT: 'emm:session:import',\r\n SESSION_DELETE: 'emm:session:delete',\r\n\r\n // === 渲染进程上报(可选)===\r\n RENDERER_REPORT: 'emm:renderer:report',\r\n RENDERER_REQUEST: 'emm:renderer:request',\r\n} as const\r\n","/**\r\n * Dashboard Preload 脚本\r\n * 在监控面板 BrowserWindow 中使用\r\n */\r\n\r\nimport { injectMonitorPanelAPI } from '../ipc/preload-api'\r\n\r\ninjectMonitorPanelAPI()\r\n"],"mappings":";;;AAMA,sBAA2C;;;ACDpC,IAAM,eAAe;AAAA;AAAA,EAE1B,UAAU;AAAA,EACV,SAAS;AAAA;AAAA,EAGT,eAAe;AAAA,EACf,cAAc;AAAA,EACd,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,iBAAiB;AAAA;AAAA,EAGjB,mBAAmB;AAAA;AAAA,EAGnB,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,MAAM;AAAA,EACN,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,cAAc;AAAA;AAAA,EAGd,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,gBAAgB;AAAA;AAAA,EAGhB,iBAAiB;AAAA,EACjB,kBAAkB;AACpB;;;ADMO,SAAS,wBAA8B;AAC5C,QAAM,MAAuB;AAAA;AAAA,IAE3B,cAAc,CAAC,OAAe,gBAC5B,4BAAY,OAAO,aAAa,eAAe,EAAE,OAAO,YAAY,CAAC;AAAA,IACvE,aAAa,MACX,4BAAY,OAAO,aAAa,YAAY;AAAA,IAC9C,aAAa,MACX,4BAAY,OAAO,aAAa,YAAY;AAAA,IAC9C,kBAAkB,CAAC,cACjB,4BAAY,OAAO,aAAa,gBAAgB,SAAS;AAAA,IAC3D,iBAAiB,CAAC,QAAgB,aAChC,4BAAY,OAAO,aAAa,iBAAiB,EAAE,QAAQ,SAAS,CAAC;AAAA;AAAA,IAGvE,qBAAqB,CAAC,WAAmB,WAAoB,SAAkB,cAC7E,4BAAY,OAAO,aAAa,mBAAmB,EAAE,WAAW,WAAW,SAAS,UAAU,CAAC;AAAA;AAAA,IAGjG,eAAe,CAAC,cACd,4BAAY,OAAO,aAAa,gBAAgB,SAAS;AAAA,IAC3D,eAAe,MACb,4BAAY,OAAO,aAAa,cAAc;AAAA,IAChD,eAAe,CAAC,cACd,4BAAY,OAAO,aAAa,gBAAgB,SAAS;AAAA;AAAA,IAG3D,WAAW,MACT,4BAAY,OAAO,aAAa,UAAU;AAAA,IAC5C,kBAAkB,CAAC,aACjB,4BAAY,OAAO,aAAa,eAAe,QAAQ;AAAA,IACzD,SAAS,CAAC,OAAe,aACvB,4BAAY,OAAO,aAAa,MAAM,EAAE,OAAO,SAAS,CAAC;AAAA,IAC3D,WAAW,MACT,4BAAY,OAAO,aAAa,UAAU;AAAA;AAAA,IAG5C,YAAY,CAAC,aAAsC;AACjD,kCAAY,GAAG,aAAa,UAAU,CAAC,QAAQ,SAAS,SAAS,IAAI,CAAC;AAAA,IACxE;AAAA,IACA,WAAW,CAAC,aAAsC;AAChD,kCAAY,GAAG,aAAa,SAAS,CAAC,QAAQ,SAAS,SAAS,IAAI,CAAC;AAAA,IACvE;AAAA;AAAA,IAGA,wBAAwB,MAAM;AAC5B,kCAAY,mBAAmB,aAAa,QAAQ;AAAA,IACtD;AAAA,IACA,uBAAuB,MAAM;AAC3B,kCAAY,mBAAmB,aAAa,OAAO;AAAA,IACrD;AAAA,EACF;AAEA,gCAAc,kBAAkB,cAAc,GAAG;AACnD;;;AEzFA,sBAAsB;","names":[]}
1
+ {"version":3,"sources":["../src/ipc/preload-api.ts","../src/ipc/channels.ts","../src/core/dashboard-preload.ts"],"sourcesContent":["/**\r\n * Preload API 辅助\r\n * \r\n * 定义暴露给监控面板渲染进程的 API\r\n */\r\n\r\nimport { contextBridge, ipcRenderer } from 'electron'\r\nimport { IPC_CHANNELS } from './channels'\r\nimport type { SessionsListPayload, TestSession } from '../types/session'\r\n\r\n/** 监控面板 preload 注入的 API 类型 */\r\nexport interface MonitorPanelAPI {\r\n // 会话控制\r\n startSession: (label: string, description?: string) => Promise<string>\r\n stopSession: () => Promise<unknown>\r\n getSessions: () => Promise<SessionsListPayload | TestSession[]>\r\n getSessionReport: (sessionId: string) => Promise<unknown>\r\n compareSessions: (baseId: string, targetId: string) => Promise<unknown>\r\n\r\n // 数据查询\r\n getSessionSnapshots: (sessionId: string, startTime?: number, endTime?: number, maxPoints?: number) => Promise<unknown[]>\r\n\r\n // 导入导出\r\n exportSession: (sessionId: string) => Promise<{ success: boolean; filePath?: string; error?: string }>\r\n importSession: () => Promise<{ success: boolean; session?: unknown; error?: string }>\r\n deleteSession: (sessionId: string) => Promise<boolean>\r\n\r\n // 工具\r\n triggerGC: () => Promise<unknown>\r\n takeHeapSnapshot: (filePath?: string) => Promise<string>\r\n addMark: (label: string, metadata?: Record<string, unknown>) => Promise<void>\r\n getConfig: () => Promise<unknown>\r\n\r\n // 数据订阅\r\n onSnapshot: (callback: (data: unknown) => void) => void\r\n onAnomaly: (callback: (data: unknown) => void) => void\r\n\r\n // 移除监听器\r\n removeSnapshotListener: () => void\r\n removeAnomalyListener: () => void\r\n}\r\n\r\n/** 在监控面板的 preload 中注入 API */\r\nexport function injectMonitorPanelAPI(): void {\r\n const api: MonitorPanelAPI = {\r\n // 会话控制\r\n startSession: (label: string, description?: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_START, { label, description }),\r\n stopSession: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_STOP),\r\n getSessions: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.GET_SESSIONS),\r\n getSessionReport: (sessionId: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_REPORT, sessionId),\r\n compareSessions: (baseId: string, targetId: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_COMPARE, { baseId, targetId }),\r\n\r\n // 数据查询\r\n getSessionSnapshots: (sessionId: string, startTime?: number, endTime?: number, maxPoints?: number) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_SNAPSHOTS, { sessionId, startTime, endTime, maxPoints }),\r\n\r\n // 导入导出\r\n exportSession: (sessionId: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_EXPORT, sessionId),\r\n importSession: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_IMPORT),\r\n deleteSession: (sessionId: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.SESSION_DELETE, sessionId),\r\n\r\n // 工具\r\n triggerGC: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.TRIGGER_GC),\r\n takeHeapSnapshot: (filePath?: string) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.HEAP_SNAPSHOT, filePath),\r\n addMark: (label: string, metadata?: Record<string, unknown>) =>\r\n ipcRenderer.invoke(IPC_CHANNELS.MARK, { label, metadata }),\r\n getConfig: () =>\r\n ipcRenderer.invoke(IPC_CHANNELS.GET_CONFIG),\r\n\r\n // 数据订阅\r\n onSnapshot: (callback: (data: unknown) => void) => {\r\n ipcRenderer.on(IPC_CHANNELS.SNAPSHOT, (_event, data) => callback(data))\r\n },\r\n onAnomaly: (callback: (data: unknown) => void) => {\r\n ipcRenderer.on(IPC_CHANNELS.ANOMALY, (_event, data) => callback(data))\r\n },\r\n\r\n // 移除监听器\r\n removeSnapshotListener: () => {\r\n ipcRenderer.removeAllListeners(IPC_CHANNELS.SNAPSHOT)\r\n },\r\n removeAnomalyListener: () => {\r\n ipcRenderer.removeAllListeners(IPC_CHANNELS.ANOMALY)\r\n },\r\n }\r\n\r\n contextBridge.exposeInMainWorld('monitorAPI', api)\r\n}\r\n","/**\r\n * IPC 通道常量定义\r\n * 所有通道以 'emm:' 为前缀,避免与业务 IPC 冲突\r\n */\r\n\r\nexport const IPC_CHANNELS = {\r\n // === 数据推送(主进程 → 监控面板)===\r\n SNAPSHOT: 'emm:snapshot',\r\n ANOMALY: 'emm:anomaly',\r\n\r\n // === 会话控制(面板 → 主进程)===\r\n SESSION_START: 'emm:session:start',\r\n SESSION_STOP: 'emm:session:stop',\r\n SESSION_LIST: 'emm:session:list',\r\n SESSION_REPORT: 'emm:session:report',\r\n SESSION_COMPARE: 'emm:session:compare',\r\n\r\n // === 数据查询(面板 → 主进程)===\r\n SESSION_SNAPSHOTS: 'emm:session:snapshots',\r\n\r\n // === 工具操作(面板 → 主进程)===\r\n TRIGGER_GC: 'emm:gc',\r\n HEAP_SNAPSHOT: 'emm:heap-snapshot',\r\n MARK: 'emm:mark',\r\n CONFIG_UPDATE: 'emm:config:update',\r\n GET_CONFIG: 'emm:config:get',\r\n GET_SESSIONS: 'emm:sessions:get',\r\n\r\n // === 导入导出(面板 → 主进程)===\r\n SESSION_EXPORT: 'emm:session:export',\r\n SESSION_IMPORT: 'emm:session:import',\r\n SESSION_DELETE: 'emm:session:delete',\r\n\r\n // === 渲染进程上报(可选)===\r\n RENDERER_REPORT: 'emm:renderer:report',\r\n RENDERER_REQUEST: 'emm:renderer:request',\r\n} as const\r\n","/**\r\n * Dashboard Preload 脚本\r\n * 在监控面板 BrowserWindow 中使用\r\n */\r\n\r\nimport { injectMonitorPanelAPI } from '../ipc/preload-api'\r\n\r\ninjectMonitorPanelAPI()\r\n"],"mappings":";;;AAMA,sBAA2C;;;ACDpC,IAAM,eAAe;AAAA;AAAA,EAE1B,UAAU;AAAA,EACV,SAAS;AAAA;AAAA,EAGT,eAAe;AAAA,EACf,cAAc;AAAA,EACd,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,iBAAiB;AAAA;AAAA,EAGjB,mBAAmB;AAAA;AAAA,EAGnB,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,MAAM;AAAA,EACN,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,cAAc;AAAA;AAAA,EAGd,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,gBAAgB;AAAA;AAAA,EAGhB,iBAAiB;AAAA,EACjB,kBAAkB;AACpB;;;ADOO,SAAS,wBAA8B;AAC5C,QAAM,MAAuB;AAAA;AAAA,IAE3B,cAAc,CAAC,OAAe,gBAC5B,4BAAY,OAAO,aAAa,eAAe,EAAE,OAAO,YAAY,CAAC;AAAA,IACvE,aAAa,MACX,4BAAY,OAAO,aAAa,YAAY;AAAA,IAC9C,aAAa,MACX,4BAAY,OAAO,aAAa,YAAY;AAAA,IAC9C,kBAAkB,CAAC,cACjB,4BAAY,OAAO,aAAa,gBAAgB,SAAS;AAAA,IAC3D,iBAAiB,CAAC,QAAgB,aAChC,4BAAY,OAAO,aAAa,iBAAiB,EAAE,QAAQ,SAAS,CAAC;AAAA;AAAA,IAGvE,qBAAqB,CAAC,WAAmB,WAAoB,SAAkB,cAC7E,4BAAY,OAAO,aAAa,mBAAmB,EAAE,WAAW,WAAW,SAAS,UAAU,CAAC;AAAA;AAAA,IAGjG,eAAe,CAAC,cACd,4BAAY,OAAO,aAAa,gBAAgB,SAAS;AAAA,IAC3D,eAAe,MACb,4BAAY,OAAO,aAAa,cAAc;AAAA,IAChD,eAAe,CAAC,cACd,4BAAY,OAAO,aAAa,gBAAgB,SAAS;AAAA;AAAA,IAG3D,WAAW,MACT,4BAAY,OAAO,aAAa,UAAU;AAAA,IAC5C,kBAAkB,CAAC,aACjB,4BAAY,OAAO,aAAa,eAAe,QAAQ;AAAA,IACzD,SAAS,CAAC,OAAe,aACvB,4BAAY,OAAO,aAAa,MAAM,EAAE,OAAO,SAAS,CAAC;AAAA,IAC3D,WAAW,MACT,4BAAY,OAAO,aAAa,UAAU;AAAA;AAAA,IAG5C,YAAY,CAAC,aAAsC;AACjD,kCAAY,GAAG,aAAa,UAAU,CAAC,QAAQ,SAAS,SAAS,IAAI,CAAC;AAAA,IACxE;AAAA,IACA,WAAW,CAAC,aAAsC;AAChD,kCAAY,GAAG,aAAa,SAAS,CAAC,QAAQ,SAAS,SAAS,IAAI,CAAC;AAAA,IACvE;AAAA;AAAA,IAGA,wBAAwB,MAAM;AAC5B,kCAAY,mBAAmB,aAAa,QAAQ;AAAA,IACtD;AAAA,IACA,uBAAuB,MAAM;AAC3B,kCAAY,mBAAmB,aAAa,OAAO;AAAA,IACrD;AAAA,EACF;AAEA,gCAAc,kBAAkB,cAAc,GAAG;AACnD;;;AE1FA,sBAAsB;","names":[]}
package/dist/index.d.mts CHANGED
@@ -175,6 +175,17 @@ interface MonitorConfig {
175
175
  autoStart: boolean;
176
176
  /** 启动后是否自动打开监控面板,默认 true */
177
177
  openDashboardOnStart: boolean;
178
+ session: {
179
+ /**
180
+ * 监控启动后自动创建一条「进行中」会话并开始写入快照,无需在看板点「开始会话」。
181
+ * 每次进程启动一条新会话,标签带本地时间。默认 true。
182
+ */
183
+ autoStartOnLaunch: boolean;
184
+ /** 自动会话标签前缀,完整标签为 `${prefix} YYYY-MM-DD HH:mm:ss` */
185
+ autoLabelPrefix: string;
186
+ /** 自动会话描述,可选 */
187
+ autoDescription?: string;
188
+ };
178
189
  /** 采集间隔 (ms),默认 2000 */
179
190
  collectInterval: number;
180
191
  /** 落盘间隔 (条数),默认 60 */
@@ -243,6 +254,11 @@ interface SessionIndex {
243
254
  sessions: TestSession[];
244
255
  lastUpdated: number;
245
256
  }
257
+ /** IPC 拉取会话列表时附带「当前是否在录」:以主进程内存为准,避免索引里僵尸 running */
258
+ interface SessionsListPayload {
259
+ sessions: TestSession[];
260
+ activeSessionId: string | null;
261
+ }
246
262
 
247
263
  /**
248
264
  * 报告与对比相关类型
@@ -277,6 +293,17 @@ interface TrendInfo {
277
293
  /** 置信度 */
278
294
  confidence: 'high' | 'medium' | 'low';
279
295
  }
296
+ /** 会话中的阶段标记(写入报告,便于对照各阶段内存) */
297
+ interface SessionEventMark {
298
+ timestamp: number;
299
+ label: string;
300
+ metadata?: Record<string, unknown>;
301
+ /** 该标记随附快照时刻的总工作集 (KB) */
302
+ totalWorkingSetKB: number;
303
+ browserKB: number;
304
+ rendererKB: number;
305
+ gpuKB: number;
306
+ }
280
307
  /** 改进建议 */
281
308
  interface Suggestion {
282
309
  /** 建议 ID */
@@ -339,6 +366,8 @@ interface SessionReport {
339
366
  };
340
367
  anomalies: AnomalyEvent[];
341
368
  suggestions: Suggestion[];
369
+ /** 阶段标记汇总(与快照中的 marks 一致,便于表格展示) */
370
+ eventMarks?: SessionEventMark[];
342
371
  dataFile: string;
343
372
  }
344
373
  /** 指标差异 */
@@ -438,6 +467,8 @@ declare class ElectronMemoryMonitor extends EventEmitter {
438
467
  startSession(label: string, description?: string): string;
439
468
  /** 结束当前会话 */
440
469
  stopSession(): Promise<SessionReport | null>;
470
+ /** 为已落盘元数据的会话生成并写入 report.json(显式结束或被新会话顶替) */
471
+ private persistCompletedSessionReport;
441
472
  /** 打开监控面板 */
442
473
  openDashboard(): void;
443
474
  /** 关闭监控面板 */
@@ -446,6 +477,10 @@ declare class ElectronMemoryMonitor extends EventEmitter {
446
477
  getCurrentSnapshot(): MemorySnapshot | null;
447
478
  /** 获取历史会话列表 */
448
479
  getSessions(): Promise<TestSession[]>;
480
+ /**
481
+ * 供监控面板 IPC:列表来自磁盘索引,是否在录制以内存中的 currentSession 为准(避免索引僵尸 running)
482
+ */
483
+ getSessionsPayloadForIpc(): SessionsListPayload;
449
484
  /** 获取指定会话报告 */
450
485
  getSessionReport(sessionId: string): Promise<SessionReport | null>;
451
486
  /** 获取指定会话的快照数据(支持时间过滤和降采样) */
@@ -478,6 +513,7 @@ declare class ElectronMemoryMonitor extends EventEmitter {
478
513
  getConfig(): MonitorConfig;
479
514
  on(event: 'snapshot', handler: (data: MemorySnapshot) => void): this;
480
515
  on(event: 'anomaly', handler: (event: AnomalyEvent) => void): this;
516
+ on(event: 'session-start', handler: (session: TestSession) => void): this;
481
517
  on(event: 'session-end', handler: (report: SessionReport) => void): this;
482
518
  private onSnapshot;
483
519
  private mergeConfig;
@@ -515,4 +551,4 @@ declare const IPC_CHANNELS: {
515
551
  readonly RENDERER_REQUEST: "emm:renderer:request";
516
552
  };
517
553
 
518
- export { type AnomalyCategory, type AnomalyEvent, type AnomalyRule, type AnomalySeverity, type CompareReport, ElectronMemoryMonitor, type EventMark, type GCResult, IPC_CHANNELS, type Improvement, type MemorySnapshot, type MetricDiff, type MetricSummary, type MonitorConfig, type ProcessMemoryInfo, type Regression, type RendererV8Detail, type SessionIndex, type SessionReport, type Suggestion, type SystemMemoryInfo, type TestSession, type TrendInfo, type V8HeapDetailStats, type V8HeapSpaceInfo, type V8HeapStats, registerDashboardSchemePrivileged };
554
+ export { type AnomalyCategory, type AnomalyEvent, type AnomalyRule, type AnomalySeverity, type CompareReport, ElectronMemoryMonitor, type EventMark, type GCResult, IPC_CHANNELS, type Improvement, type MemorySnapshot, type MetricDiff, type MetricSummary, type MonitorConfig, type ProcessMemoryInfo, type Regression, type RendererV8Detail, type SessionEventMark, type SessionIndex, type SessionReport, type SessionsListPayload, type Suggestion, type SystemMemoryInfo, type TestSession, type TrendInfo, type V8HeapDetailStats, type V8HeapSpaceInfo, type V8HeapStats, registerDashboardSchemePrivileged };
package/dist/index.d.ts CHANGED
@@ -175,6 +175,17 @@ interface MonitorConfig {
175
175
  autoStart: boolean;
176
176
  /** 启动后是否自动打开监控面板,默认 true */
177
177
  openDashboardOnStart: boolean;
178
+ session: {
179
+ /**
180
+ * 监控启动后自动创建一条「进行中」会话并开始写入快照,无需在看板点「开始会话」。
181
+ * 每次进程启动一条新会话,标签带本地时间。默认 true。
182
+ */
183
+ autoStartOnLaunch: boolean;
184
+ /** 自动会话标签前缀,完整标签为 `${prefix} YYYY-MM-DD HH:mm:ss` */
185
+ autoLabelPrefix: string;
186
+ /** 自动会话描述,可选 */
187
+ autoDescription?: string;
188
+ };
178
189
  /** 采集间隔 (ms),默认 2000 */
179
190
  collectInterval: number;
180
191
  /** 落盘间隔 (条数),默认 60 */
@@ -243,6 +254,11 @@ interface SessionIndex {
243
254
  sessions: TestSession[];
244
255
  lastUpdated: number;
245
256
  }
257
+ /** IPC 拉取会话列表时附带「当前是否在录」:以主进程内存为准,避免索引里僵尸 running */
258
+ interface SessionsListPayload {
259
+ sessions: TestSession[];
260
+ activeSessionId: string | null;
261
+ }
246
262
 
247
263
  /**
248
264
  * 报告与对比相关类型
@@ -277,6 +293,17 @@ interface TrendInfo {
277
293
  /** 置信度 */
278
294
  confidence: 'high' | 'medium' | 'low';
279
295
  }
296
+ /** 会话中的阶段标记(写入报告,便于对照各阶段内存) */
297
+ interface SessionEventMark {
298
+ timestamp: number;
299
+ label: string;
300
+ metadata?: Record<string, unknown>;
301
+ /** 该标记随附快照时刻的总工作集 (KB) */
302
+ totalWorkingSetKB: number;
303
+ browserKB: number;
304
+ rendererKB: number;
305
+ gpuKB: number;
306
+ }
280
307
  /** 改进建议 */
281
308
  interface Suggestion {
282
309
  /** 建议 ID */
@@ -339,6 +366,8 @@ interface SessionReport {
339
366
  };
340
367
  anomalies: AnomalyEvent[];
341
368
  suggestions: Suggestion[];
369
+ /** 阶段标记汇总(与快照中的 marks 一致,便于表格展示) */
370
+ eventMarks?: SessionEventMark[];
342
371
  dataFile: string;
343
372
  }
344
373
  /** 指标差异 */
@@ -438,6 +467,8 @@ declare class ElectronMemoryMonitor extends EventEmitter {
438
467
  startSession(label: string, description?: string): string;
439
468
  /** 结束当前会话 */
440
469
  stopSession(): Promise<SessionReport | null>;
470
+ /** 为已落盘元数据的会话生成并写入 report.json(显式结束或被新会话顶替) */
471
+ private persistCompletedSessionReport;
441
472
  /** 打开监控面板 */
442
473
  openDashboard(): void;
443
474
  /** 关闭监控面板 */
@@ -446,6 +477,10 @@ declare class ElectronMemoryMonitor extends EventEmitter {
446
477
  getCurrentSnapshot(): MemorySnapshot | null;
447
478
  /** 获取历史会话列表 */
448
479
  getSessions(): Promise<TestSession[]>;
480
+ /**
481
+ * 供监控面板 IPC:列表来自磁盘索引,是否在录制以内存中的 currentSession 为准(避免索引僵尸 running)
482
+ */
483
+ getSessionsPayloadForIpc(): SessionsListPayload;
449
484
  /** 获取指定会话报告 */
450
485
  getSessionReport(sessionId: string): Promise<SessionReport | null>;
451
486
  /** 获取指定会话的快照数据(支持时间过滤和降采样) */
@@ -478,6 +513,7 @@ declare class ElectronMemoryMonitor extends EventEmitter {
478
513
  getConfig(): MonitorConfig;
479
514
  on(event: 'snapshot', handler: (data: MemorySnapshot) => void): this;
480
515
  on(event: 'anomaly', handler: (event: AnomalyEvent) => void): this;
516
+ on(event: 'session-start', handler: (session: TestSession) => void): this;
481
517
  on(event: 'session-end', handler: (report: SessionReport) => void): this;
482
518
  private onSnapshot;
483
519
  private mergeConfig;
@@ -515,4 +551,4 @@ declare const IPC_CHANNELS: {
515
551
  readonly RENDERER_REQUEST: "emm:renderer:request";
516
552
  };
517
553
 
518
- export { type AnomalyCategory, type AnomalyEvent, type AnomalyRule, type AnomalySeverity, type CompareReport, ElectronMemoryMonitor, type EventMark, type GCResult, IPC_CHANNELS, type Improvement, type MemorySnapshot, type MetricDiff, type MetricSummary, type MonitorConfig, type ProcessMemoryInfo, type Regression, type RendererV8Detail, type SessionIndex, type SessionReport, type Suggestion, type SystemMemoryInfo, type TestSession, type TrendInfo, type V8HeapDetailStats, type V8HeapSpaceInfo, type V8HeapStats, registerDashboardSchemePrivileged };
554
+ export { type AnomalyCategory, type AnomalyEvent, type AnomalyRule, type AnomalySeverity, type CompareReport, ElectronMemoryMonitor, type EventMark, type GCResult, IPC_CHANNELS, type Improvement, type MemorySnapshot, type MetricDiff, type MetricSummary, type MonitorConfig, type ProcessMemoryInfo, type Regression, type RendererV8Detail, type SessionEventMark, type SessionIndex, type SessionReport, type SessionsListPayload, type Suggestion, type SystemMemoryInfo, type TestSession, type TrendInfo, type V8HeapDetailStats, type V8HeapSpaceInfo, type V8HeapStats, registerDashboardSchemePrivileged };
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 {
@@ -1434,10 +1540,28 @@ var IPCMainHandler = class {
1434
1540
  return this.monitor.startSession(args.label, args.description);
1435
1541
  });
1436
1542
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
1437
- 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
+ }
1438
1562
  });
1439
1563
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
1440
- return this.monitor.getSessions();
1564
+ return this.monitor.getSessionsPayloadForIpc();
1441
1565
  });
1442
1566
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
1443
1567
  return this.monitor.getSessionReport(sessionId);
@@ -1461,7 +1585,7 @@ var IPCMainHandler = class {
1461
1585
  return this.monitor.getConfig();
1462
1586
  });
1463
1587
  import_electron4.ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
1464
- return this.monitor.getSessions();
1588
+ return this.monitor.getSessionsPayloadForIpc();
1465
1589
  });
1466
1590
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
1467
1591
  return this.monitor.exportSession(sessionId);
@@ -1472,8 +1596,11 @@ var IPCMainHandler = class {
1472
1596
  import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
1473
1597
  return this.monitor.deleteSession(sessionId);
1474
1598
  });
1475
- import_electron4.ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (_event, detail) => {
1476
- 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
+ });
1477
1604
  });
1478
1605
  }
1479
1606
  /** 向监控面板推送快照数据 */
@@ -1503,6 +1630,10 @@ var DEFAULT_CONFIG = {
1503
1630
  enabled: true,
1504
1631
  autoStart: true,
1505
1632
  openDashboardOnStart: true,
1633
+ session: {
1634
+ autoStartOnLaunch: true,
1635
+ autoLabelPrefix: "\u81EA\u52A8\u4F1A\u8BDD"
1636
+ },
1506
1637
  collectInterval: 2e3,
1507
1638
  persistInterval: 60,
1508
1639
  enableRendererDetail: false,
@@ -1528,6 +1659,36 @@ var DEFAULT_CONFIG = {
1528
1659
  };
1529
1660
 
1530
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
+ }
1531
1692
  var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1532
1693
  constructor(config) {
1533
1694
  super();
@@ -1571,11 +1732,20 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1571
1732
  });
1572
1733
  this.collector.start();
1573
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
+ }
1574
1746
  if (this.config.openDashboardOnStart) {
1575
1747
  this.openDashboard();
1576
1748
  }
1577
- this.persister.cleanOldSessions();
1578
- this.started = true;
1579
1749
  }
1580
1750
  /** 停止监控 */
1581
1751
  async stop() {
@@ -1604,9 +1774,16 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1604
1774
  if (!this.started) {
1605
1775
  throw new Error("Monitor is not started");
1606
1776
  }
1607
- 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
+ }
1608
1784
  this.collector.setSessionId(session.id);
1609
1785
  this.anomalyDetector.clearAnomalies();
1786
+ this.emit("session-start", session);
1610
1787
  return session.id;
1611
1788
  }
1612
1789
  /** 结束当前会话 */
@@ -1617,8 +1794,26 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1617
1794
  const completedSession = this.sessionManager.endSession();
1618
1795
  if (!completedSession) return null;
1619
1796
  this.collector.setSessionId(null);
1620
- const snapshots = this.persister.readSessionSnapshots(completedSession.id);
1621
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
+ }
1622
1817
  const report = this.analyzer.generateReport(
1623
1818
  completedSession.id,
1624
1819
  completedSession.label,
@@ -1630,8 +1825,7 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1630
1825
  completedSession.dataFile
1631
1826
  );
1632
1827
  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");
1828
+ await fs2.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
1635
1829
  this.emit("session-end", report);
1636
1830
  return report;
1637
1831
  }
@@ -1656,6 +1850,18 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1656
1850
  async getSessions() {
1657
1851
  return this.sessionManager.getSessions();
1658
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
+ }
1659
1865
  /** 获取指定会话报告 */
1660
1866
  async getSessionReport(sessionId) {
1661
1867
  const fs2 = await import("fs");
@@ -1667,7 +1873,6 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1667
1873
  const session = this.sessionManager.getSession(sessionId);
1668
1874
  if (!session || !session.endTime) return null;
1669
1875
  const snapshots = this.persister.readSessionSnapshots(sessionId);
1670
- if (snapshots.length === 0) return null;
1671
1876
  return this.analyzer.generateReport(
1672
1877
  session.id,
1673
1878
  session.label,
@@ -1689,17 +1894,18 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1689
1894
  if (endTime != null) {
1690
1895
  snapshots = snapshots.filter((s) => s.timestamp <= endTime);
1691
1896
  }
1897
+ const beforeDownsample = snapshots;
1692
1898
  const limit = maxPoints ?? 600;
1693
- if (snapshots.length > limit) {
1694
- const step = snapshots.length / limit;
1899
+ if (beforeDownsample.length > limit) {
1900
+ const step = beforeDownsample.length / limit;
1695
1901
  const sampled = [];
1696
1902
  for (let i = 0; i < limit; i++) {
1697
- sampled.push(snapshots[Math.round(i * step)]);
1903
+ sampled.push(beforeDownsample[Math.round(i * step)]);
1698
1904
  }
1699
- if (sampled[sampled.length - 1] !== snapshots[snapshots.length - 1]) {
1700
- 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];
1701
1907
  }
1702
- snapshots = sampled;
1908
+ snapshots = mergeMarksFromExcludedSnapshots(beforeDownsample, sampled);
1703
1909
  }
1704
1910
  return snapshots;
1705
1911
  }
@@ -1850,6 +2056,10 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
1850
2056
  return {
1851
2057
  ...DEFAULT_CONFIG,
1852
2058
  ...userConfig,
2059
+ session: {
2060
+ ...DEFAULT_CONFIG.session,
2061
+ ...userConfig.session || {}
2062
+ },
1853
2063
  anomaly: {
1854
2064
  ...DEFAULT_CONFIG.anomaly,
1855
2065
  ...userConfig.anomaly || {}