@electron-memory/monitor 0.1.0
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 +75 -0
- package/dist/dashboard-preload.js.map +1 -0
- package/dist/index.d.mts +510 -0
- package/dist/index.d.ts +510 -0
- package/dist/index.js +1640 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1602 -0
- package/dist/index.mjs.map +1 -0
- package/dist/preload.d.mts +18 -0
- package/dist/preload.d.ts +18 -0
- package/dist/preload.js +91 -0
- package/dist/preload.js.map +1 -0
- package/dist/preload.mjs +66 -0
- package/dist/preload.mjs.map +1 -0
- package/dist/ui/assets/index-BXj3TlLS.js +9 -0
- package/dist/ui/assets/index-DpEoEDgy.css +1 -0
- package/dist/ui/assets/vendor-BMPuFM9B.js +104 -0
- package/dist/ui/index.html +14 -0
- package/package.json +70 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1602 @@
|
|
|
1
|
+
// src/core/monitor.ts
|
|
2
|
+
import { app as app2 } from "electron";
|
|
3
|
+
import * as path3 from "path";
|
|
4
|
+
import * as v82 from "v8";
|
|
5
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
6
|
+
|
|
7
|
+
// src/core/collector.ts
|
|
8
|
+
import { app, BrowserWindow, webContents } from "electron";
|
|
9
|
+
import * as v8 from "v8";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import { EventEmitter } from "events";
|
|
12
|
+
var MemoryCollector = class extends EventEmitter {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
super();
|
|
15
|
+
this.timer = null;
|
|
16
|
+
this.seq = 0;
|
|
17
|
+
this.currentSessionId = null;
|
|
18
|
+
this.pendingMarks = [];
|
|
19
|
+
this.rendererDetails = /* @__PURE__ */ new Map();
|
|
20
|
+
this.monitorWindowId = null;
|
|
21
|
+
this.config = config;
|
|
22
|
+
}
|
|
23
|
+
/** 设置监控面板的 webContents ID,用于标记 */
|
|
24
|
+
setMonitorWindowId(id) {
|
|
25
|
+
this.monitorWindowId = id;
|
|
26
|
+
}
|
|
27
|
+
/** 设置当前会话 ID */
|
|
28
|
+
setSessionId(sessionId) {
|
|
29
|
+
this.currentSessionId = sessionId;
|
|
30
|
+
}
|
|
31
|
+
/** 添加事件标记 */
|
|
32
|
+
addMark(label, metadata) {
|
|
33
|
+
this.pendingMarks.push({
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
label,
|
|
36
|
+
metadata
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** 更新渲染进程 V8 详情 */
|
|
40
|
+
updateRendererDetail(detail) {
|
|
41
|
+
this.rendererDetails.set(detail.webContentsId, detail);
|
|
42
|
+
}
|
|
43
|
+
/** 开始采集 */
|
|
44
|
+
start() {
|
|
45
|
+
if (this.timer) return;
|
|
46
|
+
this.collect();
|
|
47
|
+
this.timer = setInterval(() => {
|
|
48
|
+
this.collect();
|
|
49
|
+
}, this.config.collectInterval);
|
|
50
|
+
}
|
|
51
|
+
/** 停止采集 */
|
|
52
|
+
stop() {
|
|
53
|
+
if (this.timer) {
|
|
54
|
+
clearInterval(this.timer);
|
|
55
|
+
this.timer = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 执行一次采集 */
|
|
59
|
+
collect() {
|
|
60
|
+
try {
|
|
61
|
+
const snapshot = this.buildSnapshot();
|
|
62
|
+
this.emit("snapshot", snapshot);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
this.emit("error", err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** 构建完整的内存快照 */
|
|
68
|
+
buildSnapshot() {
|
|
69
|
+
const timestamp = Date.now();
|
|
70
|
+
const processes = this.collectProcesses();
|
|
71
|
+
const mainProcessMemory = this.collectMainProcessMemory();
|
|
72
|
+
const mainProcessV8Detail = this.collectMainProcessV8Detail();
|
|
73
|
+
const system = this.collectSystemMemory();
|
|
74
|
+
const totalWorkingSetSize = processes.reduce(
|
|
75
|
+
(sum, p) => p.isMonitorProcess ? sum : sum + p.memory.workingSetSize,
|
|
76
|
+
0
|
|
77
|
+
);
|
|
78
|
+
const marks = this.pendingMarks.length > 0 ? [...this.pendingMarks] : void 0;
|
|
79
|
+
this.pendingMarks = [];
|
|
80
|
+
const rendererDetails = this.rendererDetails.size > 0 ? Array.from(this.rendererDetails.values()) : void 0;
|
|
81
|
+
const snapshot = {
|
|
82
|
+
timestamp,
|
|
83
|
+
sessionId: this.currentSessionId ?? void 0,
|
|
84
|
+
seq: this.seq++,
|
|
85
|
+
processes,
|
|
86
|
+
totalWorkingSetSize,
|
|
87
|
+
mainProcessMemory,
|
|
88
|
+
mainProcessV8Detail,
|
|
89
|
+
system,
|
|
90
|
+
rendererDetails,
|
|
91
|
+
marks
|
|
92
|
+
};
|
|
93
|
+
return snapshot;
|
|
94
|
+
}
|
|
95
|
+
/** 采集所有进程信息 */
|
|
96
|
+
collectProcesses() {
|
|
97
|
+
const metrics = app.getAppMetrics();
|
|
98
|
+
const wcList = webContents.getAllWebContents();
|
|
99
|
+
const pidToWc = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const wc of wcList) {
|
|
101
|
+
try {
|
|
102
|
+
const pid = wc.getOSProcessId();
|
|
103
|
+
pidToWc.set(pid, wc);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const wcIdToTitle = /* @__PURE__ */ new Map();
|
|
108
|
+
const allWindows = BrowserWindow.getAllWindows();
|
|
109
|
+
for (const win of allWindows) {
|
|
110
|
+
try {
|
|
111
|
+
wcIdToTitle.set(win.webContents.id, win.getTitle());
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return metrics.map((metric) => {
|
|
116
|
+
const wc = pidToWc.get(metric.pid);
|
|
117
|
+
let windowTitle;
|
|
118
|
+
let webContentsId;
|
|
119
|
+
let isMonitorProcess = false;
|
|
120
|
+
if (wc) {
|
|
121
|
+
webContentsId = wc.id;
|
|
122
|
+
windowTitle = wcIdToTitle.get(wc.id);
|
|
123
|
+
if (this.monitorWindowId !== null && wc.id === this.monitorWindowId) {
|
|
124
|
+
isMonitorProcess = true;
|
|
125
|
+
windowTitle = "[Memory Monitor]";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let name = windowTitle;
|
|
129
|
+
if (windowTitle && this.config.processLabels[windowTitle]) {
|
|
130
|
+
name = this.config.processLabels[windowTitle];
|
|
131
|
+
}
|
|
132
|
+
const info = {
|
|
133
|
+
pid: metric.pid,
|
|
134
|
+
type: metric.type,
|
|
135
|
+
name,
|
|
136
|
+
isMonitorProcess,
|
|
137
|
+
cpu: {
|
|
138
|
+
percentCPUUsage: metric.cpu.percentCPUUsage,
|
|
139
|
+
idleWakeupsPerSecond: metric.cpu.idleWakeupsPerSecond
|
|
140
|
+
},
|
|
141
|
+
memory: {
|
|
142
|
+
workingSetSize: metric.memory.workingSetSize,
|
|
143
|
+
peakWorkingSetSize: metric.memory.peakWorkingSetSize,
|
|
144
|
+
privateBytes: metric.memory.privateBytes
|
|
145
|
+
},
|
|
146
|
+
webContentsId,
|
|
147
|
+
windowTitle
|
|
148
|
+
};
|
|
149
|
+
return info;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** 采集主进程 Node.js 内存 */
|
|
153
|
+
collectMainProcessMemory() {
|
|
154
|
+
const mem = process.memoryUsage();
|
|
155
|
+
return {
|
|
156
|
+
heapUsed: mem.heapUsed,
|
|
157
|
+
heapTotal: mem.heapTotal,
|
|
158
|
+
external: mem.external,
|
|
159
|
+
arrayBuffers: mem.arrayBuffers,
|
|
160
|
+
rss: mem.rss
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** 采集主进程 V8 详细统计 */
|
|
164
|
+
collectMainProcessV8Detail() {
|
|
165
|
+
const mem = process.memoryUsage();
|
|
166
|
+
const heapStats = v8.getHeapStatistics();
|
|
167
|
+
let heapSpaces;
|
|
168
|
+
if (this.config.enableV8HeapSpaces) {
|
|
169
|
+
heapSpaces = v8.getHeapSpaceStatistics().map((space) => ({
|
|
170
|
+
name: space.space_name ?? space.spaceName,
|
|
171
|
+
size: space.space_size ?? space.spaceSize,
|
|
172
|
+
usedSize: space.space_used_size ?? space.spaceUsedSize,
|
|
173
|
+
availableSize: space.space_available_size ?? space.spaceAvailableSize,
|
|
174
|
+
physicalSize: space.physical_space_size ?? space.physicalSpaceSize
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
heapUsed: mem.heapUsed,
|
|
179
|
+
heapTotal: mem.heapTotal,
|
|
180
|
+
external: mem.external,
|
|
181
|
+
arrayBuffers: mem.arrayBuffers,
|
|
182
|
+
rss: mem.rss,
|
|
183
|
+
totalHeapSize: heapStats.total_heap_size ?? heapStats.totalHeapSize,
|
|
184
|
+
usedHeapSize: heapStats.used_heap_size ?? heapStats.usedHeapSize,
|
|
185
|
+
heapSizeLimit: heapStats.heap_size_limit ?? heapStats.heapSizeLimit,
|
|
186
|
+
mallocedMemory: heapStats.malloced_memory ?? heapStats.mallocedMemory,
|
|
187
|
+
peakMallocedMemory: heapStats.peak_malloced_memory ?? heapStats.peakMallocedMemory,
|
|
188
|
+
numberOfDetachedContexts: heapStats.number_of_detached_contexts ?? heapStats.numberOfDetachedContexts,
|
|
189
|
+
numberOfNativeContexts: heapStats.number_of_native_contexts ?? heapStats.numberOfNativeContexts,
|
|
190
|
+
heapSpaces
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** 采集系统内存 */
|
|
194
|
+
collectSystemMemory() {
|
|
195
|
+
const total = os.totalmem();
|
|
196
|
+
const free = os.freemem();
|
|
197
|
+
const used = total - free;
|
|
198
|
+
return {
|
|
199
|
+
total,
|
|
200
|
+
free,
|
|
201
|
+
used,
|
|
202
|
+
usagePercent: Math.round(used / total * 1e4) / 100
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/core/persister.ts
|
|
208
|
+
import * as fs from "fs";
|
|
209
|
+
import * as path from "path";
|
|
210
|
+
var DataPersister = class {
|
|
211
|
+
constructor(config, storageDir) {
|
|
212
|
+
this.buffer = [];
|
|
213
|
+
this.currentStream = null;
|
|
214
|
+
this.currentDataFile = null;
|
|
215
|
+
this.config = config;
|
|
216
|
+
this.storageDir = storageDir;
|
|
217
|
+
this.ensureDirectory(this.storageDir);
|
|
218
|
+
}
|
|
219
|
+
/** 获取存储目录 */
|
|
220
|
+
getStorageDir() {
|
|
221
|
+
return this.storageDir;
|
|
222
|
+
}
|
|
223
|
+
/** 创建新的会话数据文件 */
|
|
224
|
+
createSessionFiles(sessionId) {
|
|
225
|
+
const sessionDir = path.join(this.storageDir, sessionId);
|
|
226
|
+
this.ensureDirectory(sessionDir);
|
|
227
|
+
const dataFile = path.join(sessionDir, "snapshots.jsonl");
|
|
228
|
+
const metaFile = path.join(sessionDir, "meta.json");
|
|
229
|
+
this.closeStream();
|
|
230
|
+
this.currentDataFile = dataFile;
|
|
231
|
+
this.currentStream = fs.createWriteStream(dataFile, { flags: "a" });
|
|
232
|
+
return { dataFile, metaFile };
|
|
233
|
+
}
|
|
234
|
+
/** 写入快照数据 */
|
|
235
|
+
writeSnapshot(snapshot) {
|
|
236
|
+
this.buffer.push(snapshot);
|
|
237
|
+
if (this.buffer.length >= this.config.persistInterval) {
|
|
238
|
+
this.flush();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** 刷新缓冲区到磁盘 */
|
|
242
|
+
flush() {
|
|
243
|
+
if (this.buffer.length === 0 || !this.currentStream) return;
|
|
244
|
+
const lines = this.buffer.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
245
|
+
this.currentStream.write(lines);
|
|
246
|
+
this.buffer = [];
|
|
247
|
+
}
|
|
248
|
+
/** 保存会话元信息 */
|
|
249
|
+
saveSessionMeta(session) {
|
|
250
|
+
const metaFile = path.join(this.storageDir, session.id, "meta.json");
|
|
251
|
+
fs.writeFileSync(metaFile, JSON.stringify(session, null, 2), "utf-8");
|
|
252
|
+
this.updateSessionIndex(session);
|
|
253
|
+
}
|
|
254
|
+
/** 读取会话元信息 */
|
|
255
|
+
readSessionMeta(sessionId) {
|
|
256
|
+
const metaFile = path.join(this.storageDir, sessionId, "meta.json");
|
|
257
|
+
try {
|
|
258
|
+
const content = fs.readFileSync(metaFile, "utf-8");
|
|
259
|
+
return JSON.parse(content);
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** 获取所有会话列表 */
|
|
265
|
+
getSessions() {
|
|
266
|
+
const indexFile = path.join(this.storageDir, "sessions.json");
|
|
267
|
+
try {
|
|
268
|
+
const content = fs.readFileSync(indexFile, "utf-8");
|
|
269
|
+
const index = JSON.parse(content);
|
|
270
|
+
return index.sessions;
|
|
271
|
+
} catch {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** 读取会话的所有快照数据 */
|
|
276
|
+
readSessionSnapshots(sessionId) {
|
|
277
|
+
const dataFile = path.join(this.storageDir, sessionId, "snapshots.jsonl");
|
|
278
|
+
try {
|
|
279
|
+
const content = fs.readFileSync(dataFile, "utf-8");
|
|
280
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
281
|
+
return lines.map((line) => JSON.parse(line));
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** 关闭流并刷新缓冲区 */
|
|
287
|
+
close() {
|
|
288
|
+
this.flush();
|
|
289
|
+
this.closeStream();
|
|
290
|
+
}
|
|
291
|
+
/** 清理过期会话 */
|
|
292
|
+
cleanOldSessions() {
|
|
293
|
+
const sessions = this.getSessions();
|
|
294
|
+
if (sessions.length <= this.config.storage.maxSessions) return;
|
|
295
|
+
const toRemove = sessions.sort((a, b) => a.startTime - b.startTime).slice(0, sessions.length - this.config.storage.maxSessions);
|
|
296
|
+
for (const session of toRemove) {
|
|
297
|
+
const sessionDir = path.join(this.storageDir, session.id);
|
|
298
|
+
try {
|
|
299
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const remaining = sessions.filter((s) => !toRemove.includes(s));
|
|
304
|
+
this.saveSessionIndex(remaining);
|
|
305
|
+
}
|
|
306
|
+
/** 导出会话数据为单个 JSON 包 */
|
|
307
|
+
exportSession(sessionId) {
|
|
308
|
+
const meta = this.readSessionMeta(sessionId);
|
|
309
|
+
const snapshotsFile = path.join(this.storageDir, sessionId, "snapshots.jsonl");
|
|
310
|
+
const reportFile = path.join(this.storageDir, sessionId, "report.json");
|
|
311
|
+
let snapshots = "";
|
|
312
|
+
try {
|
|
313
|
+
snapshots = fs.readFileSync(snapshotsFile, "utf-8");
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
let report = null;
|
|
317
|
+
try {
|
|
318
|
+
report = fs.readFileSync(reportFile, "utf-8");
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
return { meta, snapshots, report };
|
|
322
|
+
}
|
|
323
|
+
/** 导入会话数据 */
|
|
324
|
+
importSession(data) {
|
|
325
|
+
const { meta, snapshots, report } = data;
|
|
326
|
+
const sessionDir = path.join(this.storageDir, meta.id);
|
|
327
|
+
this.ensureDirectory(sessionDir);
|
|
328
|
+
const snapshotsFile = path.join(sessionDir, "snapshots.jsonl");
|
|
329
|
+
fs.writeFileSync(snapshotsFile, snapshots, "utf-8");
|
|
330
|
+
meta.dataFile = snapshotsFile;
|
|
331
|
+
meta.metaFile = path.join(sessionDir, "meta.json");
|
|
332
|
+
fs.writeFileSync(meta.metaFile, JSON.stringify(meta, null, 2), "utf-8");
|
|
333
|
+
if (report) {
|
|
334
|
+
const reportFile = path.join(sessionDir, "report.json");
|
|
335
|
+
fs.writeFileSync(reportFile, report, "utf-8");
|
|
336
|
+
}
|
|
337
|
+
this.updateSessionIndex(meta);
|
|
338
|
+
return meta;
|
|
339
|
+
}
|
|
340
|
+
/** 删除指定会话 */
|
|
341
|
+
deleteSession(sessionId) {
|
|
342
|
+
const sessionDir = path.join(this.storageDir, sessionId);
|
|
343
|
+
try {
|
|
344
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
345
|
+
} catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
const sessions = this.getSessions().filter((s) => s.id !== sessionId);
|
|
349
|
+
this.saveSessionIndex(sessions);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
// ===== 私有方法 =====
|
|
353
|
+
closeStream() {
|
|
354
|
+
if (this.currentStream) {
|
|
355
|
+
this.currentStream.end();
|
|
356
|
+
this.currentStream = null;
|
|
357
|
+
this.currentDataFile = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
updateSessionIndex(session) {
|
|
361
|
+
const sessions = this.getSessions();
|
|
362
|
+
const existingIdx = sessions.findIndex((s) => s.id === session.id);
|
|
363
|
+
if (existingIdx >= 0) {
|
|
364
|
+
sessions[existingIdx] = session;
|
|
365
|
+
} else {
|
|
366
|
+
sessions.push(session);
|
|
367
|
+
}
|
|
368
|
+
this.saveSessionIndex(sessions);
|
|
369
|
+
}
|
|
370
|
+
saveSessionIndex(sessions) {
|
|
371
|
+
const indexFile = path.join(this.storageDir, "sessions.json");
|
|
372
|
+
const index = {
|
|
373
|
+
sessions,
|
|
374
|
+
lastUpdated: Date.now()
|
|
375
|
+
};
|
|
376
|
+
fs.writeFileSync(indexFile, JSON.stringify(index, null, 2), "utf-8");
|
|
377
|
+
}
|
|
378
|
+
ensureDirectory(dir) {
|
|
379
|
+
if (!fs.existsSync(dir)) {
|
|
380
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/core/utils.ts
|
|
386
|
+
function v4() {
|
|
387
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
388
|
+
const r = Math.random() * 16 | 0;
|
|
389
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
390
|
+
return v.toString(16);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function percentile(arr, p) {
|
|
394
|
+
if (arr.length === 0) return 0;
|
|
395
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
396
|
+
const idx = p / 100 * (sorted.length - 1);
|
|
397
|
+
const lower = Math.floor(idx);
|
|
398
|
+
const upper = Math.ceil(idx);
|
|
399
|
+
if (lower === upper) return sorted[lower];
|
|
400
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
|
|
401
|
+
}
|
|
402
|
+
function average(arr) {
|
|
403
|
+
if (arr.length === 0) return 0;
|
|
404
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
405
|
+
}
|
|
406
|
+
function linearRegression(values, timestamps) {
|
|
407
|
+
const n = values.length;
|
|
408
|
+
if (n < 2) return { slope: 0, r2: 0, intercept: values[0] || 0 };
|
|
409
|
+
const t0 = timestamps[0];
|
|
410
|
+
const xs = timestamps.map((t) => (t - t0) / 1e3);
|
|
411
|
+
const sumX = xs.reduce((a, b) => a + b, 0);
|
|
412
|
+
const sumY = values.reduce((a, b) => a + b, 0);
|
|
413
|
+
const sumXY = xs.reduce((sum, x, i) => sum + x * values[i], 0);
|
|
414
|
+
const sumX2 = xs.reduce((sum, x) => sum + x * x, 0);
|
|
415
|
+
const sumY2 = values.reduce((sum, y) => sum + y * y, 0);
|
|
416
|
+
const denominator = n * sumX2 - sumX * sumX;
|
|
417
|
+
if (denominator === 0) return { slope: 0, r2: 0, intercept: sumY / n };
|
|
418
|
+
const slope = (n * sumXY - sumX * sumY) / denominator;
|
|
419
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
420
|
+
const meanY = sumY / n;
|
|
421
|
+
const ssTotal = sumY2 - n * meanY * meanY;
|
|
422
|
+
const ssResidual = values.reduce((sum, y, i) => {
|
|
423
|
+
const predicted = intercept + slope * xs[i];
|
|
424
|
+
return sum + (y - predicted) ** 2;
|
|
425
|
+
}, 0);
|
|
426
|
+
const r2 = ssTotal === 0 ? 0 : 1 - ssResidual / ssTotal;
|
|
427
|
+
return { slope, r2, intercept };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/core/session.ts
|
|
431
|
+
var SessionManager = class {
|
|
432
|
+
constructor(persister) {
|
|
433
|
+
this.currentSession = null;
|
|
434
|
+
this.persister = persister;
|
|
435
|
+
}
|
|
436
|
+
/** 获取当前正在运行的会话 */
|
|
437
|
+
getCurrentSession() {
|
|
438
|
+
return this.currentSession;
|
|
439
|
+
}
|
|
440
|
+
/** 开始新会话 */
|
|
441
|
+
startSession(label, description) {
|
|
442
|
+
if (this.currentSession && this.currentSession.status === "running") {
|
|
443
|
+
this.endSession();
|
|
444
|
+
}
|
|
445
|
+
const sessionId = v4();
|
|
446
|
+
const { dataFile, metaFile } = this.persister.createSessionFiles(sessionId);
|
|
447
|
+
const session = {
|
|
448
|
+
id: sessionId,
|
|
449
|
+
label,
|
|
450
|
+
description,
|
|
451
|
+
startTime: Date.now(),
|
|
452
|
+
status: "running",
|
|
453
|
+
snapshotCount: 0,
|
|
454
|
+
dataFile,
|
|
455
|
+
metaFile
|
|
456
|
+
};
|
|
457
|
+
this.currentSession = session;
|
|
458
|
+
this.persister.saveSessionMeta(session);
|
|
459
|
+
return session;
|
|
460
|
+
}
|
|
461
|
+
/** 结束当前会话 */
|
|
462
|
+
endSession() {
|
|
463
|
+
if (!this.currentSession) return null;
|
|
464
|
+
this.currentSession.endTime = Date.now();
|
|
465
|
+
this.currentSession.duration = this.currentSession.endTime - this.currentSession.startTime;
|
|
466
|
+
this.currentSession.status = "completed";
|
|
467
|
+
this.persister.flush();
|
|
468
|
+
this.persister.saveSessionMeta(this.currentSession);
|
|
469
|
+
const session = { ...this.currentSession };
|
|
470
|
+
this.currentSession = null;
|
|
471
|
+
return session;
|
|
472
|
+
}
|
|
473
|
+
/** 增加当前会话的快照计数 */
|
|
474
|
+
incrementSnapshotCount() {
|
|
475
|
+
if (this.currentSession) {
|
|
476
|
+
this.currentSession.snapshotCount++;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** 获取所有会话 */
|
|
480
|
+
getSessions() {
|
|
481
|
+
return this.persister.getSessions();
|
|
482
|
+
}
|
|
483
|
+
/** 获取指定会话 */
|
|
484
|
+
getSession(sessionId) {
|
|
485
|
+
return this.persister.readSessionMeta(sessionId);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// src/core/anomaly.ts
|
|
490
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
491
|
+
var AnomalyDetector = class extends EventEmitter2 {
|
|
492
|
+
constructor(config) {
|
|
493
|
+
super();
|
|
494
|
+
this.snapshots = [];
|
|
495
|
+
this.timer = null;
|
|
496
|
+
this.maxWindowSize = 300;
|
|
497
|
+
// 保留最近 300 条(5 分钟 @1s间隔)
|
|
498
|
+
this.detectedAnomalies = [];
|
|
499
|
+
this.config = config;
|
|
500
|
+
this.builtinRules = this.createBuiltinRules();
|
|
501
|
+
}
|
|
502
|
+
/** 添加快照到检测窗口 */
|
|
503
|
+
addSnapshot(snapshot) {
|
|
504
|
+
this.snapshots.push(snapshot);
|
|
505
|
+
if (this.snapshots.length > this.maxWindowSize) {
|
|
506
|
+
this.snapshots.shift();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/** 获取所有检测到的异常 */
|
|
510
|
+
getAnomalies() {
|
|
511
|
+
return [...this.detectedAnomalies];
|
|
512
|
+
}
|
|
513
|
+
/** 清空异常记录 */
|
|
514
|
+
clearAnomalies() {
|
|
515
|
+
this.detectedAnomalies = [];
|
|
516
|
+
}
|
|
517
|
+
/** 开始定时检测 */
|
|
518
|
+
start() {
|
|
519
|
+
if (!this.config.anomaly.enabled || this.timer) return;
|
|
520
|
+
this.timer = setInterval(() => {
|
|
521
|
+
this.runDetection();
|
|
522
|
+
}, this.config.anomaly.checkInterval);
|
|
523
|
+
}
|
|
524
|
+
/** 停止检测 */
|
|
525
|
+
stop() {
|
|
526
|
+
if (this.timer) {
|
|
527
|
+
clearInterval(this.timer);
|
|
528
|
+
this.timer = null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/** 执行一次检测 */
|
|
532
|
+
runDetection() {
|
|
533
|
+
if (this.snapshots.length < 10) return;
|
|
534
|
+
const latest = this.snapshots[this.snapshots.length - 1];
|
|
535
|
+
const allRules = [...this.builtinRules, ...this.config.anomaly.rules];
|
|
536
|
+
for (const rule of allRules) {
|
|
537
|
+
if (!rule.enabled) continue;
|
|
538
|
+
try {
|
|
539
|
+
const anomaly = rule.detect(this.snapshots, latest);
|
|
540
|
+
if (anomaly) {
|
|
541
|
+
this.detectedAnomalies.push(anomaly);
|
|
542
|
+
this.emit("anomaly", anomaly);
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/** 创建内置检测规则 */
|
|
549
|
+
createBuiltinRules() {
|
|
550
|
+
return [
|
|
551
|
+
// 规则1:总内存持续增长
|
|
552
|
+
{
|
|
553
|
+
id: "continuous-growth",
|
|
554
|
+
name: "\u603B\u5185\u5B58\u6301\u7EED\u589E\u957F",
|
|
555
|
+
enabled: true,
|
|
556
|
+
detect: (snapshots) => {
|
|
557
|
+
if (snapshots.length < 60) return null;
|
|
558
|
+
const values = snapshots.map((s) => s.totalWorkingSetSize);
|
|
559
|
+
const timestamps = snapshots.map((s) => s.timestamp);
|
|
560
|
+
const { slope, r2 } = linearRegression(values, timestamps);
|
|
561
|
+
if (slope > 10 && r2 > 0.7) {
|
|
562
|
+
return {
|
|
563
|
+
id: v4(),
|
|
564
|
+
timestamp: Date.now(),
|
|
565
|
+
severity: r2 > 0.9 ? "critical" : "warning",
|
|
566
|
+
category: "memory-leak",
|
|
567
|
+
title: "\u603B\u5185\u5B58\u6301\u7EED\u589E\u957F",
|
|
568
|
+
description: `\u5185\u5B58\u4EE5 ${slope.toFixed(2)} KB/s \u7684\u901F\u7387\u6301\u7EED\u589E\u957F (R\xB2=${r2.toFixed(3)})`,
|
|
569
|
+
value: slope,
|
|
570
|
+
threshold: 10
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
// 规则2:内存突增(spike)
|
|
577
|
+
{
|
|
578
|
+
id: "memory-spike",
|
|
579
|
+
name: "\u5185\u5B58\u7A81\u589E",
|
|
580
|
+
enabled: true,
|
|
581
|
+
detect: (snapshots, latest) => {
|
|
582
|
+
if (snapshots.length < 10) return null;
|
|
583
|
+
const recentValues = snapshots.slice(-30).map((s) => s.totalWorkingSetSize);
|
|
584
|
+
const avg = average(recentValues);
|
|
585
|
+
const current = latest.totalWorkingSetSize;
|
|
586
|
+
if (avg > 0 && (current - avg) / avg > 0.5) {
|
|
587
|
+
return {
|
|
588
|
+
id: v4(),
|
|
589
|
+
timestamp: Date.now(),
|
|
590
|
+
severity: "warning",
|
|
591
|
+
category: "spike",
|
|
592
|
+
title: "\u5185\u5B58\u7A81\u589E",
|
|
593
|
+
description: `\u603B\u5185\u5B58\u4ECE ${Math.round(avg)} KB \u7A81\u589E\u5230 ${current} KB (+${((current - avg) / avg * 100).toFixed(1)}%)`,
|
|
594
|
+
value: current,
|
|
595
|
+
threshold: avg * 1.5
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
// 规则3:分离上下文检测
|
|
602
|
+
{
|
|
603
|
+
id: "detached-contexts",
|
|
604
|
+
name: "\u5206\u79BB\u4E0A\u4E0B\u6587",
|
|
605
|
+
enabled: true,
|
|
606
|
+
detect: (_snapshots, latest) => {
|
|
607
|
+
const detached = latest.mainProcessV8Detail?.numberOfDetachedContexts;
|
|
608
|
+
if (detached && detached > 0) {
|
|
609
|
+
return {
|
|
610
|
+
id: v4(),
|
|
611
|
+
timestamp: Date.now(),
|
|
612
|
+
severity: "critical",
|
|
613
|
+
category: "detached-context",
|
|
614
|
+
title: `\u68C0\u6D4B\u5230 ${detached} \u4E2A\u5206\u79BB\u7684 V8 \u4E0A\u4E0B\u6587`,
|
|
615
|
+
description: "\u5B58\u5728\u672A\u6B63\u786E\u9500\u6BC1\u7684 BrowserWindow \u6216 WebContents\uFF0C\u53EF\u80FD\u5BFC\u81F4\u5185\u5B58\u6CC4\u6F0F",
|
|
616
|
+
value: detached,
|
|
617
|
+
threshold: 0
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
// 规则4:V8 堆使用率过高
|
|
624
|
+
{
|
|
625
|
+
id: "heap-usage-high",
|
|
626
|
+
name: "V8 \u5806\u4F7F\u7528\u7387\u8FC7\u9AD8",
|
|
627
|
+
enabled: true,
|
|
628
|
+
detect: (_snapshots, latest) => {
|
|
629
|
+
const { heapUsed, heapTotal } = latest.mainProcessMemory;
|
|
630
|
+
if (heapTotal > 0) {
|
|
631
|
+
const usagePercent = heapUsed / heapTotal;
|
|
632
|
+
if (usagePercent > 0.85) {
|
|
633
|
+
return {
|
|
634
|
+
id: v4(),
|
|
635
|
+
timestamp: Date.now(),
|
|
636
|
+
severity: usagePercent > 0.95 ? "critical" : "warning",
|
|
637
|
+
category: "threshold",
|
|
638
|
+
title: `V8 \u5806\u4F7F\u7528\u7387 ${(usagePercent * 100).toFixed(1)}%`,
|
|
639
|
+
description: `\u4E3B\u8FDB\u7A0B V8 \u5806\u4F7F\u7528 ${Math.round(heapUsed / 1024 / 1024)} MB / ${Math.round(heapTotal / 1024 / 1024)} MB`,
|
|
640
|
+
value: usagePercent * 100,
|
|
641
|
+
threshold: 85
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// src/core/analyzer.ts
|
|
653
|
+
import * as os2 from "os";
|
|
654
|
+
var Analyzer = class {
|
|
655
|
+
/** 生成会话报告 */
|
|
656
|
+
generateReport(sessionId, label, description, startTime, endTime, snapshots, anomalies, dataFile) {
|
|
657
|
+
if (snapshots.length === 0) {
|
|
658
|
+
throw new Error("No snapshots to analyze");
|
|
659
|
+
}
|
|
660
|
+
const environment = this.collectEnvironment();
|
|
661
|
+
const summary = this.computeSummary(snapshots);
|
|
662
|
+
const suggestions = this.generateSuggestions(snapshots, summary, anomalies);
|
|
663
|
+
return {
|
|
664
|
+
sessionId,
|
|
665
|
+
label,
|
|
666
|
+
description,
|
|
667
|
+
startTime,
|
|
668
|
+
endTime,
|
|
669
|
+
duration: endTime - startTime,
|
|
670
|
+
environment,
|
|
671
|
+
summary,
|
|
672
|
+
anomalies,
|
|
673
|
+
suggestions,
|
|
674
|
+
dataFile
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/** 对比两个会话报告 */
|
|
678
|
+
compareReports(base, target) {
|
|
679
|
+
const overall = {
|
|
680
|
+
totalMemory: this.diffMetric(base.summary.totalMemory, target.summary.totalMemory),
|
|
681
|
+
browserMemory: this.diffMetric(base.summary.byProcessType.browser, target.summary.byProcessType.browser),
|
|
682
|
+
rendererMemory: this.diffMetricArrayAvg(
|
|
683
|
+
base.summary.byProcessType.renderer,
|
|
684
|
+
target.summary.byProcessType.renderer
|
|
685
|
+
),
|
|
686
|
+
gpuMemory: base.summary.byProcessType.gpu && target.summary.byProcessType.gpu ? this.diffMetric(base.summary.byProcessType.gpu, target.summary.byProcessType.gpu) : null
|
|
687
|
+
};
|
|
688
|
+
const v8Heap = {
|
|
689
|
+
heapUsed: this.diffMetric(base.summary.mainV8Heap.heapUsed, target.summary.mainV8Heap.heapUsed),
|
|
690
|
+
heapTotal: this.diffMetric(base.summary.mainV8Heap.heapTotal, target.summary.mainV8Heap.heapTotal),
|
|
691
|
+
external: this.diffMetric(base.summary.mainV8Heap.external, target.summary.mainV8Heap.external)
|
|
692
|
+
};
|
|
693
|
+
const trendChanges = this.compareTrends(base.summary.trends, target.summary.trends);
|
|
694
|
+
const regressions = this.findRegressions(overall, v8Heap);
|
|
695
|
+
const improvements = this.findImprovements(overall, v8Heap);
|
|
696
|
+
const { verdict, verdictReason } = this.determineVerdict(regressions, overall);
|
|
697
|
+
return {
|
|
698
|
+
base: { sessionId: base.sessionId, label: base.label },
|
|
699
|
+
target: { sessionId: target.sessionId, label: target.label },
|
|
700
|
+
overall,
|
|
701
|
+
v8Heap,
|
|
702
|
+
trendChanges,
|
|
703
|
+
regressions,
|
|
704
|
+
improvements,
|
|
705
|
+
verdict,
|
|
706
|
+
verdictReason
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
// ===== 私有方法 =====
|
|
710
|
+
collectEnvironment() {
|
|
711
|
+
const cpus2 = os2.cpus();
|
|
712
|
+
return {
|
|
713
|
+
electronVersion: process.versions.electron || "unknown",
|
|
714
|
+
chromeVersion: process.versions.chrome || "unknown",
|
|
715
|
+
nodeVersion: process.versions.node || "unknown",
|
|
716
|
+
platform: process.platform,
|
|
717
|
+
arch: process.arch,
|
|
718
|
+
totalSystemMemory: os2.totalmem(),
|
|
719
|
+
cpuModel: cpus2.length > 0 ? cpus2[0].model : "unknown",
|
|
720
|
+
cpuCores: cpus2.length
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
computeSummary(snapshots) {
|
|
724
|
+
const timestamps = snapshots.map((s) => s.timestamp);
|
|
725
|
+
const processCounts = snapshots.map((s) => s.processes.length);
|
|
726
|
+
const totalMemoryValues = snapshots.map((s) => s.totalWorkingSetSize);
|
|
727
|
+
const browserValues = snapshots.map(
|
|
728
|
+
(s) => s.processes.filter((p) => p.type === "Browser").reduce((sum, p) => sum + p.memory.workingSetSize, 0)
|
|
729
|
+
);
|
|
730
|
+
const rendererSummaries = this.computeRendererSummaries(snapshots);
|
|
731
|
+
const gpuValues = snapshots.map(
|
|
732
|
+
(s) => s.processes.filter((p) => p.type === "GPU").reduce((sum, p) => sum + p.memory.workingSetSize, 0)
|
|
733
|
+
);
|
|
734
|
+
const hasGpu = gpuValues.some((v) => v > 0);
|
|
735
|
+
const utilityValues = snapshots.map(
|
|
736
|
+
(s) => s.processes.filter((p) => p.type === "Utility").reduce((sum, p) => sum + p.memory.workingSetSize, 0)
|
|
737
|
+
);
|
|
738
|
+
const hasUtility = utilityValues.some((v) => v > 0);
|
|
739
|
+
const heapUsedValues = snapshots.map((s) => s.mainProcessMemory.heapUsed);
|
|
740
|
+
const heapTotalValues = snapshots.map((s) => s.mainProcessMemory.heapTotal);
|
|
741
|
+
const externalValues = snapshots.map((s) => s.mainProcessMemory.external);
|
|
742
|
+
const arrayBufferValues = snapshots.map((s) => s.mainProcessMemory.arrayBuffers);
|
|
743
|
+
const rendererTotalValues = snapshots.map(
|
|
744
|
+
(s) => s.processes.filter((p) => p.type === "Tab" && !p.isMonitorProcess).reduce((sum, p) => sum + p.memory.workingSetSize, 0)
|
|
745
|
+
);
|
|
746
|
+
return {
|
|
747
|
+
totalProcesses: {
|
|
748
|
+
min: Math.min(...processCounts),
|
|
749
|
+
max: Math.max(...processCounts),
|
|
750
|
+
avg: Math.round(average(processCounts))
|
|
751
|
+
},
|
|
752
|
+
totalMemory: this.computeMetricSummary(totalMemoryValues),
|
|
753
|
+
byProcessType: {
|
|
754
|
+
browser: this.computeMetricSummary(browserValues),
|
|
755
|
+
renderer: rendererSummaries,
|
|
756
|
+
gpu: hasGpu ? this.computeMetricSummary(gpuValues) : null,
|
|
757
|
+
utility: hasUtility ? this.computeMetricSummary(utilityValues) : null
|
|
758
|
+
},
|
|
759
|
+
mainV8Heap: {
|
|
760
|
+
heapUsed: this.computeMetricSummary(heapUsedValues),
|
|
761
|
+
heapTotal: this.computeMetricSummary(heapTotalValues),
|
|
762
|
+
external: this.computeMetricSummary(externalValues),
|
|
763
|
+
arrayBuffers: this.computeMetricSummary(arrayBufferValues)
|
|
764
|
+
},
|
|
765
|
+
trends: {
|
|
766
|
+
totalMemory: this.computeTrend(totalMemoryValues, timestamps),
|
|
767
|
+
browserMemory: this.computeTrend(browserValues, timestamps),
|
|
768
|
+
rendererMemory: this.computeTrend(rendererTotalValues, timestamps)
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
computeRendererSummaries(snapshots) {
|
|
773
|
+
const allPids = /* @__PURE__ */ new Set();
|
|
774
|
+
for (const snapshot of snapshots) {
|
|
775
|
+
for (const p of snapshot.processes) {
|
|
776
|
+
if (p.type === "Tab" && !p.isMonitorProcess) {
|
|
777
|
+
allPids.add(p.pid);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const summaries = [];
|
|
782
|
+
for (const pid of allPids) {
|
|
783
|
+
const values = snapshots.map((s) => {
|
|
784
|
+
const proc = s.processes.find((p) => p.pid === pid);
|
|
785
|
+
return proc ? proc.memory.workingSetSize : null;
|
|
786
|
+
}).filter((v) => v !== null);
|
|
787
|
+
if (values.length > 0) {
|
|
788
|
+
summaries.push(this.computeMetricSummary(values));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return summaries;
|
|
792
|
+
}
|
|
793
|
+
computeMetricSummary(values) {
|
|
794
|
+
if (values.length === 0) {
|
|
795
|
+
return { initial: 0, final: 0, min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0, delta: 0, deltaPercent: 0 };
|
|
796
|
+
}
|
|
797
|
+
const initial = values[0];
|
|
798
|
+
const final = values[values.length - 1];
|
|
799
|
+
const delta = final - initial;
|
|
800
|
+
const deltaPercent = initial !== 0 ? delta / initial * 100 : 0;
|
|
801
|
+
return {
|
|
802
|
+
initial,
|
|
803
|
+
final,
|
|
804
|
+
min: Math.min(...values),
|
|
805
|
+
max: Math.max(...values),
|
|
806
|
+
avg: Math.round(average(values)),
|
|
807
|
+
p50: Math.round(percentile(values, 50)),
|
|
808
|
+
p95: Math.round(percentile(values, 95)),
|
|
809
|
+
p99: Math.round(percentile(values, 99)),
|
|
810
|
+
delta: Math.round(delta),
|
|
811
|
+
deltaPercent: Math.round(deltaPercent * 100) / 100
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
computeTrend(values, timestamps) {
|
|
815
|
+
if (values.length < 10) {
|
|
816
|
+
return { slope: 0, r2: 0, direction: "stable", confidence: "low" };
|
|
817
|
+
}
|
|
818
|
+
const { slope, r2 } = linearRegression(values, timestamps);
|
|
819
|
+
let direction = "stable";
|
|
820
|
+
if (slope > 1 && r2 > 0.3) direction = "growing";
|
|
821
|
+
else if (slope < -1 && r2 > 0.3) direction = "shrinking";
|
|
822
|
+
let confidence = "low";
|
|
823
|
+
if (r2 > 0.8) confidence = "high";
|
|
824
|
+
else if (r2 > 0.5) confidence = "medium";
|
|
825
|
+
return { slope, r2, direction, confidence };
|
|
826
|
+
}
|
|
827
|
+
generateSuggestions(snapshots, summary, _anomalies) {
|
|
828
|
+
const suggestions = [];
|
|
829
|
+
const latest = snapshots[snapshots.length - 1];
|
|
830
|
+
if (latest.mainProcessV8Detail?.numberOfDetachedContexts > 0) {
|
|
831
|
+
suggestions.push({
|
|
832
|
+
id: "detached-contexts",
|
|
833
|
+
severity: "critical",
|
|
834
|
+
category: "memory-leak",
|
|
835
|
+
title: "\u68C0\u6D4B\u5230\u5206\u79BB\u7684 V8 \u4E0A\u4E0B\u6587 (Detached Contexts)",
|
|
836
|
+
description: `\u53D1\u73B0 ${latest.mainProcessV8Detail.numberOfDetachedContexts} \u4E2A\u5206\u79BB\u4E0A\u4E0B\u6587\uFF0C\u901A\u5E38\u610F\u5473\u7740\u5B58\u5728\u672A\u6B63\u786E\u9500\u6BC1\u7684 BrowserWindow \u6216 WebContents \u5B9E\u4F8B\u3002`,
|
|
837
|
+
suggestions: [
|
|
838
|
+
"\u68C0\u67E5\u6240\u6709 BrowserWindow \u662F\u5426\u5728\u5173\u95ED\u65F6\u8C03\u7528\u4E86 destroy()",
|
|
839
|
+
"\u68C0\u67E5\u662F\u5426\u6709\u95ED\u5305\u6301\u6709\u5DF2\u5173\u95ED\u7A97\u53E3\u7684 webContents \u5F15\u7528",
|
|
840
|
+
'\u4F7F\u7528 Chrome DevTools Memory \u9762\u677F\u505A\u5806\u5FEB\u7167\uFF0C\u641C\u7D22 "Detached" \u5173\u952E\u5B57',
|
|
841
|
+
"\u68C0\u67E5 ipcMain.on \u76D1\u542C\u5668\u662F\u5426\u5728\u7A97\u53E3\u5173\u95ED\u540E\u6B63\u786E\u79FB\u9664"
|
|
842
|
+
],
|
|
843
|
+
relatedCode: [
|
|
844
|
+
'win.on("closed", () => { win = null })',
|
|
845
|
+
"win.destroy() // \u800C\u4E0D\u4EC5\u4EC5\u662F win.close()"
|
|
846
|
+
]
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
if (summary.trends.browserMemory.direction === "growing" && summary.trends.browserMemory.confidence === "high") {
|
|
850
|
+
suggestions.push({
|
|
851
|
+
id: "main-process-leak",
|
|
852
|
+
severity: "warning",
|
|
853
|
+
category: "memory-leak",
|
|
854
|
+
title: "\u4E3B\u8FDB\u7A0B\u5185\u5B58\u5B58\u5728\u6301\u7EED\u589E\u957F\u8D8B\u52BF",
|
|
855
|
+
description: `\u4E3B\u8FDB\u7A0B\u5185\u5B58\u4EE5 ${summary.trends.browserMemory.slope.toFixed(2)} KB/s \u7684\u901F\u7387\u589E\u957F (R\xB2=${summary.trends.browserMemory.r2.toFixed(3)})`,
|
|
856
|
+
suggestions: [
|
|
857
|
+
"\u68C0\u67E5\u4E3B\u8FDB\u7A0B\u4E2D\u662F\u5426\u6709\u672A\u6E05\u7406\u7684 setInterval/setTimeout",
|
|
858
|
+
"\u68C0\u67E5 ipcMain.on \u662F\u5426\u5B58\u5728\u91CD\u590D\u6CE8\u518C",
|
|
859
|
+
"\u68C0\u67E5\u662F\u5426\u6709\u6301\u7EED\u589E\u957F\u7684 Map/Set/Array \u7F13\u5B58\u672A\u8BBE\u7F6E\u4E0A\u9650",
|
|
860
|
+
"\u68C0\u67E5 EventEmitter \u76D1\u542C\u5668\u662F\u5426\u6B63\u786E\u79FB\u9664",
|
|
861
|
+
"\u8FD0\u884C --expose-gc \u5E76\u624B\u52A8\u89E6\u53D1 GC\uFF0C\u89C2\u5BDF\u5185\u5B58\u662F\u5426\u56DE\u843D"
|
|
862
|
+
]
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const highRenderers = summary.byProcessType.renderer.filter((r) => r.max > 300 * 1024);
|
|
866
|
+
if (highRenderers.length > 0) {
|
|
867
|
+
suggestions.push({
|
|
868
|
+
id: "renderer-memory-high",
|
|
869
|
+
severity: "warning",
|
|
870
|
+
category: "optimization",
|
|
871
|
+
title: "\u6E32\u67D3\u8FDB\u7A0B\u5185\u5B58\u5360\u7528\u8FC7\u9AD8",
|
|
872
|
+
description: `\u6709 ${highRenderers.length} \u4E2A\u6E32\u67D3\u8FDB\u7A0B\u5185\u5B58\u5CF0\u503C\u8D85\u8FC7 300MB`,
|
|
873
|
+
suggestions: [
|
|
874
|
+
"\u68C0\u67E5\u662F\u5426\u52A0\u8F7D\u4E86\u8FC7\u5927\u7684\u56FE\u7247\u8D44\u6E90\uFF08\u8003\u8651\u61D2\u52A0\u8F7D/\u538B\u7F29\uFF09",
|
|
875
|
+
"\u68C0\u67E5 DOM \u8282\u70B9\u6570\u91CF\uFF08\u8D85\u8FC7 1500 \u4E2A\u8282\u70B9\u4F1A\u663E\u8457\u589E\u52A0\u5185\u5B58\uFF09",
|
|
876
|
+
"\u68C0\u67E5\u662F\u5426\u6709\u5927\u91CF\u672A\u9500\u6BC1\u7684 React \u7EC4\u4EF6\u5B9E\u4F8B",
|
|
877
|
+
"\u8003\u8651\u4F7F\u7528\u865A\u62DF\u5217\u8868\uFF08Virtual List\uFF09\u66FF\u4EE3\u957F\u5217\u8868",
|
|
878
|
+
"\u68C0\u67E5 Canvas/WebGL \u8D44\u6E90\u662F\u5426\u6B63\u786E\u91CA\u653E"
|
|
879
|
+
]
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
const { heapUsed, heapTotal } = summary.mainV8Heap;
|
|
883
|
+
if (heapTotal.avg > 0 && heapUsed.avg / heapTotal.avg > 0.8) {
|
|
884
|
+
suggestions.push({
|
|
885
|
+
id: "gc-ineffective",
|
|
886
|
+
severity: "warning",
|
|
887
|
+
category: "memory-leak",
|
|
888
|
+
title: "V8 \u5806\u4F7F\u7528\u7387\u957F\u671F\u504F\u9AD8 (>80%)",
|
|
889
|
+
description: "\u5806\u4F7F\u7528\u7387\u957F\u671F\u8D85\u8FC7 80%\uFF0CGC \u65E0\u6CD5\u6709\u6548\u91CA\u653E\u5185\u5B58\uFF0C\u7591\u4F3C\u5B58\u5728\u5185\u5B58\u6CC4\u6F0F",
|
|
890
|
+
suggestions: [
|
|
891
|
+
"\u5BFC\u51FA\u5806\u5FEB\u7167 (Heap Snapshot)\uFF0C\u4F7F\u7528 Chrome DevTools \u5206\u6790\u5BF9\u8C61\u7559\u5B58",
|
|
892
|
+
'\u5BF9\u6BD4\u4E24\u4E2A\u65F6\u95F4\u70B9\u7684\u5806\u5FEB\u7167\uFF0C\u67E5\u627E "Allocated between snapshots" \u4E2D\u7684\u6CC4\u6F0F\u5BF9\u8C61',
|
|
893
|
+
"\u68C0\u67E5 Event Listeners \u662F\u5426\u6B63\u786E\u6E05\u7406",
|
|
894
|
+
"\u68C0\u67E5 Promise \u94FE\u662F\u5426\u6709\u672A\u5904\u7406\u7684 rejection \u5BFC\u81F4\u5F15\u7528\u672A\u91CA\u653E"
|
|
895
|
+
]
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
if (summary.mainV8Heap.arrayBuffers.avg > 50 * 1024 * 1024) {
|
|
899
|
+
suggestions.push({
|
|
900
|
+
id: "arraybuffer-high",
|
|
901
|
+
severity: "info",
|
|
902
|
+
category: "optimization",
|
|
903
|
+
title: "ArrayBuffer \u5185\u5B58\u5360\u7528\u504F\u9AD8",
|
|
904
|
+
description: "ArrayBuffer \u5E73\u5747\u5360\u7528\u8D85\u8FC7 50MB",
|
|
905
|
+
suggestions: [
|
|
906
|
+
"\u68C0\u67E5 Buffer.alloc / Buffer.from \u7684\u4F7F\u7528\uFF0C\u786E\u4FDD\u7528\u5B8C\u540E\u4E0D\u518D\u6301\u6709\u5F15\u7528",
|
|
907
|
+
"\u5982\u679C\u4F7F\u7528 IPC \u4F20\u8F93\u5927\u6570\u636E\uFF0C\u8003\u8651\u5206\u7247\u4F20\u8F93\u6216\u4F7F\u7528 MessagePort",
|
|
908
|
+
"\u68C0\u67E5 Blob/File \u5BF9\u8C61\u662F\u5426\u53CA\u65F6\u91CA\u653E"
|
|
909
|
+
]
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
if (summary.totalProcesses.max > 10) {
|
|
913
|
+
suggestions.push({
|
|
914
|
+
id: "too-many-processes",
|
|
915
|
+
severity: "warning",
|
|
916
|
+
category: "architecture",
|
|
917
|
+
title: `\u8FDB\u7A0B\u6570\u91CF\u504F\u591A (\u6700\u9AD8 ${summary.totalProcesses.max} \u4E2A)`,
|
|
918
|
+
description: "\u8FC7\u591A\u7684\u8FDB\u7A0B\u4F1A\u663E\u8457\u589E\u52A0\u5185\u5B58\u5F00\u9500",
|
|
919
|
+
suggestions: [
|
|
920
|
+
"\u68C0\u67E5\u662F\u5426\u521B\u5EFA\u4E86\u4E0D\u5FC5\u8981\u7684 BrowserWindow",
|
|
921
|
+
"\u8003\u8651\u590D\u7528\u7A97\u53E3\u800C\u975E\u6BCF\u6B21\u521B\u5EFA\u65B0\u7A97\u53E3",
|
|
922
|
+
"\u4F7F\u7528 webContents.setBackgroundThrottling(true) \u51CF\u5C11\u540E\u53F0\u8FDB\u7A0B\u5F00\u9500"
|
|
923
|
+
]
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
if (latest.mainProcessV8Detail?.heapSpaces) {
|
|
927
|
+
const oldSpace = latest.mainProcessV8Detail.heapSpaces.find((s) => s.name === "old_space");
|
|
928
|
+
const totalUsed = latest.mainProcessV8Detail.heapSpaces.reduce((sum, s) => sum + s.usedSize, 0);
|
|
929
|
+
if (oldSpace && totalUsed > 0 && oldSpace.usedSize / totalUsed > 0.85) {
|
|
930
|
+
suggestions.push({
|
|
931
|
+
id: "old-space-dominant",
|
|
932
|
+
severity: "info",
|
|
933
|
+
category: "optimization",
|
|
934
|
+
title: "V8 old_space \u5360\u6BD4\u8D85\u8FC7 85%",
|
|
935
|
+
description: "\u5927\u91CF\u5BF9\u8C61\u5B58\u6D3B\u5230 old generation\uFF0C\u53EF\u80FD\u5B58\u5728\u957F\u751F\u547D\u5468\u671F\u7684\u5927\u5BF9\u8C61\u6216\u7F13\u5B58\u672A\u56DE\u6536",
|
|
936
|
+
suggestions: [
|
|
937
|
+
"\u4F7F\u7528\u5806\u5FEB\u7167\u5206\u6790 old_space \u4E2D\u7684\u5927\u5BF9\u8C61",
|
|
938
|
+
"\u68C0\u67E5\u5168\u5C40\u7F13\u5B58\u662F\u5426\u8BBE\u7F6E\u4E86\u8FC7\u671F\u7B56\u7565\u6216\u5BB9\u91CF\u4E0A\u9650",
|
|
939
|
+
"\u8003\u8651\u4F7F\u7528 WeakMap/WeakRef \u66FF\u4EE3\u5F3A\u5F15\u7528\u7F13\u5B58",
|
|
940
|
+
"\u68C0\u67E5\u95ED\u5305\u662F\u5426\u610F\u5916\u6301\u6709\u5927\u91CF\u5916\u90E8\u53D8\u91CF"
|
|
941
|
+
]
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return suggestions;
|
|
946
|
+
}
|
|
947
|
+
diffMetric(base, target) {
|
|
948
|
+
const delta = target.avg - base.avg;
|
|
949
|
+
const deltaPercent = base.avg !== 0 ? delta / base.avg * 100 : 0;
|
|
950
|
+
let status = "unchanged";
|
|
951
|
+
if (deltaPercent > 3) status = "degraded";
|
|
952
|
+
else if (deltaPercent < -3) status = "improved";
|
|
953
|
+
let severity;
|
|
954
|
+
if (Math.abs(deltaPercent) > 15) severity = "critical";
|
|
955
|
+
else if (Math.abs(deltaPercent) > 5) severity = "major";
|
|
956
|
+
else severity = "minor";
|
|
957
|
+
return {
|
|
958
|
+
base: base.avg,
|
|
959
|
+
target: target.avg,
|
|
960
|
+
delta: Math.round(delta),
|
|
961
|
+
deltaPercent: Math.round(deltaPercent * 100) / 100,
|
|
962
|
+
status,
|
|
963
|
+
severity
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
diffMetricArrayAvg(baseArr, targetArr) {
|
|
967
|
+
const baseAvg = baseArr.length > 0 ? average(baseArr.map((s) => s.avg)) : 0;
|
|
968
|
+
const targetAvg = targetArr.length > 0 ? average(targetArr.map((s) => s.avg)) : 0;
|
|
969
|
+
const baseSummary = {
|
|
970
|
+
initial: 0,
|
|
971
|
+
final: 0,
|
|
972
|
+
min: 0,
|
|
973
|
+
max: 0,
|
|
974
|
+
avg: baseAvg,
|
|
975
|
+
p50: 0,
|
|
976
|
+
p95: 0,
|
|
977
|
+
p99: 0,
|
|
978
|
+
delta: 0,
|
|
979
|
+
deltaPercent: 0
|
|
980
|
+
};
|
|
981
|
+
const targetSummary = {
|
|
982
|
+
initial: 0,
|
|
983
|
+
final: 0,
|
|
984
|
+
min: 0,
|
|
985
|
+
max: 0,
|
|
986
|
+
avg: targetAvg,
|
|
987
|
+
p50: 0,
|
|
988
|
+
p95: 0,
|
|
989
|
+
p99: 0,
|
|
990
|
+
delta: 0,
|
|
991
|
+
deltaPercent: 0
|
|
992
|
+
};
|
|
993
|
+
return this.diffMetric(baseSummary, targetSummary);
|
|
994
|
+
}
|
|
995
|
+
compareTrends(baseTrends, targetTrends) {
|
|
996
|
+
const metrics = ["totalMemory", "browserMemory", "rendererMemory"];
|
|
997
|
+
return metrics.map((metric) => {
|
|
998
|
+
const baseSlope = baseTrends[metric].slope;
|
|
999
|
+
const targetSlope = targetTrends[metric].slope;
|
|
1000
|
+
let change = "unchanged";
|
|
1001
|
+
if (targetSlope > baseSlope + 1) change = "degraded";
|
|
1002
|
+
else if (targetSlope < baseSlope - 1) change = "improved";
|
|
1003
|
+
return { metric, baseSlope, targetSlope, change };
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
findRegressions(overall, v8Heap) {
|
|
1007
|
+
const regressions = [];
|
|
1008
|
+
const checks = [
|
|
1009
|
+
{ metric: "\u603B\u5185\u5B58", diff: overall.totalMemory, warnThreshold: 5, failThreshold: 15 },
|
|
1010
|
+
{ metric: "\u4E3B\u8FDB\u7A0B\u5185\u5B58", diff: overall.browserMemory, warnThreshold: 10, failThreshold: 25 },
|
|
1011
|
+
{ metric: "\u6E32\u67D3\u8FDB\u7A0B\u5185\u5B58", diff: overall.rendererMemory, warnThreshold: 10, failThreshold: 25 },
|
|
1012
|
+
{ metric: "V8 Heap Used", diff: v8Heap.heapUsed, warnThreshold: 10, failThreshold: 30 }
|
|
1013
|
+
];
|
|
1014
|
+
for (const check of checks) {
|
|
1015
|
+
if (check.diff.deltaPercent > check.warnThreshold) {
|
|
1016
|
+
regressions.push({
|
|
1017
|
+
metric: check.metric,
|
|
1018
|
+
description: `${check.metric}\u589E\u957F ${check.diff.deltaPercent.toFixed(1)}%`,
|
|
1019
|
+
baseValue: check.diff.base,
|
|
1020
|
+
targetValue: check.diff.target,
|
|
1021
|
+
deltaPercent: check.diff.deltaPercent,
|
|
1022
|
+
severity: check.diff.deltaPercent > check.failThreshold ? "critical" : "major",
|
|
1023
|
+
suggestion: `${check.metric}\u589E\u957F\u8D85\u8FC7\u9884\u671F\uFF0C\u5EFA\u8BAE\u68C0\u67E5\u65B0\u589E\u4EE3\u7801\u4E2D\u7684\u5185\u5B58\u4F7F\u7528`
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return regressions;
|
|
1028
|
+
}
|
|
1029
|
+
findImprovements(overall, v8Heap) {
|
|
1030
|
+
const improvements = [];
|
|
1031
|
+
const checks = [
|
|
1032
|
+
{ metric: "\u603B\u5185\u5B58", diff: overall.totalMemory },
|
|
1033
|
+
{ metric: "\u4E3B\u8FDB\u7A0B\u5185\u5B58", diff: overall.browserMemory },
|
|
1034
|
+
{ metric: "V8 Heap Used", diff: v8Heap.heapUsed }
|
|
1035
|
+
];
|
|
1036
|
+
for (const check of checks) {
|
|
1037
|
+
if (check.diff.deltaPercent < -3) {
|
|
1038
|
+
improvements.push({
|
|
1039
|
+
metric: check.metric,
|
|
1040
|
+
description: `${check.metric}\u51CF\u5C11 ${Math.abs(check.diff.deltaPercent).toFixed(1)}%`,
|
|
1041
|
+
baseValue: check.diff.base,
|
|
1042
|
+
targetValue: check.diff.target,
|
|
1043
|
+
deltaPercent: check.diff.deltaPercent
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return improvements;
|
|
1048
|
+
}
|
|
1049
|
+
determineVerdict(regressions, overall) {
|
|
1050
|
+
const critical = regressions.filter((r) => r.severity === "critical");
|
|
1051
|
+
const major = regressions.filter((r) => r.severity === "major");
|
|
1052
|
+
if (critical.length > 0) {
|
|
1053
|
+
return {
|
|
1054
|
+
verdict: "fail",
|
|
1055
|
+
verdictReason: `\u5B58\u5728 ${critical.length} \u9879\u4E25\u91CD\u52A3\u5316\uFF1A${critical.map((r) => r.metric).join("\u3001")}`
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
if (major.length > 0 || overall.totalMemory.deltaPercent > 5) {
|
|
1059
|
+
return {
|
|
1060
|
+
verdict: "warn",
|
|
1061
|
+
verdictReason: `\u5B58\u5728 ${major.length} \u9879\u52A3\u5316\uFF0C\u603B\u5185\u5B58\u53D8\u5316 ${overall.totalMemory.deltaPercent.toFixed(1)}%`
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
verdict: "pass",
|
|
1066
|
+
verdictReason: "\u6240\u6709\u5185\u5B58\u6307\u6807\u5728\u6B63\u5E38\u8303\u56F4\u5185"
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// src/core/dashboard.ts
|
|
1072
|
+
import { BrowserWindow as BrowserWindow2 } from "electron";
|
|
1073
|
+
import * as path2 from "path";
|
|
1074
|
+
var DashboardManager = class {
|
|
1075
|
+
constructor(config) {
|
|
1076
|
+
this.window = null;
|
|
1077
|
+
this.config = config;
|
|
1078
|
+
}
|
|
1079
|
+
/** 获取面板窗口 */
|
|
1080
|
+
getWindow() {
|
|
1081
|
+
return this.window;
|
|
1082
|
+
}
|
|
1083
|
+
/** 获取面板 webContents ID */
|
|
1084
|
+
getWebContentsId() {
|
|
1085
|
+
if (this.window && !this.window.isDestroyed()) {
|
|
1086
|
+
return this.window.webContents.id;
|
|
1087
|
+
}
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
/** 打开监控面板 */
|
|
1091
|
+
open() {
|
|
1092
|
+
if (this.window && !this.window.isDestroyed()) {
|
|
1093
|
+
this.window.focus();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const preloadPath = path2.join(__dirname, "dashboard-preload.js");
|
|
1097
|
+
this.window = new BrowserWindow2({
|
|
1098
|
+
width: this.config.dashboard.width,
|
|
1099
|
+
height: this.config.dashboard.height,
|
|
1100
|
+
title: "Electron Memory Monitor",
|
|
1101
|
+
alwaysOnTop: this.config.dashboard.alwaysOnTop,
|
|
1102
|
+
webPreferences: {
|
|
1103
|
+
preload: preloadPath,
|
|
1104
|
+
contextIsolation: true,
|
|
1105
|
+
nodeIntegration: false
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
const uiPath = path2.join(__dirname, "ui", "index.html");
|
|
1109
|
+
this.window.loadFile(uiPath);
|
|
1110
|
+
this.window.on("closed", () => {
|
|
1111
|
+
this.window = null;
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
/** 关闭监控面板 */
|
|
1115
|
+
close() {
|
|
1116
|
+
if (this.window && !this.window.isDestroyed()) {
|
|
1117
|
+
this.window.close();
|
|
1118
|
+
this.window = null;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/** 销毁面板 */
|
|
1122
|
+
destroy() {
|
|
1123
|
+
if (this.window && !this.window.isDestroyed()) {
|
|
1124
|
+
this.window.destroy();
|
|
1125
|
+
this.window = null;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
// src/ipc/main-handler.ts
|
|
1131
|
+
import { ipcMain } from "electron";
|
|
1132
|
+
|
|
1133
|
+
// src/ipc/channels.ts
|
|
1134
|
+
var IPC_CHANNELS = {
|
|
1135
|
+
// === 数据推送(主进程 → 监控面板)===
|
|
1136
|
+
SNAPSHOT: "emm:snapshot",
|
|
1137
|
+
ANOMALY: "emm:anomaly",
|
|
1138
|
+
// === 会话控制(面板 → 主进程)===
|
|
1139
|
+
SESSION_START: "emm:session:start",
|
|
1140
|
+
SESSION_STOP: "emm:session:stop",
|
|
1141
|
+
SESSION_LIST: "emm:session:list",
|
|
1142
|
+
SESSION_REPORT: "emm:session:report",
|
|
1143
|
+
SESSION_COMPARE: "emm:session:compare",
|
|
1144
|
+
// === 数据查询(面板 → 主进程)===
|
|
1145
|
+
SESSION_SNAPSHOTS: "emm:session:snapshots",
|
|
1146
|
+
// === 工具操作(面板 → 主进程)===
|
|
1147
|
+
TRIGGER_GC: "emm:gc",
|
|
1148
|
+
HEAP_SNAPSHOT: "emm:heap-snapshot",
|
|
1149
|
+
MARK: "emm:mark",
|
|
1150
|
+
CONFIG_UPDATE: "emm:config:update",
|
|
1151
|
+
GET_CONFIG: "emm:config:get",
|
|
1152
|
+
GET_SESSIONS: "emm:sessions:get",
|
|
1153
|
+
// === 导入导出(面板 → 主进程)===
|
|
1154
|
+
SESSION_EXPORT: "emm:session:export",
|
|
1155
|
+
SESSION_IMPORT: "emm:session:import",
|
|
1156
|
+
SESSION_DELETE: "emm:session:delete",
|
|
1157
|
+
// === 渲染进程上报(可选)===
|
|
1158
|
+
RENDERER_REPORT: "emm:renderer:report",
|
|
1159
|
+
RENDERER_REQUEST: "emm:renderer:request"
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// src/ipc/main-handler.ts
|
|
1163
|
+
var IPCMainHandler = class {
|
|
1164
|
+
constructor(monitor) {
|
|
1165
|
+
this.monitor = monitor;
|
|
1166
|
+
}
|
|
1167
|
+
/** 注册所有 IPC handlers */
|
|
1168
|
+
register() {
|
|
1169
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_START, (_event, args) => {
|
|
1170
|
+
return this.monitor.startSession(args.label, args.description);
|
|
1171
|
+
});
|
|
1172
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
|
|
1173
|
+
return this.monitor.stopSession();
|
|
1174
|
+
});
|
|
1175
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
|
|
1176
|
+
return this.monitor.getSessions();
|
|
1177
|
+
});
|
|
1178
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
|
|
1179
|
+
return this.monitor.getSessionReport(sessionId);
|
|
1180
|
+
});
|
|
1181
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_COMPARE, async (_event, args) => {
|
|
1182
|
+
return this.monitor.compareSessions(args.baseId, args.targetId);
|
|
1183
|
+
});
|
|
1184
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_SNAPSHOTS, async (_event, args) => {
|
|
1185
|
+
return this.monitor.getSessionSnapshots(args.sessionId, args.startTime, args.endTime, args.maxPoints);
|
|
1186
|
+
});
|
|
1187
|
+
ipcMain.handle(IPC_CHANNELS.TRIGGER_GC, async () => {
|
|
1188
|
+
return this.monitor.triggerGC();
|
|
1189
|
+
});
|
|
1190
|
+
ipcMain.handle(IPC_CHANNELS.HEAP_SNAPSHOT, async (_event, filePath) => {
|
|
1191
|
+
return this.monitor.takeHeapSnapshot(filePath);
|
|
1192
|
+
});
|
|
1193
|
+
ipcMain.handle(IPC_CHANNELS.MARK, (_event, args) => {
|
|
1194
|
+
this.monitor.mark(args.label, args.metadata);
|
|
1195
|
+
});
|
|
1196
|
+
ipcMain.handle(IPC_CHANNELS.GET_CONFIG, () => {
|
|
1197
|
+
return this.monitor.getConfig();
|
|
1198
|
+
});
|
|
1199
|
+
ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
|
|
1200
|
+
return this.monitor.getSessions();
|
|
1201
|
+
});
|
|
1202
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
|
|
1203
|
+
return this.monitor.exportSession(sessionId);
|
|
1204
|
+
});
|
|
1205
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_IMPORT, async () => {
|
|
1206
|
+
return this.monitor.importSession();
|
|
1207
|
+
});
|
|
1208
|
+
ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
|
|
1209
|
+
return this.monitor.deleteSession(sessionId);
|
|
1210
|
+
});
|
|
1211
|
+
ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (_event, detail) => {
|
|
1212
|
+
this.monitor.updateRendererDetail(detail);
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
/** 向监控面板推送快照数据 */
|
|
1216
|
+
pushSnapshot(dashboardWindow, data) {
|
|
1217
|
+
if (dashboardWindow && !dashboardWindow.isDestroyed()) {
|
|
1218
|
+
dashboardWindow.webContents.send(IPC_CHANNELS.SNAPSHOT, data);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/** 向监控面板推送异常事件 */
|
|
1222
|
+
pushAnomaly(dashboardWindow, data) {
|
|
1223
|
+
if (dashboardWindow && !dashboardWindow.isDestroyed()) {
|
|
1224
|
+
dashboardWindow.webContents.send(IPC_CHANNELS.ANOMALY, data);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
/** 移除所有注册的 handlers */
|
|
1228
|
+
unregister() {
|
|
1229
|
+
const channels = Object.values(IPC_CHANNELS);
|
|
1230
|
+
for (const channel of channels) {
|
|
1231
|
+
ipcMain.removeHandler(channel);
|
|
1232
|
+
ipcMain.removeAllListeners(channel);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
// src/types/config.ts
|
|
1238
|
+
var DEFAULT_CONFIG = {
|
|
1239
|
+
enabled: true,
|
|
1240
|
+
autoStart: true,
|
|
1241
|
+
openDashboardOnStart: true,
|
|
1242
|
+
collectInterval: 2e3,
|
|
1243
|
+
persistInterval: 60,
|
|
1244
|
+
enableRendererDetail: false,
|
|
1245
|
+
enableV8HeapSpaces: true,
|
|
1246
|
+
anomaly: {
|
|
1247
|
+
enabled: true,
|
|
1248
|
+
checkInterval: 3e4,
|
|
1249
|
+
rules: []
|
|
1250
|
+
},
|
|
1251
|
+
storage: {
|
|
1252
|
+
directory: "",
|
|
1253
|
+
// 运行时由 app.getPath('userData') 填充
|
|
1254
|
+
maxSessions: 50,
|
|
1255
|
+
maxSessionDuration: 24 * 60 * 60 * 1e3
|
|
1256
|
+
},
|
|
1257
|
+
dashboard: {
|
|
1258
|
+
width: 1400,
|
|
1259
|
+
height: 900,
|
|
1260
|
+
alwaysOnTop: false
|
|
1261
|
+
},
|
|
1262
|
+
processLabels: {}
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// src/core/monitor.ts
|
|
1266
|
+
var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
1267
|
+
constructor(config) {
|
|
1268
|
+
super();
|
|
1269
|
+
this.started = false;
|
|
1270
|
+
this.latestSnapshot = null;
|
|
1271
|
+
this.config = this.mergeConfig(config);
|
|
1272
|
+
if (!this.config.enabled) {
|
|
1273
|
+
this.collector = null;
|
|
1274
|
+
this.anomalyDetector = null;
|
|
1275
|
+
this.analyzer = null;
|
|
1276
|
+
this.dashboard = null;
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
this.collector = new MemoryCollector(this.config);
|
|
1280
|
+
this.anomalyDetector = new AnomalyDetector(this.config);
|
|
1281
|
+
this.analyzer = new Analyzer();
|
|
1282
|
+
this.dashboard = new DashboardManager(this.config);
|
|
1283
|
+
if (this.config.autoStart) {
|
|
1284
|
+
this.start();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
// ============ 生命周期 ============
|
|
1288
|
+
/** 启动监控 */
|
|
1289
|
+
async start() {
|
|
1290
|
+
if (!this.config.enabled || this.started) return;
|
|
1291
|
+
if (!app2.isReady()) {
|
|
1292
|
+
await app2.whenReady();
|
|
1293
|
+
}
|
|
1294
|
+
const storageDir = this.config.storage.directory || path3.join(app2.getPath("userData"), "memory-monitor");
|
|
1295
|
+
this.persister = new DataPersister(this.config, storageDir);
|
|
1296
|
+
this.sessionManager = new SessionManager(this.persister);
|
|
1297
|
+
this.ipcHandler = new IPCMainHandler(this);
|
|
1298
|
+
this.ipcHandler.register();
|
|
1299
|
+
this.collector.on("snapshot", (snapshot) => {
|
|
1300
|
+
this.onSnapshot(snapshot);
|
|
1301
|
+
});
|
|
1302
|
+
this.anomalyDetector.on("anomaly", (anomaly) => {
|
|
1303
|
+
this.emit("anomaly", anomaly);
|
|
1304
|
+
this.ipcHandler.pushAnomaly(this.dashboard.getWindow(), anomaly);
|
|
1305
|
+
});
|
|
1306
|
+
this.collector.start();
|
|
1307
|
+
this.anomalyDetector.start();
|
|
1308
|
+
if (this.config.openDashboardOnStart) {
|
|
1309
|
+
this.openDashboard();
|
|
1310
|
+
}
|
|
1311
|
+
this.persister.cleanOldSessions();
|
|
1312
|
+
this.started = true;
|
|
1313
|
+
}
|
|
1314
|
+
/** 停止监控 */
|
|
1315
|
+
async stop() {
|
|
1316
|
+
if (!this.started) return;
|
|
1317
|
+
this.collector.stop();
|
|
1318
|
+
this.anomalyDetector.stop();
|
|
1319
|
+
const currentSession = this.sessionManager.getCurrentSession();
|
|
1320
|
+
if (currentSession) {
|
|
1321
|
+
await this.stopSession();
|
|
1322
|
+
}
|
|
1323
|
+
this.persister.close();
|
|
1324
|
+
this.started = false;
|
|
1325
|
+
}
|
|
1326
|
+
/** 销毁实例 */
|
|
1327
|
+
async destroy() {
|
|
1328
|
+
await this.stop();
|
|
1329
|
+
this.dashboard.destroy();
|
|
1330
|
+
if (this.ipcHandler) {
|
|
1331
|
+
this.ipcHandler.unregister();
|
|
1332
|
+
}
|
|
1333
|
+
this.removeAllListeners();
|
|
1334
|
+
}
|
|
1335
|
+
// ============ 会话控制 ============
|
|
1336
|
+
/** 开始新会话 */
|
|
1337
|
+
startSession(label, description) {
|
|
1338
|
+
if (!this.started) {
|
|
1339
|
+
throw new Error("Monitor is not started");
|
|
1340
|
+
}
|
|
1341
|
+
const session = this.sessionManager.startSession(label, description);
|
|
1342
|
+
this.collector.setSessionId(session.id);
|
|
1343
|
+
this.anomalyDetector.clearAnomalies();
|
|
1344
|
+
return session.id;
|
|
1345
|
+
}
|
|
1346
|
+
/** 结束当前会话 */
|
|
1347
|
+
async stopSession() {
|
|
1348
|
+
if (!this.started) return null;
|
|
1349
|
+
const session = this.sessionManager.getCurrentSession();
|
|
1350
|
+
if (!session) return null;
|
|
1351
|
+
const completedSession = this.sessionManager.endSession();
|
|
1352
|
+
if (!completedSession) return null;
|
|
1353
|
+
this.collector.setSessionId(null);
|
|
1354
|
+
const snapshots = this.persister.readSessionSnapshots(completedSession.id);
|
|
1355
|
+
const anomalies = this.anomalyDetector.getAnomalies();
|
|
1356
|
+
const report = this.analyzer.generateReport(
|
|
1357
|
+
completedSession.id,
|
|
1358
|
+
completedSession.label,
|
|
1359
|
+
completedSession.description,
|
|
1360
|
+
completedSession.startTime,
|
|
1361
|
+
completedSession.endTime,
|
|
1362
|
+
snapshots,
|
|
1363
|
+
anomalies,
|
|
1364
|
+
completedSession.dataFile
|
|
1365
|
+
);
|
|
1366
|
+
const reportPath = path3.join(this.persister.getStorageDir(), completedSession.id, "report.json");
|
|
1367
|
+
const fs2 = await import("fs");
|
|
1368
|
+
fs2.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
1369
|
+
this.emit("session-end", report);
|
|
1370
|
+
return report;
|
|
1371
|
+
}
|
|
1372
|
+
// ============ 监控面板 ============
|
|
1373
|
+
/** 打开监控面板 */
|
|
1374
|
+
openDashboard() {
|
|
1375
|
+
this.dashboard.open();
|
|
1376
|
+
const wcId = this.dashboard.getWebContentsId();
|
|
1377
|
+
this.collector.setMonitorWindowId(wcId);
|
|
1378
|
+
}
|
|
1379
|
+
/** 关闭监控面板 */
|
|
1380
|
+
closeDashboard() {
|
|
1381
|
+
this.dashboard.close();
|
|
1382
|
+
this.collector.setMonitorWindowId(null);
|
|
1383
|
+
}
|
|
1384
|
+
// ============ 数据访问 ============
|
|
1385
|
+
/** 获取当前最新快照 */
|
|
1386
|
+
getCurrentSnapshot() {
|
|
1387
|
+
return this.latestSnapshot;
|
|
1388
|
+
}
|
|
1389
|
+
/** 获取历史会话列表 */
|
|
1390
|
+
async getSessions() {
|
|
1391
|
+
return this.sessionManager.getSessions();
|
|
1392
|
+
}
|
|
1393
|
+
/** 获取指定会话报告 */
|
|
1394
|
+
async getSessionReport(sessionId) {
|
|
1395
|
+
const fs2 = await import("fs");
|
|
1396
|
+
const reportPath = path3.join(this.persister.getStorageDir(), sessionId, "report.json");
|
|
1397
|
+
try {
|
|
1398
|
+
const content = fs2.readFileSync(reportPath, "utf-8");
|
|
1399
|
+
return JSON.parse(content);
|
|
1400
|
+
} catch {
|
|
1401
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
1402
|
+
if (!session || !session.endTime) return null;
|
|
1403
|
+
const snapshots = this.persister.readSessionSnapshots(sessionId);
|
|
1404
|
+
if (snapshots.length === 0) return null;
|
|
1405
|
+
return this.analyzer.generateReport(
|
|
1406
|
+
session.id,
|
|
1407
|
+
session.label,
|
|
1408
|
+
session.description,
|
|
1409
|
+
session.startTime,
|
|
1410
|
+
session.endTime,
|
|
1411
|
+
snapshots,
|
|
1412
|
+
[],
|
|
1413
|
+
session.dataFile
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/** 获取指定会话的快照数据(支持时间过滤和降采样) */
|
|
1418
|
+
async getSessionSnapshots(sessionId, startTime, endTime, maxPoints) {
|
|
1419
|
+
let snapshots = this.persister.readSessionSnapshots(sessionId);
|
|
1420
|
+
if (startTime != null) {
|
|
1421
|
+
snapshots = snapshots.filter((s) => s.timestamp >= startTime);
|
|
1422
|
+
}
|
|
1423
|
+
if (endTime != null) {
|
|
1424
|
+
snapshots = snapshots.filter((s) => s.timestamp <= endTime);
|
|
1425
|
+
}
|
|
1426
|
+
const limit = maxPoints ?? 600;
|
|
1427
|
+
if (snapshots.length > limit) {
|
|
1428
|
+
const step = snapshots.length / limit;
|
|
1429
|
+
const sampled = [];
|
|
1430
|
+
for (let i = 0; i < limit; i++) {
|
|
1431
|
+
sampled.push(snapshots[Math.round(i * step)]);
|
|
1432
|
+
}
|
|
1433
|
+
if (sampled[sampled.length - 1] !== snapshots[snapshots.length - 1]) {
|
|
1434
|
+
sampled[sampled.length - 1] = snapshots[snapshots.length - 1];
|
|
1435
|
+
}
|
|
1436
|
+
snapshots = sampled;
|
|
1437
|
+
}
|
|
1438
|
+
return snapshots;
|
|
1439
|
+
}
|
|
1440
|
+
/** 对比两个会话 */
|
|
1441
|
+
async compareSessions(baseId, targetId) {
|
|
1442
|
+
const baseReport = await this.getSessionReport(baseId);
|
|
1443
|
+
const targetReport = await this.getSessionReport(targetId);
|
|
1444
|
+
if (!baseReport || !targetReport) return null;
|
|
1445
|
+
return this.analyzer.compareReports(baseReport, targetReport);
|
|
1446
|
+
}
|
|
1447
|
+
/** 导出会话数据(供 IPC 调用,弹出保存对话框) */
|
|
1448
|
+
async exportSession(sessionId) {
|
|
1449
|
+
try {
|
|
1450
|
+
const { dialog } = await import("electron");
|
|
1451
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
1452
|
+
if (!session) return { success: false, error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728" };
|
|
1453
|
+
const defaultName = `emm-${session.label.replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, "_")}-${new Date(session.startTime).toISOString().slice(0, 10)}.emmsession`;
|
|
1454
|
+
const result = await dialog.showSaveDialog({
|
|
1455
|
+
title: "\u5BFC\u51FA\u4F1A\u8BDD\u6570\u636E",
|
|
1456
|
+
defaultPath: defaultName,
|
|
1457
|
+
filters: [
|
|
1458
|
+
{ name: "EMM Session", extensions: ["emmsession"] },
|
|
1459
|
+
{ name: "JSON \u6587\u4EF6", extensions: ["json"] },
|
|
1460
|
+
{ name: "\u6240\u6709\u6587\u4EF6", extensions: ["*"] }
|
|
1461
|
+
]
|
|
1462
|
+
});
|
|
1463
|
+
if (result.canceled || !result.filePath) {
|
|
1464
|
+
return { success: false, error: "\u7528\u6237\u53D6\u6D88" };
|
|
1465
|
+
}
|
|
1466
|
+
const exportData = this.persister.exportSession(sessionId);
|
|
1467
|
+
const fs2 = await import("fs");
|
|
1468
|
+
const fileContent = JSON.stringify({
|
|
1469
|
+
version: 1,
|
|
1470
|
+
exportTime: Date.now(),
|
|
1471
|
+
...exportData
|
|
1472
|
+
}, null, 2);
|
|
1473
|
+
fs2.writeFileSync(result.filePath, fileContent, "utf-8");
|
|
1474
|
+
return { success: true, filePath: result.filePath };
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
return { success: false, error: String(err) };
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
/** 导入会话数据(供 IPC 调用,弹出打开对话框) */
|
|
1480
|
+
async importSession() {
|
|
1481
|
+
try {
|
|
1482
|
+
const { dialog } = await import("electron");
|
|
1483
|
+
const result = await dialog.showOpenDialog({
|
|
1484
|
+
title: "\u5BFC\u5165\u4F1A\u8BDD\u6570\u636E",
|
|
1485
|
+
filters: [
|
|
1486
|
+
{ name: "EMM Session", extensions: ["emmsession"] },
|
|
1487
|
+
{ name: "JSON \u6587\u4EF6", extensions: ["json"] },
|
|
1488
|
+
{ name: "\u6240\u6709\u6587\u4EF6", extensions: ["*"] }
|
|
1489
|
+
],
|
|
1490
|
+
properties: ["openFile"]
|
|
1491
|
+
});
|
|
1492
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
1493
|
+
return { success: false, error: "\u7528\u6237\u53D6\u6D88" };
|
|
1494
|
+
}
|
|
1495
|
+
const fs2 = await import("fs");
|
|
1496
|
+
const content = fs2.readFileSync(result.filePaths[0], "utf-8");
|
|
1497
|
+
const parsed = JSON.parse(content);
|
|
1498
|
+
if (!parsed.meta || !parsed.snapshots) {
|
|
1499
|
+
return { success: false, error: "\u6587\u4EF6\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u7F3A\u5C11 meta \u6216 snapshots \u6570\u636E" };
|
|
1500
|
+
}
|
|
1501
|
+
const session = this.persister.importSession({
|
|
1502
|
+
meta: parsed.meta,
|
|
1503
|
+
snapshots: parsed.snapshots,
|
|
1504
|
+
report: parsed.report || null
|
|
1505
|
+
});
|
|
1506
|
+
return { success: true, session };
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
return { success: false, error: String(err) };
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/** 删除指定会话 */
|
|
1512
|
+
async deleteSession(sessionId) {
|
|
1513
|
+
return this.persister.deleteSession(sessionId);
|
|
1514
|
+
}
|
|
1515
|
+
// ============ 工具方法 ============
|
|
1516
|
+
/** 手动触发 GC */
|
|
1517
|
+
async triggerGC() {
|
|
1518
|
+
const beforeMem = process.memoryUsage();
|
|
1519
|
+
if (global.gc) {
|
|
1520
|
+
global.gc();
|
|
1521
|
+
} else {
|
|
1522
|
+
try {
|
|
1523
|
+
v82.writeHeapSnapshot;
|
|
1524
|
+
} catch {
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1528
|
+
const afterMem = process.memoryUsage();
|
|
1529
|
+
const freed = beforeMem.heapUsed - afterMem.heapUsed;
|
|
1530
|
+
return {
|
|
1531
|
+
beforeHeapUsed: beforeMem.heapUsed,
|
|
1532
|
+
afterHeapUsed: afterMem.heapUsed,
|
|
1533
|
+
freed,
|
|
1534
|
+
freedPercent: beforeMem.heapUsed > 0 ? freed / beforeMem.heapUsed * 100 : 0,
|
|
1535
|
+
timestamp: Date.now()
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
/** 导出堆快照 */
|
|
1539
|
+
async takeHeapSnapshot(filePath) {
|
|
1540
|
+
const snapshotPath = filePath || path3.join(
|
|
1541
|
+
this.persister.getStorageDir(),
|
|
1542
|
+
`heap-${Date.now()}.heapsnapshot`
|
|
1543
|
+
);
|
|
1544
|
+
v82.writeHeapSnapshot(snapshotPath);
|
|
1545
|
+
return snapshotPath;
|
|
1546
|
+
}
|
|
1547
|
+
/** 添加事件标记 */
|
|
1548
|
+
mark(label, metadata) {
|
|
1549
|
+
this.collector.addMark(label, metadata);
|
|
1550
|
+
}
|
|
1551
|
+
/** 更新渲染进程 V8 详情 */
|
|
1552
|
+
updateRendererDetail(detail) {
|
|
1553
|
+
this.collector.updateRendererDetail(detail);
|
|
1554
|
+
}
|
|
1555
|
+
/** 获取当前配置 */
|
|
1556
|
+
getConfig() {
|
|
1557
|
+
return { ...this.config };
|
|
1558
|
+
}
|
|
1559
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1560
|
+
on(event, handler) {
|
|
1561
|
+
return super.on(event, handler);
|
|
1562
|
+
}
|
|
1563
|
+
// ============ 私有方法 ============
|
|
1564
|
+
onSnapshot(snapshot) {
|
|
1565
|
+
this.latestSnapshot = snapshot;
|
|
1566
|
+
if (this.sessionManager.getCurrentSession()) {
|
|
1567
|
+
this.persister.writeSnapshot(snapshot);
|
|
1568
|
+
this.sessionManager.incrementSnapshotCount();
|
|
1569
|
+
}
|
|
1570
|
+
this.anomalyDetector.addSnapshot(snapshot);
|
|
1571
|
+
this.ipcHandler?.pushSnapshot(this.dashboard.getWindow(), snapshot);
|
|
1572
|
+
this.emit("snapshot", snapshot);
|
|
1573
|
+
}
|
|
1574
|
+
mergeConfig(userConfig) {
|
|
1575
|
+
if (!userConfig) return { ...DEFAULT_CONFIG };
|
|
1576
|
+
return {
|
|
1577
|
+
...DEFAULT_CONFIG,
|
|
1578
|
+
...userConfig,
|
|
1579
|
+
anomaly: {
|
|
1580
|
+
...DEFAULT_CONFIG.anomaly,
|
|
1581
|
+
...userConfig.anomaly || {}
|
|
1582
|
+
},
|
|
1583
|
+
storage: {
|
|
1584
|
+
...DEFAULT_CONFIG.storage,
|
|
1585
|
+
...userConfig.storage || {}
|
|
1586
|
+
},
|
|
1587
|
+
dashboard: {
|
|
1588
|
+
...DEFAULT_CONFIG.dashboard,
|
|
1589
|
+
...userConfig.dashboard || {}
|
|
1590
|
+
},
|
|
1591
|
+
processLabels: {
|
|
1592
|
+
...DEFAULT_CONFIG.processLabels,
|
|
1593
|
+
...userConfig.processLabels || {}
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
export {
|
|
1599
|
+
ElectronMemoryMonitor,
|
|
1600
|
+
IPC_CHANNELS
|
|
1601
|
+
};
|
|
1602
|
+
//# sourceMappingURL=index.mjs.map
|