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