@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.js
CHANGED
|
@@ -299,13 +299,13 @@ var DataPersister = class {
|
|
|
299
299
|
return null;
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
-
/**
|
|
302
|
+
/** 获取所有会话列表(按 startTime 降序,最新在前;与索引写入顺序无关) */
|
|
303
303
|
getSessions() {
|
|
304
304
|
const indexFile = path.join(this.storageDir, "sessions.json");
|
|
305
305
|
try {
|
|
306
306
|
const content = fs.readFileSync(indexFile, "utf-8");
|
|
307
307
|
const index = JSON.parse(content);
|
|
308
|
-
return index.sessions;
|
|
308
|
+
return this.sortSessionsByStartDesc(index.sessions);
|
|
309
309
|
} catch {
|
|
310
310
|
return [];
|
|
311
311
|
}
|
|
@@ -408,11 +408,19 @@ var DataPersister = class {
|
|
|
408
408
|
saveSessionIndex(sessions) {
|
|
409
409
|
const indexFile = path.join(this.storageDir, "sessions.json");
|
|
410
410
|
const index = {
|
|
411
|
-
sessions,
|
|
411
|
+
sessions: this.sortSessionsByStartDesc(sessions),
|
|
412
412
|
lastUpdated: Date.now()
|
|
413
413
|
};
|
|
414
414
|
fs.writeFileSync(indexFile, JSON.stringify(index, null, 2), "utf-8");
|
|
415
415
|
}
|
|
416
|
+
/** 统一排序:startTime 新 → 旧(同毫秒时按 id 稳定排序) */
|
|
417
|
+
sortSessionsByStartDesc(sessions) {
|
|
418
|
+
return [...sessions].sort((a, b) => {
|
|
419
|
+
const t = b.startTime - a.startTime;
|
|
420
|
+
if (t !== 0) return t;
|
|
421
|
+
return String(b.id).localeCompare(String(a.id));
|
|
422
|
+
});
|
|
423
|
+
}
|
|
416
424
|
ensureDirectory(dir) {
|
|
417
425
|
if (!fs.existsSync(dir)) {
|
|
418
426
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -475,10 +483,14 @@ var SessionManager = class {
|
|
|
475
483
|
getCurrentSession() {
|
|
476
484
|
return this.currentSession;
|
|
477
485
|
}
|
|
478
|
-
/**
|
|
486
|
+
/** 开始新会话;若顶替了上一条进行中的会话,通过 `replaced` 返回以便主进程补写 report.json */
|
|
479
487
|
startSession(label, description) {
|
|
488
|
+
if (!this.currentSession) {
|
|
489
|
+
this.reconcileStaleRunningInIndex();
|
|
490
|
+
}
|
|
491
|
+
let replaced = null;
|
|
480
492
|
if (this.currentSession && this.currentSession.status === "running") {
|
|
481
|
-
this.endSession();
|
|
493
|
+
replaced = this.endSession();
|
|
482
494
|
}
|
|
483
495
|
const sessionId = v4();
|
|
484
496
|
const { dataFile, metaFile } = this.persister.createSessionFiles(sessionId);
|
|
@@ -494,7 +506,7 @@ var SessionManager = class {
|
|
|
494
506
|
};
|
|
495
507
|
this.currentSession = session;
|
|
496
508
|
this.persister.saveSessionMeta(session);
|
|
497
|
-
return session;
|
|
509
|
+
return { session, replaced };
|
|
498
510
|
}
|
|
499
511
|
/** 结束当前会话 */
|
|
500
512
|
endSession() {
|
|
@@ -522,6 +534,24 @@ var SessionManager = class {
|
|
|
522
534
|
getSession(sessionId) {
|
|
523
535
|
return this.persister.readSessionMeta(sessionId);
|
|
524
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* 当前内存无活动会话时,将索引中仍为 running 的条目标为 aborted(进程异常退出后残留)
|
|
539
|
+
*/
|
|
540
|
+
reconcileStaleRunningInIndex() {
|
|
541
|
+
if (this.currentSession) return;
|
|
542
|
+
const sessions = this.persister.getSessions();
|
|
543
|
+
const now = Date.now();
|
|
544
|
+
for (const s of sessions) {
|
|
545
|
+
if (s.status !== "running") continue;
|
|
546
|
+
const fixed = {
|
|
547
|
+
...s,
|
|
548
|
+
status: "aborted",
|
|
549
|
+
endTime: now,
|
|
550
|
+
duration: now - s.startTime
|
|
551
|
+
};
|
|
552
|
+
this.persister.saveSessionMeta(fixed);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
525
555
|
};
|
|
526
556
|
|
|
527
557
|
// src/core/anomaly.ts
|
|
@@ -692,10 +722,38 @@ var os2 = __toESM(require("os"));
|
|
|
692
722
|
var Analyzer = class {
|
|
693
723
|
/** 生成会话报告 */
|
|
694
724
|
generateReport(sessionId, label, description, startTime, endTime, snapshots, anomalies, dataFile) {
|
|
725
|
+
const environment = this.collectEnvironment();
|
|
726
|
+
const eventMarks = this.collectEventMarks(snapshots);
|
|
695
727
|
if (snapshots.length === 0) {
|
|
696
|
-
|
|
728
|
+
const summary2 = this.emptySummary();
|
|
729
|
+
const suggestions2 = [
|
|
730
|
+
{
|
|
731
|
+
id: "no-snapshots",
|
|
732
|
+
severity: "info",
|
|
733
|
+
category: "optimization",
|
|
734
|
+
title: "\u4F1A\u8BDD\u5185\u6CA1\u6709\u53EF\u7528\u7684\u5185\u5B58\u5FEB\u7167",
|
|
735
|
+
description: "\u7ED3\u675F\u4F1A\u8BDD\u65F6\u78C1\u76D8\u4E0A\u5C1A\u672A\u5199\u5165\u4EFB\u4F55\u91C7\u6837\u70B9\uFF08\u4F8B\u5982\u521A\u542F\u52A8\u5C31\u7ACB\u523B\u7ED3\u675F\uFF0C\u6216\u91C7\u96C6\u95F4\u9694\u5C1A\u672A\u89E6\u53D1\uFF09\u3002\u62A5\u544A\u4E2D\u7684\u7EDF\u8BA1\u4E0E\u8D8B\u52BF\u65E0\u6CD5\u8BA1\u7B97\u3002",
|
|
736
|
+
suggestions: [
|
|
737
|
+
"\u4FDD\u6301\u4F1A\u8BDD\u5F00\u542F\u81F3\u5C11\u4E00\u4E2A\u91C7\u96C6\u5468\u671F\u540E\u518D\u70B9\u300C\u7ED3\u675F\u4F1A\u8BDD\u300D",
|
|
738
|
+
"\u5728\u914D\u7F6E\u4E2D\u9002\u5F53\u7F29\u77ED\u91C7\u96C6\u95F4\u9694\u4EE5\u4FBF\u66F4\u5FEB\u5F97\u5230\u6570\u636E"
|
|
739
|
+
]
|
|
740
|
+
}
|
|
741
|
+
];
|
|
742
|
+
return {
|
|
743
|
+
sessionId,
|
|
744
|
+
label,
|
|
745
|
+
description,
|
|
746
|
+
startTime,
|
|
747
|
+
endTime,
|
|
748
|
+
duration: endTime - startTime,
|
|
749
|
+
environment,
|
|
750
|
+
summary: summary2,
|
|
751
|
+
anomalies,
|
|
752
|
+
suggestions: suggestions2,
|
|
753
|
+
eventMarks,
|
|
754
|
+
dataFile
|
|
755
|
+
};
|
|
697
756
|
}
|
|
698
|
-
const environment = this.collectEnvironment();
|
|
699
757
|
const summary = this.computeSummary(snapshots);
|
|
700
758
|
const suggestions = this.generateSuggestions(snapshots, summary, anomalies);
|
|
701
759
|
return {
|
|
@@ -709,6 +767,7 @@ var Analyzer = class {
|
|
|
709
767
|
summary,
|
|
710
768
|
anomalies,
|
|
711
769
|
suggestions,
|
|
770
|
+
eventMarks,
|
|
712
771
|
dataFile
|
|
713
772
|
};
|
|
714
773
|
}
|
|
@@ -745,6 +804,53 @@ var Analyzer = class {
|
|
|
745
804
|
};
|
|
746
805
|
}
|
|
747
806
|
// ===== 私有方法 =====
|
|
807
|
+
emptySummary() {
|
|
808
|
+
const z = this.computeMetricSummary([]);
|
|
809
|
+
const stable = { slope: 0, r2: 0, direction: "stable", confidence: "low" };
|
|
810
|
+
return {
|
|
811
|
+
totalProcesses: { min: 0, max: 0, avg: 0 },
|
|
812
|
+
totalMemory: z,
|
|
813
|
+
byProcessType: {
|
|
814
|
+
browser: z,
|
|
815
|
+
renderer: [],
|
|
816
|
+
gpu: null,
|
|
817
|
+
utility: null
|
|
818
|
+
},
|
|
819
|
+
mainV8Heap: {
|
|
820
|
+
heapUsed: z,
|
|
821
|
+
heapTotal: z,
|
|
822
|
+
external: z,
|
|
823
|
+
arrayBuffers: z
|
|
824
|
+
},
|
|
825
|
+
trends: {
|
|
826
|
+
totalMemory: stable,
|
|
827
|
+
browserMemory: stable,
|
|
828
|
+
rendererMemory: stable
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
/** 从快照展平所有标记,并附上该采样点的分类内存(KB) */
|
|
833
|
+
collectEventMarks(snapshots) {
|
|
834
|
+
const out = [];
|
|
835
|
+
for (const s of snapshots) {
|
|
836
|
+
if (!s.marks?.length) continue;
|
|
837
|
+
const browserKB = s.processes.filter((p) => p.type === "Browser").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
|
|
838
|
+
const rendererKB = s.processes.filter((p) => p.type === "Tab" && !p.isMonitorProcess).reduce((sum, p) => sum + p.memory.workingSetSize, 0);
|
|
839
|
+
const gpuKB = s.processes.filter((p) => p.type === "GPU").reduce((sum, p) => sum + p.memory.workingSetSize, 0);
|
|
840
|
+
for (const m of s.marks) {
|
|
841
|
+
out.push({
|
|
842
|
+
timestamp: m.timestamp,
|
|
843
|
+
label: m.label,
|
|
844
|
+
metadata: m.metadata,
|
|
845
|
+
totalWorkingSetKB: s.totalWorkingSetSize,
|
|
846
|
+
browserKB,
|
|
847
|
+
rendererKB,
|
|
848
|
+
gpuKB
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
748
854
|
collectEnvironment() {
|
|
749
855
|
const cpus2 = os2.cpus();
|
|
750
856
|
return {
|
|
@@ -1114,6 +1220,7 @@ var path3 = __toESM(require("path"));
|
|
|
1114
1220
|
var import_electron2 = require("electron");
|
|
1115
1221
|
var import_promises = require("fs/promises");
|
|
1116
1222
|
var path2 = __toESM(require("path"));
|
|
1223
|
+
var import_node_url = require("url");
|
|
1117
1224
|
var SCHEME = "emm-dashboard";
|
|
1118
1225
|
var privilegedRegistered = false;
|
|
1119
1226
|
var handlerRegistered = false;
|
|
@@ -1148,6 +1255,48 @@ function isPathInsideRoot(filePath, root) {
|
|
|
1148
1255
|
}
|
|
1149
1256
|
return true;
|
|
1150
1257
|
}
|
|
1258
|
+
function urlToUiRelativePath(requestUrl) {
|
|
1259
|
+
let u;
|
|
1260
|
+
try {
|
|
1261
|
+
u = new URL(requestUrl);
|
|
1262
|
+
} catch {
|
|
1263
|
+
return "";
|
|
1264
|
+
}
|
|
1265
|
+
let p = "";
|
|
1266
|
+
try {
|
|
1267
|
+
p = decodeURIComponent(u.pathname || "");
|
|
1268
|
+
} catch {
|
|
1269
|
+
p = u.pathname || "";
|
|
1270
|
+
}
|
|
1271
|
+
p = p.replace(/^\/+/, "");
|
|
1272
|
+
const host = (u.hostname || "").toLowerCase();
|
|
1273
|
+
if (p.includes("..")) {
|
|
1274
|
+
return "";
|
|
1275
|
+
}
|
|
1276
|
+
if (host === "electron" || host === "") {
|
|
1277
|
+
return p || "index.html";
|
|
1278
|
+
}
|
|
1279
|
+
if (p) {
|
|
1280
|
+
return `${host}/${p}`.replace(/\\/g, "/");
|
|
1281
|
+
}
|
|
1282
|
+
if (host.includes(".")) {
|
|
1283
|
+
return host;
|
|
1284
|
+
}
|
|
1285
|
+
return "index.html";
|
|
1286
|
+
}
|
|
1287
|
+
function bufferToResponseBody(buf) {
|
|
1288
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1289
|
+
}
|
|
1290
|
+
function looksLikeHtml(buf) {
|
|
1291
|
+
let i = 0;
|
|
1292
|
+
if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
|
|
1293
|
+
i = 3;
|
|
1294
|
+
}
|
|
1295
|
+
while (i < buf.length && (buf[i] === 32 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13)) {
|
|
1296
|
+
i += 1;
|
|
1297
|
+
}
|
|
1298
|
+
return i < buf.length && buf[i] === 60;
|
|
1299
|
+
}
|
|
1151
1300
|
function corsHeaders() {
|
|
1152
1301
|
return {
|
|
1153
1302
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -1189,24 +1338,40 @@ function ensureDashboardProtocolHandler(uiRoot) {
|
|
|
1189
1338
|
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
1190
1339
|
}
|
|
1191
1340
|
try {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
} catch {
|
|
1196
|
-
return new Response("Bad URL", { status: 400, headers: corsHeaders() });
|
|
1341
|
+
const rel = urlToUiRelativePath(request.url);
|
|
1342
|
+
if (!rel || rel.includes("..")) {
|
|
1343
|
+
return new Response("Bad path", { status: 400, headers: corsHeaders() });
|
|
1197
1344
|
}
|
|
1198
|
-
|
|
1199
|
-
if (!rel) {
|
|
1200
|
-
rel = "index.html";
|
|
1201
|
-
}
|
|
1202
|
-
const filePath = path2.resolve(path2.join(base, rel));
|
|
1345
|
+
const filePath = path2.resolve(path2.join(base, ...rel.split("/")));
|
|
1203
1346
|
if (!isPathInsideRoot(filePath, base)) {
|
|
1204
1347
|
return new Response("Forbidden", { status: 403, headers: corsHeaders() });
|
|
1205
1348
|
}
|
|
1206
|
-
const
|
|
1349
|
+
const fileUrl = (0, import_node_url.pathToFileURL)(filePath).href;
|
|
1350
|
+
let upstream;
|
|
1351
|
+
try {
|
|
1352
|
+
upstream = await import_electron2.net.fetch(fileUrl);
|
|
1353
|
+
} catch {
|
|
1354
|
+
upstream = new Response(null, { status: 599 });
|
|
1355
|
+
}
|
|
1356
|
+
let buf;
|
|
1357
|
+
if (!upstream.ok) {
|
|
1358
|
+
buf = await (0, import_promises.readFile)(filePath);
|
|
1359
|
+
} else {
|
|
1360
|
+
const ab = await upstream.arrayBuffer();
|
|
1361
|
+
buf = Buffer.from(ab);
|
|
1362
|
+
}
|
|
1207
1363
|
const ext = path2.extname(filePath).toLowerCase();
|
|
1364
|
+
if (ext === ".html" && !looksLikeHtml(buf)) {
|
|
1365
|
+
console.error(
|
|
1366
|
+
"[@electron-memory/monitor] emm-dashboard: not HTML at",
|
|
1367
|
+
filePath,
|
|
1368
|
+
"url=",
|
|
1369
|
+
request.url
|
|
1370
|
+
);
|
|
1371
|
+
return new Response("Invalid dashboard HTML", { status: 500, headers: corsHeaders() });
|
|
1372
|
+
}
|
|
1208
1373
|
const mime = MIME_BY_EXT[ext] || "application/octet-stream";
|
|
1209
|
-
return new Response(
|
|
1374
|
+
return new Response(bufferToResponseBody(buf), {
|
|
1210
1375
|
status: 200,
|
|
1211
1376
|
headers: {
|
|
1212
1377
|
"Content-Type": mime,
|
|
@@ -1216,6 +1381,7 @@ function ensureDashboardProtocolHandler(uiRoot) {
|
|
|
1216
1381
|
});
|
|
1217
1382
|
} catch (err) {
|
|
1218
1383
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1384
|
+
console.error("[@electron-memory/monitor] emm-dashboard:", request.url, err);
|
|
1219
1385
|
return new Response(msg, { status: 404, headers: corsHeaders() });
|
|
1220
1386
|
}
|
|
1221
1387
|
});
|
|
@@ -1374,10 +1540,28 @@ var IPCMainHandler = class {
|
|
|
1374
1540
|
return this.monitor.startSession(args.label, args.description);
|
|
1375
1541
|
});
|
|
1376
1542
|
import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_STOP, async () => {
|
|
1377
|
-
|
|
1543
|
+
try {
|
|
1544
|
+
const report = await this.monitor.stopSession();
|
|
1545
|
+
if (!report) {
|
|
1546
|
+
return { ok: false, reason: "no_active_session" };
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
ok: true,
|
|
1550
|
+
sessionId: report.sessionId,
|
|
1551
|
+
label: report.label,
|
|
1552
|
+
durationMs: report.duration
|
|
1553
|
+
};
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
console.error("[electron-memory-monitor] SESSION_STOP failed:", err);
|
|
1556
|
+
return {
|
|
1557
|
+
ok: false,
|
|
1558
|
+
reason: "error",
|
|
1559
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1378
1562
|
});
|
|
1379
1563
|
import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_LIST, async () => {
|
|
1380
|
-
return this.monitor.
|
|
1564
|
+
return this.monitor.getSessionsPayloadForIpc();
|
|
1381
1565
|
});
|
|
1382
1566
|
import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_REPORT, async (_event, sessionId) => {
|
|
1383
1567
|
return this.monitor.getSessionReport(sessionId);
|
|
@@ -1401,7 +1585,7 @@ var IPCMainHandler = class {
|
|
|
1401
1585
|
return this.monitor.getConfig();
|
|
1402
1586
|
});
|
|
1403
1587
|
import_electron4.ipcMain.handle(IPC_CHANNELS.GET_SESSIONS, async () => {
|
|
1404
|
-
return this.monitor.
|
|
1588
|
+
return this.monitor.getSessionsPayloadForIpc();
|
|
1405
1589
|
});
|
|
1406
1590
|
import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_EXPORT, async (_event, sessionId) => {
|
|
1407
1591
|
return this.monitor.exportSession(sessionId);
|
|
@@ -1412,8 +1596,11 @@ var IPCMainHandler = class {
|
|
|
1412
1596
|
import_electron4.ipcMain.handle(IPC_CHANNELS.SESSION_DELETE, async (_event, sessionId) => {
|
|
1413
1597
|
return this.monitor.deleteSession(sessionId);
|
|
1414
1598
|
});
|
|
1415
|
-
import_electron4.ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (
|
|
1416
|
-
this.monitor.updateRendererDetail(
|
|
1599
|
+
import_electron4.ipcMain.on(IPC_CHANNELS.RENDERER_REPORT, (event, detail) => {
|
|
1600
|
+
this.monitor.updateRendererDetail({
|
|
1601
|
+
...detail,
|
|
1602
|
+
webContentsId: event.sender.id
|
|
1603
|
+
});
|
|
1417
1604
|
});
|
|
1418
1605
|
}
|
|
1419
1606
|
/** 向监控面板推送快照数据 */
|
|
@@ -1443,6 +1630,10 @@ var DEFAULT_CONFIG = {
|
|
|
1443
1630
|
enabled: true,
|
|
1444
1631
|
autoStart: true,
|
|
1445
1632
|
openDashboardOnStart: true,
|
|
1633
|
+
session: {
|
|
1634
|
+
autoStartOnLaunch: true,
|
|
1635
|
+
autoLabelPrefix: "\u81EA\u52A8\u4F1A\u8BDD"
|
|
1636
|
+
},
|
|
1446
1637
|
collectInterval: 2e3,
|
|
1447
1638
|
persistInterval: 60,
|
|
1448
1639
|
enableRendererDetail: false,
|
|
@@ -1468,6 +1659,36 @@ var DEFAULT_CONFIG = {
|
|
|
1468
1659
|
};
|
|
1469
1660
|
|
|
1470
1661
|
// src/core/monitor.ts
|
|
1662
|
+
function mergeMarksFromExcludedSnapshots(full, sampled) {
|
|
1663
|
+
const marked = full.filter((s) => s.marks && s.marks.length > 0);
|
|
1664
|
+
if (marked.length === 0) return sampled;
|
|
1665
|
+
const sampledRefs = new Set(sampled);
|
|
1666
|
+
const result = sampled.map((s) => ({
|
|
1667
|
+
...s,
|
|
1668
|
+
marks: s.marks?.length ? [...s.marks] : void 0
|
|
1669
|
+
}));
|
|
1670
|
+
for (const src of marked) {
|
|
1671
|
+
if (sampledRefs.has(src)) continue;
|
|
1672
|
+
let bestIdx = 0;
|
|
1673
|
+
let bestDiff = Infinity;
|
|
1674
|
+
for (let i = 0; i < result.length; i++) {
|
|
1675
|
+
const d = Math.abs(result[i].timestamp - src.timestamp);
|
|
1676
|
+
if (d < bestDiff) {
|
|
1677
|
+
bestDiff = d;
|
|
1678
|
+
bestIdx = i;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const target = result[bestIdx];
|
|
1682
|
+
target.marks = [...target.marks || [], ...src.marks];
|
|
1683
|
+
}
|
|
1684
|
+
return result;
|
|
1685
|
+
}
|
|
1686
|
+
function formatAutoSessionLabel(prefix) {
|
|
1687
|
+
const d = /* @__PURE__ */ new Date();
|
|
1688
|
+
const p2 = (n) => String(n).padStart(2, "0");
|
|
1689
|
+
const stamp = `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
|
|
1690
|
+
return `${prefix || "\u81EA\u52A8\u4F1A\u8BDD"} ${stamp}`;
|
|
1691
|
+
}
|
|
1471
1692
|
var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
1472
1693
|
constructor(config) {
|
|
1473
1694
|
super();
|
|
@@ -1511,11 +1732,20 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
|
1511
1732
|
});
|
|
1512
1733
|
this.collector.start();
|
|
1513
1734
|
this.anomalyDetector.start();
|
|
1735
|
+
this.persister.cleanOldSessions();
|
|
1736
|
+
this.sessionManager.reconcileStaleRunningInIndex();
|
|
1737
|
+
this.started = true;
|
|
1738
|
+
if (this.config.session.autoStartOnLaunch) {
|
|
1739
|
+
try {
|
|
1740
|
+
const label = formatAutoSessionLabel(this.config.session.autoLabelPrefix);
|
|
1741
|
+
this.startSession(label, this.config.session.autoDescription);
|
|
1742
|
+
} catch (err) {
|
|
1743
|
+
console.error("[@electron-memory/monitor] autoStartOnLaunch failed:", err);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1514
1746
|
if (this.config.openDashboardOnStart) {
|
|
1515
1747
|
this.openDashboard();
|
|
1516
1748
|
}
|
|
1517
|
-
this.persister.cleanOldSessions();
|
|
1518
|
-
this.started = true;
|
|
1519
1749
|
}
|
|
1520
1750
|
/** 停止监控 */
|
|
1521
1751
|
async stop() {
|
|
@@ -1544,9 +1774,16 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
|
1544
1774
|
if (!this.started) {
|
|
1545
1775
|
throw new Error("Monitor is not started");
|
|
1546
1776
|
}
|
|
1547
|
-
const
|
|
1777
|
+
const anomaliesForReplaced = this.anomalyDetector.getAnomalies();
|
|
1778
|
+
const { session, replaced } = this.sessionManager.startSession(label, description);
|
|
1779
|
+
if (replaced) {
|
|
1780
|
+
void this.persistCompletedSessionReport(replaced, anomaliesForReplaced).catch((err) => {
|
|
1781
|
+
console.error("[@electron-memory/monitor] \u88AB\u9876\u66FF\u4F1A\u8BDD\u7684\u62A5\u544A\u5199\u5165\u5931\u8D25:", err);
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1548
1784
|
this.collector.setSessionId(session.id);
|
|
1549
1785
|
this.anomalyDetector.clearAnomalies();
|
|
1786
|
+
this.emit("session-start", session);
|
|
1550
1787
|
return session.id;
|
|
1551
1788
|
}
|
|
1552
1789
|
/** 结束当前会话 */
|
|
@@ -1557,8 +1794,26 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
|
1557
1794
|
const completedSession = this.sessionManager.endSession();
|
|
1558
1795
|
if (!completedSession) return null;
|
|
1559
1796
|
this.collector.setSessionId(null);
|
|
1560
|
-
const snapshots = this.persister.readSessionSnapshots(completedSession.id);
|
|
1561
1797
|
const anomalies = this.anomalyDetector.getAnomalies();
|
|
1798
|
+
const report = await this.persistCompletedSessionReport(completedSession, anomalies);
|
|
1799
|
+
return report;
|
|
1800
|
+
}
|
|
1801
|
+
/** 为已落盘元数据的会话生成并写入 report.json(显式结束或被新会话顶替) */
|
|
1802
|
+
async persistCompletedSessionReport(completedSession, anomalies) {
|
|
1803
|
+
const fs2 = await import("fs/promises");
|
|
1804
|
+
const snapshotsPath = path4.join(
|
|
1805
|
+
this.persister.getStorageDir(),
|
|
1806
|
+
completedSession.id,
|
|
1807
|
+
"snapshots.jsonl"
|
|
1808
|
+
);
|
|
1809
|
+
let snapshots = [];
|
|
1810
|
+
try {
|
|
1811
|
+
const content = await fs2.readFile(snapshotsPath, "utf-8");
|
|
1812
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
1813
|
+
snapshots = lines.map((line) => JSON.parse(line));
|
|
1814
|
+
} catch {
|
|
1815
|
+
snapshots = this.persister.readSessionSnapshots(completedSession.id);
|
|
1816
|
+
}
|
|
1562
1817
|
const report = this.analyzer.generateReport(
|
|
1563
1818
|
completedSession.id,
|
|
1564
1819
|
completedSession.label,
|
|
@@ -1570,8 +1825,7 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
|
1570
1825
|
completedSession.dataFile
|
|
1571
1826
|
);
|
|
1572
1827
|
const reportPath = path4.join(this.persister.getStorageDir(), completedSession.id, "report.json");
|
|
1573
|
-
|
|
1574
|
-
fs2.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
1828
|
+
await fs2.writeFile(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
1575
1829
|
this.emit("session-end", report);
|
|
1576
1830
|
return report;
|
|
1577
1831
|
}
|
|
@@ -1596,6 +1850,18 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
|
1596
1850
|
async getSessions() {
|
|
1597
1851
|
return this.sessionManager.getSessions();
|
|
1598
1852
|
}
|
|
1853
|
+
/**
|
|
1854
|
+
* 供监控面板 IPC:列表来自磁盘索引,是否在录制以内存中的 currentSession 为准(避免索引僵尸 running)
|
|
1855
|
+
*/
|
|
1856
|
+
getSessionsPayloadForIpc() {
|
|
1857
|
+
if (!this.started) {
|
|
1858
|
+
return { sessions: [], activeSessionId: null };
|
|
1859
|
+
}
|
|
1860
|
+
return {
|
|
1861
|
+
sessions: this.sessionManager.getSessions(),
|
|
1862
|
+
activeSessionId: this.sessionManager.getCurrentSession()?.id ?? null
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1599
1865
|
/** 获取指定会话报告 */
|
|
1600
1866
|
async getSessionReport(sessionId) {
|
|
1601
1867
|
const fs2 = await import("fs");
|
|
@@ -1607,7 +1873,6 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
|
1607
1873
|
const session = this.sessionManager.getSession(sessionId);
|
|
1608
1874
|
if (!session || !session.endTime) return null;
|
|
1609
1875
|
const snapshots = this.persister.readSessionSnapshots(sessionId);
|
|
1610
|
-
if (snapshots.length === 0) return null;
|
|
1611
1876
|
return this.analyzer.generateReport(
|
|
1612
1877
|
session.id,
|
|
1613
1878
|
session.label,
|
|
@@ -1629,17 +1894,18 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
|
1629
1894
|
if (endTime != null) {
|
|
1630
1895
|
snapshots = snapshots.filter((s) => s.timestamp <= endTime);
|
|
1631
1896
|
}
|
|
1897
|
+
const beforeDownsample = snapshots;
|
|
1632
1898
|
const limit = maxPoints ?? 600;
|
|
1633
|
-
if (
|
|
1634
|
-
const step =
|
|
1899
|
+
if (beforeDownsample.length > limit) {
|
|
1900
|
+
const step = beforeDownsample.length / limit;
|
|
1635
1901
|
const sampled = [];
|
|
1636
1902
|
for (let i = 0; i < limit; i++) {
|
|
1637
|
-
sampled.push(
|
|
1903
|
+
sampled.push(beforeDownsample[Math.round(i * step)]);
|
|
1638
1904
|
}
|
|
1639
|
-
if (sampled[sampled.length - 1] !==
|
|
1640
|
-
sampled[sampled.length - 1] =
|
|
1905
|
+
if (sampled[sampled.length - 1] !== beforeDownsample[beforeDownsample.length - 1]) {
|
|
1906
|
+
sampled[sampled.length - 1] = beforeDownsample[beforeDownsample.length - 1];
|
|
1641
1907
|
}
|
|
1642
|
-
snapshots = sampled;
|
|
1908
|
+
snapshots = mergeMarksFromExcludedSnapshots(beforeDownsample, sampled);
|
|
1643
1909
|
}
|
|
1644
1910
|
return snapshots;
|
|
1645
1911
|
}
|
|
@@ -1790,6 +2056,10 @@ var ElectronMemoryMonitor = class extends import_events3.EventEmitter {
|
|
|
1790
2056
|
return {
|
|
1791
2057
|
...DEFAULT_CONFIG,
|
|
1792
2058
|
...userConfig,
|
|
2059
|
+
session: {
|
|
2060
|
+
...DEFAULT_CONFIG.session,
|
|
2061
|
+
...userConfig.session || {}
|
|
2062
|
+
},
|
|
1793
2063
|
anomaly: {
|
|
1794
2064
|
...DEFAULT_CONFIG.anomaly,
|
|
1795
2065
|
...userConfig.anomaly || {}
|