@electron-memory/monitor 0.2.2 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard-preload.js.map +1 -1
- package/dist/index.d.mts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +308 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +309 -39
- package/dist/index.mjs.map +1 -1
- package/dist/ui/assets/index-BTI73y9e.css +1 -0
- package/dist/ui/assets/index-mExwYeTZ.js +9 -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.mjs
CHANGED
|
@@ -261,13 +261,13 @@ var DataPersister = class {
|
|
|
261
261
|
return null;
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
-
/**
|
|
264
|
+
/** 获取所有会话列表(按 startTime 降序,最新在前;与索引写入顺序无关) */
|
|
265
265
|
getSessions() {
|
|
266
266
|
const indexFile = path.join(this.storageDir, "sessions.json");
|
|
267
267
|
try {
|
|
268
268
|
const content = fs.readFileSync(indexFile, "utf-8");
|
|
269
269
|
const index = JSON.parse(content);
|
|
270
|
-
return index.sessions;
|
|
270
|
+
return this.sortSessionsByStartDesc(index.sessions);
|
|
271
271
|
} catch {
|
|
272
272
|
return [];
|
|
273
273
|
}
|
|
@@ -370,11 +370,19 @@ var DataPersister = class {
|
|
|
370
370
|
saveSessionIndex(sessions) {
|
|
371
371
|
const indexFile = path.join(this.storageDir, "sessions.json");
|
|
372
372
|
const index = {
|
|
373
|
-
sessions,
|
|
373
|
+
sessions: this.sortSessionsByStartDesc(sessions),
|
|
374
374
|
lastUpdated: Date.now()
|
|
375
375
|
};
|
|
376
376
|
fs.writeFileSync(indexFile, JSON.stringify(index, null, 2), "utf-8");
|
|
377
377
|
}
|
|
378
|
+
/** 统一排序:startTime 新 → 旧(同毫秒时按 id 稳定排序) */
|
|
379
|
+
sortSessionsByStartDesc(sessions) {
|
|
380
|
+
return [...sessions].sort((a, b) => {
|
|
381
|
+
const t = b.startTime - a.startTime;
|
|
382
|
+
if (t !== 0) return t;
|
|
383
|
+
return String(b.id).localeCompare(String(a.id));
|
|
384
|
+
});
|
|
385
|
+
}
|
|
378
386
|
ensureDirectory(dir) {
|
|
379
387
|
if (!fs.existsSync(dir)) {
|
|
380
388
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -437,10 +445,14 @@ var SessionManager = class {
|
|
|
437
445
|
getCurrentSession() {
|
|
438
446
|
return this.currentSession;
|
|
439
447
|
}
|
|
440
|
-
/**
|
|
448
|
+
/** 开始新会话;若顶替了上一条进行中的会话,通过 `replaced` 返回以便主进程补写 report.json */
|
|
441
449
|
startSession(label, description) {
|
|
450
|
+
if (!this.currentSession) {
|
|
451
|
+
this.reconcileStaleRunningInIndex();
|
|
452
|
+
}
|
|
453
|
+
let replaced = null;
|
|
442
454
|
if (this.currentSession && this.currentSession.status === "running") {
|
|
443
|
-
this.endSession();
|
|
455
|
+
replaced = this.endSession();
|
|
444
456
|
}
|
|
445
457
|
const sessionId = v4();
|
|
446
458
|
const { dataFile, metaFile } = this.persister.createSessionFiles(sessionId);
|
|
@@ -456,7 +468,7 @@ var SessionManager = class {
|
|
|
456
468
|
};
|
|
457
469
|
this.currentSession = session;
|
|
458
470
|
this.persister.saveSessionMeta(session);
|
|
459
|
-
return session;
|
|
471
|
+
return { session, replaced };
|
|
460
472
|
}
|
|
461
473
|
/** 结束当前会话 */
|
|
462
474
|
endSession() {
|
|
@@ -484,6 +496,24 @@ var SessionManager = class {
|
|
|
484
496
|
getSession(sessionId) {
|
|
485
497
|
return this.persister.readSessionMeta(sessionId);
|
|
486
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* 当前内存无活动会话时,将索引中仍为 running 的条目标为 aborted(进程异常退出后残留)
|
|
501
|
+
*/
|
|
502
|
+
reconcileStaleRunningInIndex() {
|
|
503
|
+
if (this.currentSession) return;
|
|
504
|
+
const sessions = this.persister.getSessions();
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
for (const s of sessions) {
|
|
507
|
+
if (s.status !== "running") continue;
|
|
508
|
+
const fixed = {
|
|
509
|
+
...s,
|
|
510
|
+
status: "aborted",
|
|
511
|
+
endTime: now,
|
|
512
|
+
duration: now - s.startTime
|
|
513
|
+
};
|
|
514
|
+
this.persister.saveSessionMeta(fixed);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
487
517
|
};
|
|
488
518
|
|
|
489
519
|
// src/core/anomaly.ts
|
|
@@ -654,10 +684,38 @@ import * as os2 from "os";
|
|
|
654
684
|
var Analyzer = class {
|
|
655
685
|
/** 生成会话报告 */
|
|
656
686
|
generateReport(sessionId, label, description, startTime, endTime, snapshots, anomalies, dataFile) {
|
|
687
|
+
const environment = this.collectEnvironment();
|
|
688
|
+
const eventMarks = this.collectEventMarks(snapshots);
|
|
657
689
|
if (snapshots.length === 0) {
|
|
658
|
-
|
|
690
|
+
const summary2 = this.emptySummary();
|
|
691
|
+
const suggestions2 = [
|
|
692
|
+
{
|
|
693
|
+
id: "no-snapshots",
|
|
694
|
+
severity: "info",
|
|
695
|
+
category: "optimization",
|
|
696
|
+
title: "\u4F1A\u8BDD\u5185\u6CA1\u6709\u53EF\u7528\u7684\u5185\u5B58\u5FEB\u7167",
|
|
697
|
+
description: "\u7ED3\u675F\u4F1A\u8BDD\u65F6\u78C1\u76D8\u4E0A\u5C1A\u672A\u5199\u5165\u4EFB\u4F55\u91C7\u6837\u70B9\uFF08\u4F8B\u5982\u521A\u542F\u52A8\u5C31\u7ACB\u523B\u7ED3\u675F\uFF0C\u6216\u91C7\u96C6\u95F4\u9694\u5C1A\u672A\u89E6\u53D1\uFF09\u3002\u62A5\u544A\u4E2D\u7684\u7EDF\u8BA1\u4E0E\u8D8B\u52BF\u65E0\u6CD5\u8BA1\u7B97\u3002",
|
|
698
|
+
suggestions: [
|
|
699
|
+
"\u4FDD\u6301\u4F1A\u8BDD\u5F00\u542F\u81F3\u5C11\u4E00\u4E2A\u91C7\u96C6\u5468\u671F\u540E\u518D\u70B9\u300C\u7ED3\u675F\u4F1A\u8BDD\u300D",
|
|
700
|
+
"\u5728\u914D\u7F6E\u4E2D\u9002\u5F53\u7F29\u77ED\u91C7\u96C6\u95F4\u9694\u4EE5\u4FBF\u66F4\u5FEB\u5F97\u5230\u6570\u636E"
|
|
701
|
+
]
|
|
702
|
+
}
|
|
703
|
+
];
|
|
704
|
+
return {
|
|
705
|
+
sessionId,
|
|
706
|
+
label,
|
|
707
|
+
description,
|
|
708
|
+
startTime,
|
|
709
|
+
endTime,
|
|
710
|
+
duration: endTime - startTime,
|
|
711
|
+
environment,
|
|
712
|
+
summary: summary2,
|
|
713
|
+
anomalies,
|
|
714
|
+
suggestions: suggestions2,
|
|
715
|
+
eventMarks,
|
|
716
|
+
dataFile
|
|
717
|
+
};
|
|
659
718
|
}
|
|
660
|
-
const environment = this.collectEnvironment();
|
|
661
719
|
const summary = this.computeSummary(snapshots);
|
|
662
720
|
const suggestions = this.generateSuggestions(snapshots, summary, anomalies);
|
|
663
721
|
return {
|
|
@@ -671,6 +729,7 @@ var Analyzer = class {
|
|
|
671
729
|
summary,
|
|
672
730
|
anomalies,
|
|
673
731
|
suggestions,
|
|
732
|
+
eventMarks,
|
|
674
733
|
dataFile
|
|
675
734
|
};
|
|
676
735
|
}
|
|
@@ -707,6 +766,53 @@ var Analyzer = class {
|
|
|
707
766
|
};
|
|
708
767
|
}
|
|
709
768
|
// ===== 私有方法 =====
|
|
769
|
+
emptySummary() {
|
|
770
|
+
const z = this.computeMetricSummary([]);
|
|
771
|
+
const stable = { slope: 0, r2: 0, direction: "stable", confidence: "low" };
|
|
772
|
+
return {
|
|
773
|
+
totalProcesses: { min: 0, max: 0, avg: 0 },
|
|
774
|
+
totalMemory: z,
|
|
775
|
+
byProcessType: {
|
|
776
|
+
browser: z,
|
|
777
|
+
renderer: [],
|
|
778
|
+
gpu: null,
|
|
779
|
+
utility: null
|
|
780
|
+
},
|
|
781
|
+
mainV8Heap: {
|
|
782
|
+
heapUsed: z,
|
|
783
|
+
heapTotal: z,
|
|
784
|
+
external: z,
|
|
785
|
+
arrayBuffers: z
|
|
786
|
+
},
|
|
787
|
+
trends: {
|
|
788
|
+
totalMemory: stable,
|
|
789
|
+
browserMemory: stable,
|
|
790
|
+
rendererMemory: stable
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/** 从快照展平所有标记,并附上该采样点的分类内存(KB) */
|
|
795
|
+
collectEventMarks(snapshots) {
|
|
796
|
+
const out = [];
|
|
797
|
+
for (const s of snapshots) {
|
|
798
|
+
if (!s.marks?.length) continue;
|
|
799
|
+
const browserKB = s.processes.filter((p) => p.type === "Browser").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
|
|
800
|
+
const rendererKB = s.processes.filter((p) => p.type === "Tab" && !p.isMonitorProcess).reduce((sum, p) => sum + p.memory.workingSetSize, 0);
|
|
801
|
+
const gpuKB = s.processes.filter((p) => p.type === "GPU").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
|
|
802
|
+
for (const m of s.marks) {
|
|
803
|
+
out.push({
|
|
804
|
+
timestamp: m.timestamp,
|
|
805
|
+
label: m.label,
|
|
806
|
+
metadata: m.metadata,
|
|
807
|
+
totalWorkingSetKB: s.totalWorkingSetSize,
|
|
808
|
+
browserKB,
|
|
809
|
+
rendererKB,
|
|
810
|
+
gpuKB
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return out;
|
|
815
|
+
}
|
|
710
816
|
collectEnvironment() {
|
|
711
817
|
const cpus2 = os2.cpus();
|
|
712
818
|
return {
|
|
@@ -1073,9 +1179,10 @@ import { BrowserWindow as BrowserWindow2 } from "electron";
|
|
|
1073
1179
|
import * as path3 from "path";
|
|
1074
1180
|
|
|
1075
1181
|
// src/core/dashboard-protocol.ts
|
|
1076
|
-
import { app as app2, protocol } from "electron";
|
|
1182
|
+
import { app as app2, net, protocol } from "electron";
|
|
1077
1183
|
import { readFile } from "fs/promises";
|
|
1078
1184
|
import * as path2 from "path";
|
|
1185
|
+
import { pathToFileURL } from "url";
|
|
1079
1186
|
var SCHEME = "emm-dashboard";
|
|
1080
1187
|
var privilegedRegistered = false;
|
|
1081
1188
|
var handlerRegistered = false;
|
|
@@ -1110,6 +1217,48 @@ function isPathInsideRoot(filePath, root) {
|
|
|
1110
1217
|
}
|
|
1111
1218
|
return true;
|
|
1112
1219
|
}
|
|
1220
|
+
function urlToUiRelativePath(requestUrl) {
|
|
1221
|
+
let u;
|
|
1222
|
+
try {
|
|
1223
|
+
u = new URL(requestUrl);
|
|
1224
|
+
} catch {
|
|
1225
|
+
return "";
|
|
1226
|
+
}
|
|
1227
|
+
let p = "";
|
|
1228
|
+
try {
|
|
1229
|
+
p = decodeURIComponent(u.pathname || "");
|
|
1230
|
+
} catch {
|
|
1231
|
+
p = u.pathname || "";
|
|
1232
|
+
}
|
|
1233
|
+
p = p.replace(/^\/+/, "");
|
|
1234
|
+
const host = (u.hostname || "").toLowerCase();
|
|
1235
|
+
if (p.includes("..")) {
|
|
1236
|
+
return "";
|
|
1237
|
+
}
|
|
1238
|
+
if (host === "electron" || host === "") {
|
|
1239
|
+
return p || "index.html";
|
|
1240
|
+
}
|
|
1241
|
+
if (p) {
|
|
1242
|
+
return `${host}/${p}`.replace(/\\/g, "/");
|
|
1243
|
+
}
|
|
1244
|
+
if (host.includes(".")) {
|
|
1245
|
+
return host;
|
|
1246
|
+
}
|
|
1247
|
+
return "index.html";
|
|
1248
|
+
}
|
|
1249
|
+
function bufferToResponseBody(buf) {
|
|
1250
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1251
|
+
}
|
|
1252
|
+
function looksLikeHtml(buf) {
|
|
1253
|
+
let i = 0;
|
|
1254
|
+
if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
|
|
1255
|
+
i = 3;
|
|
1256
|
+
}
|
|
1257
|
+
while (i < buf.length && (buf[i] === 32 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13)) {
|
|
1258
|
+
i += 1;
|
|
1259
|
+
}
|
|
1260
|
+
return i < buf.length && buf[i] === 60;
|
|
1261
|
+
}
|
|
1113
1262
|
function corsHeaders() {
|
|
1114
1263
|
return {
|
|
1115
1264
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -1151,24 +1300,40 @@ function ensureDashboardProtocolHandler(uiRoot) {
|
|
|
1151
1300
|
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
1152
1301
|
}
|
|
1153
1302
|
try {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
} catch {
|
|
1158
|
-
return new Response("Bad URL", { status: 400, headers: corsHeaders() });
|
|
1303
|
+
const rel = urlToUiRelativePath(request.url);
|
|
1304
|
+
if (!rel || rel.includes("..")) {
|
|
1305
|
+
return new Response("Bad path", { status: 400, headers: corsHeaders() });
|
|
1159
1306
|
}
|
|
1160
|
-
|
|
1161
|
-
if (!rel) {
|
|
1162
|
-
rel = "index.html";
|
|
1163
|
-
}
|
|
1164
|
-
const filePath = path2.resolve(path2.join(base, rel));
|
|
1307
|
+
const filePath = path2.resolve(path2.join(base, ...rel.split("/")));
|
|
1165
1308
|
if (!isPathInsideRoot(filePath, base)) {
|
|
1166
1309
|
return new Response("Forbidden", { status: 403, headers: corsHeaders() });
|
|
1167
1310
|
}
|
|
1168
|
-
const
|
|
1311
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
1312
|
+
let upstream;
|
|
1313
|
+
try {
|
|
1314
|
+
upstream = await net.fetch(fileUrl);
|
|
1315
|
+
} catch {
|
|
1316
|
+
upstream = new Response(null, { status: 599 });
|
|
1317
|
+
}
|
|
1318
|
+
let buf;
|
|
1319
|
+
if (!upstream.ok) {
|
|
1320
|
+
buf = await readFile(filePath);
|
|
1321
|
+
} else {
|
|
1322
|
+
const ab = await upstream.arrayBuffer();
|
|
1323
|
+
buf = Buffer.from(ab);
|
|
1324
|
+
}
|
|
1169
1325
|
const ext = path2.extname(filePath).toLowerCase();
|
|
1326
|
+
if (ext === ".html" && !looksLikeHtml(buf)) {
|
|
1327
|
+
console.error(
|
|
1328
|
+
"[@electron-memory/monitor] emm-dashboard: not HTML at",
|
|
1329
|
+
filePath,
|
|
1330
|
+
"url=",
|
|
1331
|
+
request.url
|
|
1332
|
+
);
|
|
1333
|
+
return new Response("Invalid dashboard HTML", { status: 500, headers: corsHeaders() });
|
|
1334
|
+
}
|
|
1170
1335
|
const mime = MIME_BY_EXT[ext] || "application/octet-stream";
|
|
1171
|
-
return new Response(
|
|
1336
|
+
return new Response(bufferToResponseBody(buf), {
|
|
1172
1337
|
status: 200,
|
|
1173
1338
|
headers: {
|
|
1174
1339
|
"Content-Type": mime,
|
|
@@ -1178,6 +1343,7 @@ function ensureDashboardProtocolHandler(uiRoot) {
|
|
|
1178
1343
|
});
|
|
1179
1344
|
} catch (err) {
|
|
1180
1345
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1346
|
+
console.error("[@electron-memory/monitor] emm-dashboard:", request.url, err);
|
|
1181
1347
|
return new Response(msg, { status: 404, headers: corsHeaders() });
|
|
1182
1348
|
}
|
|
1183
1349
|
});
|
|
@@ -1336,10 +1502,28 @@ var IPCMainHandler = class {
|
|
|
1336
1502
|
return this.monitor.startSession(args.label, args.description);
|
|
1337
1503
|
});
|
|
1338
1504
|
ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
|
|
1339
|
-
|
|
1505
|
+
try {
|
|
1506
|
+
const report = await this.monitor.stopSession();
|
|
1507
|
+
if (!report) {
|
|
1508
|
+
return { ok: false, reason: "no_active_session" };
|
|
1509
|
+
}
|
|
1510
|
+
return {
|
|
1511
|
+
ok: true,
|
|
1512
|
+
sessionId: report.sessionId,
|
|
1513
|
+
label: report.label,
|
|
1514
|
+
durationMs: report.duration
|
|
1515
|
+
};
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
console.error("[electron-memory-monitor] SESSION_STOP failed:", err);
|
|
1518
|
+
return {
|
|
1519
|
+
ok: false,
|
|
1520
|
+
reason: "error",
|
|
1521
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1340
1524
|
});
|
|
1341
1525
|
ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
|
|
1342
|
-
return this.monitor.
|
|
1526
|
+
return this.monitor.getSessionsPayloadForIpc();
|
|
1343
1527
|
});
|
|
1344
1528
|
ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
|
|
1345
1529
|
return this.monitor.getSessionReport(sessionId);
|
|
@@ -1363,7 +1547,7 @@ var IPCMainHandler = class {
|
|
|
1363
1547
|
return this.monitor.getConfig();
|
|
1364
1548
|
});
|
|
1365
1549
|
ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
|
|
1366
|
-
return this.monitor.
|
|
1550
|
+
return this.monitor.getSessionsPayloadForIpc();
|
|
1367
1551
|
});
|
|
1368
1552
|
ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
|
|
1369
1553
|
return this.monitor.exportSession(sessionId);
|
|
@@ -1374,8 +1558,11 @@ var IPCMainHandler = class {
|
|
|
1374
1558
|
ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
|
|
1375
1559
|
return this.monitor.deleteSession(sessionId);
|
|
1376
1560
|
});
|
|
1377
|
-
ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (
|
|
1378
|
-
this.monitor.updateRendererDetail(
|
|
1561
|
+
ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (event, detail) => {
|
|
1562
|
+
this.monitor.updateRendererDetail({
|
|
1563
|
+
...detail,
|
|
1564
|
+
webContentsId: event.sender.id
|
|
1565
|
+
});
|
|
1379
1566
|
});
|
|
1380
1567
|
}
|
|
1381
1568
|
/** 向监控面板推送快照数据 */
|
|
@@ -1405,6 +1592,10 @@ var DEFAULT_CONFIG = {
|
|
|
1405
1592
|
enabled: true,
|
|
1406
1593
|
autoStart: true,
|
|
1407
1594
|
openDashboardOnStart: true,
|
|
1595
|
+
session: {
|
|
1596
|
+
autoStartOnLaunch: true,
|
|
1597
|
+
autoLabelPrefix: "\u81EA\u52A8\u4F1A\u8BDD"
|
|
1598
|
+
},
|
|
1408
1599
|
collectInterval: 2e3,
|
|
1409
1600
|
persistInterval: 60,
|
|
1410
1601
|
enableRendererDetail: false,
|
|
@@ -1430,6 +1621,36 @@ var DEFAULT_CONFIG = {
|
|
|
1430
1621
|
};
|
|
1431
1622
|
|
|
1432
1623
|
// src/core/monitor.ts
|
|
1624
|
+
function mergeMarksFromExcludedSnapshots(full, sampled) {
|
|
1625
|
+
const marked = full.filter((s) => s.marks && s.marks.length > 0);
|
|
1626
|
+
if (marked.length === 0) return sampled;
|
|
1627
|
+
const sampledRefs = new Set(sampled);
|
|
1628
|
+
const result = sampled.map((s) => ({
|
|
1629
|
+
...s,
|
|
1630
|
+
marks: s.marks?.length ? [...s.marks] : void 0
|
|
1631
|
+
}));
|
|
1632
|
+
for (const src of marked) {
|
|
1633
|
+
if (sampledRefs.has(src)) continue;
|
|
1634
|
+
let bestIdx = 0;
|
|
1635
|
+
let bestDiff = Infinity;
|
|
1636
|
+
for (let i = 0; i < result.length; i++) {
|
|
1637
|
+
const d = Math.abs(result[i].timestamp - src.timestamp);
|
|
1638
|
+
if (d < bestDiff) {
|
|
1639
|
+
bestDiff = d;
|
|
1640
|
+
bestIdx = i;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
const target = result[bestIdx];
|
|
1644
|
+
target.marks = [...target.marks || [], ...src.marks];
|
|
1645
|
+
}
|
|
1646
|
+
return result;
|
|
1647
|
+
}
|
|
1648
|
+
function formatAutoSessionLabel(prefix) {
|
|
1649
|
+
const d = /* @__PURE__ */ new Date();
|
|
1650
|
+
const p2 = (n) => String(n).padStart(2, "0");
|
|
1651
|
+
const stamp = `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
|
|
1652
|
+
return `${prefix || "\u81EA\u52A8\u4F1A\u8BDD"} ${stamp}`;
|
|
1653
|
+
}
|
|
1433
1654
|
var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
1434
1655
|
constructor(config) {
|
|
1435
1656
|
super();
|
|
@@ -1473,11 +1694,20 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
|
1473
1694
|
});
|
|
1474
1695
|
this.collector.start();
|
|
1475
1696
|
this.anomalyDetector.start();
|
|
1697
|
+
this.persister.cleanOldSessions();
|
|
1698
|
+
this.sessionManager.reconcileStaleRunningInIndex();
|
|
1699
|
+
this.started = true;
|
|
1700
|
+
if (this.config.session.autoStartOnLaunch) {
|
|
1701
|
+
try {
|
|
1702
|
+
const label = formatAutoSessionLabel(this.config.session.autoLabelPrefix);
|
|
1703
|
+
this.startSession(label, this.config.session.autoDescription);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
console.error("[@electron-memory/monitor] autoStartOnLaunch failed:", err);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1476
1708
|
if (this.config.openDashboardOnStart) {
|
|
1477
1709
|
this.openDashboard();
|
|
1478
1710
|
}
|
|
1479
|
-
this.persister.cleanOldSessions();
|
|
1480
|
-
this.started = true;
|
|
1481
1711
|
}
|
|
1482
1712
|
/** 停止监控 */
|
|
1483
1713
|
async stop() {
|
|
@@ -1506,9 +1736,16 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
|
1506
1736
|
if (!this.started) {
|
|
1507
1737
|
throw new Error("Monitor is not started");
|
|
1508
1738
|
}
|
|
1509
|
-
const
|
|
1739
|
+
const anomaliesForReplaced = this.anomalyDetector.getAnomalies();
|
|
1740
|
+
const { session, replaced } = this.sessionManager.startSession(label, description);
|
|
1741
|
+
if (replaced) {
|
|
1742
|
+
void this.persistCompletedSessionReport(replaced, anomaliesForReplaced).catch((err) => {
|
|
1743
|
+
console.error("[@electron-memory/monitor] \u88AB\u9876\u66FF\u4F1A\u8BDD\u7684\u62A5\u544A\u5199\u5165\u5931\u8D25:", err);
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1510
1746
|
this.collector.setSessionId(session.id);
|
|
1511
1747
|
this.anomalyDetector.clearAnomalies();
|
|
1748
|
+
this.emit("session-start", session);
|
|
1512
1749
|
return session.id;
|
|
1513
1750
|
}
|
|
1514
1751
|
/** 结束当前会话 */
|
|
@@ -1519,8 +1756,26 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
|
1519
1756
|
const completedSession = this.sessionManager.endSession();
|
|
1520
1757
|
if (!completedSession) return null;
|
|
1521
1758
|
this.collector.setSessionId(null);
|
|
1522
|
-
const snapshots = this.persister.readSessionSnapshots(completedSession.id);
|
|
1523
1759
|
const anomalies = this.anomalyDetector.getAnomalies();
|
|
1760
|
+
const report = await this.persistCompletedSessionReport(completedSession, anomalies);
|
|
1761
|
+
return report;
|
|
1762
|
+
}
|
|
1763
|
+
/** 为已落盘元数据的会话生成并写入 report.json(显式结束或被新会话顶替) */
|
|
1764
|
+
async persistCompletedSessionReport(completedSession, anomalies) {
|
|
1765
|
+
const fs2 = await import("fs/promises");
|
|
1766
|
+
const snapshotsPath = path4.join(
|
|
1767
|
+
this.persister.getStorageDir(),
|
|
1768
|
+
completedSession.id,
|
|
1769
|
+
"snapshots.jsonl"
|
|
1770
|
+
);
|
|
1771
|
+
let snapshots = [];
|
|
1772
|
+
try {
|
|
1773
|
+
const content = await fs2.readFile(snapshotsPath, "utf-8");
|
|
1774
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
1775
|
+
snapshots = lines.map((line) => JSON.parse(line));
|
|
1776
|
+
} catch {
|
|
1777
|
+
snapshots = this.persister.readSessionSnapshots(completedSession.id);
|
|
1778
|
+
}
|
|
1524
1779
|
const report = this.analyzer.generateReport(
|
|
1525
1780
|
completedSession.id,
|
|
1526
1781
|
completedSession.label,
|
|
@@ -1532,8 +1787,7 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
|
1532
1787
|
completedSession.dataFile
|
|
1533
1788
|
);
|
|
1534
1789
|
const reportPath = path4.join(this.persister.getStorageDir(), completedSession.id, "report.json");
|
|
1535
|
-
|
|
1536
|
-
fs2.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
1790
|
+
await fs2.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
1537
1791
|
this.emit("session-end", report);
|
|
1538
1792
|
return report;
|
|
1539
1793
|
}
|
|
@@ -1558,6 +1812,18 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
|
1558
1812
|
async getSessions() {
|
|
1559
1813
|
return this.sessionManager.getSessions();
|
|
1560
1814
|
}
|
|
1815
|
+
/**
|
|
1816
|
+
* 供监控面板 IPC:列表来自磁盘索引,是否在录制以内存中的 currentSession 为准(避免索引僵尸 running)
|
|
1817
|
+
*/
|
|
1818
|
+
getSessionsPayloadForIpc() {
|
|
1819
|
+
if (!this.started) {
|
|
1820
|
+
return { sessions: [], activeSessionId: null };
|
|
1821
|
+
}
|
|
1822
|
+
return {
|
|
1823
|
+
sessions: this.sessionManager.getSessions(),
|
|
1824
|
+
activeSessionId: this.sessionManager.getCurrentSession()?.id ?? null
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1561
1827
|
/** 获取指定会话报告 */
|
|
1562
1828
|
async getSessionReport(sessionId) {
|
|
1563
1829
|
const fs2 = await import("fs");
|
|
@@ -1569,7 +1835,6 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
|
1569
1835
|
const session = this.sessionManager.getSession(sessionId);
|
|
1570
1836
|
if (!session || !session.endTime) return null;
|
|
1571
1837
|
const snapshots = this.persister.readSessionSnapshots(sessionId);
|
|
1572
|
-
if (snapshots.length === 0) return null;
|
|
1573
1838
|
return this.analyzer.generateReport(
|
|
1574
1839
|
session.id,
|
|
1575
1840
|
session.label,
|
|
@@ -1591,17 +1856,18 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
|
1591
1856
|
if (endTime != null) {
|
|
1592
1857
|
snapshots = snapshots.filter((s) => s.timestamp <= endTime);
|
|
1593
1858
|
}
|
|
1859
|
+
const beforeDownsample = snapshots;
|
|
1594
1860
|
const limit = maxPoints ?? 600;
|
|
1595
|
-
if (
|
|
1596
|
-
const step =
|
|
1861
|
+
if (beforeDownsample.length > limit) {
|
|
1862
|
+
const step = beforeDownsample.length / limit;
|
|
1597
1863
|
const sampled = [];
|
|
1598
1864
|
for (let i = 0; i < limit; i++) {
|
|
1599
|
-
sampled.push(
|
|
1865
|
+
sampled.push(beforeDownsample[Math.round(i * step)]);
|
|
1600
1866
|
}
|
|
1601
|
-
if (sampled[sampled.length - 1] !==
|
|
1602
|
-
sampled[sampled.length - 1] =
|
|
1867
|
+
if (sampled[sampled.length - 1] !== beforeDownsample[beforeDownsample.length - 1]) {
|
|
1868
|
+
sampled[sampled.length - 1] = beforeDownsample[beforeDownsample.length - 1];
|
|
1603
1869
|
}
|
|
1604
|
-
snapshots = sampled;
|
|
1870
|
+
snapshots = mergeMarksFromExcludedSnapshots(beforeDownsample, sampled);
|
|
1605
1871
|
}
|
|
1606
1872
|
return snapshots;
|
|
1607
1873
|
}
|
|
@@ -1752,6 +2018,10 @@ var ElectronMemoryMonitor = class extends EventEmitter3 {
|
|
|
1752
2018
|
return {
|
|
1753
2019
|
...DEFAULT_CONFIG,
|
|
1754
2020
|
...userConfig,
|
|
2021
|
+
session: {
|
|
2022
|
+
...DEFAULT_CONFIG.session,
|
|
2023
|
+
...userConfig.session || {}
|
|
2024
|
+
},
|
|
1755
2025
|
anomaly: {
|
|
1756
2026
|
...DEFAULT_CONFIG.anomaly,
|
|
1757
2027
|
...userConfig.anomaly || {}
|