dsh-flash-mem-mon 0.1.7 → 0.2.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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/index.js +309 -4
- package/dist/pure.js +54 -1
- package/lib/client.js +174 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-flash-mem-mon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Performance monitor provider for [dsh-flash](https://github.com/tcgbp/dsh-flash) — registers a host-performance-alert provider (RSS absolute thresholds, RSS growth rate, Major GC frequency) via `ctx.get('dockFlashAlerts')` and exposes `/memory-trend` for trend sparklines.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-flash-mem-mon
|
|
2
2
|
|
|
3
|
-
dsh-flash
|
|
3
|
+
dsh-flash 的性能监控提供器 — 通过 `ctx.get('dockFlashAlerts')` 注册主机性能告警提供器(RSS 绝对阈值、RSS 增长率、Major GC 频率),并暴露 `/memory-trend` 路由用于趋势图。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { writeHeapSnapshot } from 'node:v8';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir, tmpdir } from 'node:os';
|
|
1
6
|
// Default export only (`export default Schema`); there is no named `Schema`.
|
|
2
7
|
import Schema from '@deepseek-ai/schemastery';
|
|
3
|
-
import { DEFAULT_ALERT_CONFIG, isFullGc, rssSlopePerMin } from './pure.js';
|
|
8
|
+
import { countArchivedSessions, DEFAULT_ALERT_CONFIG, formatBytes, isFullGc, rssSlopePerMin, sumBytes, } from './pure.js';
|
|
4
9
|
export const name = 'dsh-flash-mem-mon';
|
|
5
|
-
//
|
|
6
|
-
|
|
10
|
+
// Lazy-injected services. All are resolved via ctx.inject() at apply-time, so
|
|
11
|
+
// the plugin still boots even when a service (e.g. workspaceRegistry) is
|
|
12
|
+
// unavailable; declaring them here documents the dependency.
|
|
13
|
+
export const inject = ['workspaceRegistry'];
|
|
7
14
|
// ── Memory alert thresholds (RSS MB) and GC thresholds (major GC/min) ──
|
|
8
15
|
// All values come from the single canonical DEFAULT_ALERT_CONFIG in
|
|
9
16
|
// ./pure.js. The browser client half mirrors these (see lib/client.js
|
|
@@ -19,6 +26,14 @@ const DEFAULT_GC_THRESHOLD_INFO = DEFAULT_ALERT_CONFIG.gcThresholdInfo;
|
|
|
19
26
|
const DEFAULT_GC_THRESHOLD_WARNING = DEFAULT_ALERT_CONFIG.gcThresholdWarning;
|
|
20
27
|
const DEFAULT_GC_THRESHOLD_ERROR = DEFAULT_ALERT_CONFIG.gcThresholdError;
|
|
21
28
|
const DEFAULT_MEM_GROWTH_ALERT_PER_MIN = DEFAULT_ALERT_CONFIG.memGrowthAlertPerMin;
|
|
29
|
+
// Session-footprint (disk) polling interval. Deliberately NOT a member of
|
|
30
|
+
// DEFAULT_ALERT_CONFIG: test/defaults-drift.test.ts asserts that object contains
|
|
31
|
+
// EXACTLY the 10 memory/GC keys, and a footprint key here would break that
|
|
32
|
+
// precise set. Disk scans are comparatively expensive (recursive readdir +
|
|
33
|
+
// stat over every session directory), so they run far less often than memory.
|
|
34
|
+
// The host caches the last scan and the client reads the cached snapshot, so
|
|
35
|
+
// the route itself never triggers an on-demand scan.
|
|
36
|
+
const FOOTPRINT_POLL_INTERVAL_MS = 300000; // 5 min
|
|
22
37
|
/**
|
|
23
38
|
* Sentinel passed to `query()` / `gcEvents()` as the `since` lower bound to
|
|
24
39
|
* mean "no lower bound — return ALL stored records". These methods treat any
|
|
@@ -227,9 +242,12 @@ class MemoryTrendCollector {
|
|
|
227
242
|
// because they are rare and the general ring buffer may evict them.
|
|
228
243
|
if (isFullGc(kind)) {
|
|
229
244
|
const last = this._gcMajorEvents[this._gcMajorEvents.length - 1];
|
|
230
|
-
if (last && event.ts - last.ts <
|
|
245
|
+
if (last && event.ts - last.ts < 2000) {
|
|
231
246
|
// Same full-GC cycle (phase entries arriving back-to-back):
|
|
232
247
|
// merge into the existing event, keeping the worst-case pause.
|
|
248
|
+
// Window is generous (2s) because one cycle's phases can arrive
|
|
249
|
+
// over a second apart under real load; merging here prevents one
|
|
250
|
+
// Full GC from being reported as several adjacent events.
|
|
233
251
|
last.duration = Math.max(last.duration, event.duration);
|
|
234
252
|
last.ts = event.ts;
|
|
235
253
|
}
|
|
@@ -284,6 +302,13 @@ class MemoryTrendCollector {
|
|
|
284
302
|
// not the current owner, so overlapping apply/dispose cycles stay safe.
|
|
285
303
|
let _memoryTrend = null;
|
|
286
304
|
let _ownerCtx = null;
|
|
305
|
+
// ── Heap-snapshot dump state ─────────────────────────────────────────────
|
|
306
|
+
// The "dump heap now" disposal button on an alert POSTs dump-heap, which
|
|
307
|
+
// writes a .heapsnapshot (possibly tens–hundreds of MB) and records it here
|
|
308
|
+
// so the browser client can pull it down via the heap-dump route. Only the
|
|
309
|
+
// latest snapshot is kept for download; dumps are user-paced, not automatic,
|
|
310
|
+
// because writeHeapSnapshot() blocks the host main thread for seconds.
|
|
311
|
+
let _lastSnapshot = null;
|
|
287
312
|
/** Adopt the process-wide collector for `ctx`; no-op if it is already owned. */
|
|
288
313
|
function startTrend(ctx, config) {
|
|
289
314
|
if (_ownerCtx && _ownerCtx !== ctx)
|
|
@@ -307,6 +332,173 @@ function stopTrend(ctx) {
|
|
|
307
332
|
_ownerCtx = null;
|
|
308
333
|
_memoryTrend?.stop();
|
|
309
334
|
}
|
|
335
|
+
class SessionFootprintCollector {
|
|
336
|
+
_snapshot = null;
|
|
337
|
+
_timer = null;
|
|
338
|
+
_running = false;
|
|
339
|
+
start() {
|
|
340
|
+
if (this._running)
|
|
341
|
+
return;
|
|
342
|
+
this._running = true;
|
|
343
|
+
this._scan().then(() => { this._schedule(); }).catch(() => { this._schedule(); });
|
|
344
|
+
}
|
|
345
|
+
stop() {
|
|
346
|
+
this._running = false;
|
|
347
|
+
if (this._timer) {
|
|
348
|
+
clearTimeout(this._timer);
|
|
349
|
+
this._timer = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** Immediately trigger a re-scan (e.g. right after the workspace registry is
|
|
353
|
+
* captured) so the very first archived count is not stuck at 0. */
|
|
354
|
+
rescan() {
|
|
355
|
+
if (!this._running)
|
|
356
|
+
return;
|
|
357
|
+
this._scan().catch(() => { });
|
|
358
|
+
}
|
|
359
|
+
snapshot() {
|
|
360
|
+
return this._snapshot;
|
|
361
|
+
}
|
|
362
|
+
_schedule() {
|
|
363
|
+
if (!this._running)
|
|
364
|
+
return;
|
|
365
|
+
this._timer = setTimeout(() => {
|
|
366
|
+
this._timer = null;
|
|
367
|
+
this._scan().then(() => { this._schedule(); }).catch(() => { this._schedule(); });
|
|
368
|
+
}, FOOTPRINT_POLL_INTERVAL_MS);
|
|
369
|
+
this._timer.unref?.();
|
|
370
|
+
}
|
|
371
|
+
async _scan() {
|
|
372
|
+
const sessionsRoot = resolveSessionsRoot();
|
|
373
|
+
const workspaces = await this._scanWorkspaces(sessionsRoot);
|
|
374
|
+
const snapshot = {
|
|
375
|
+
totalSessions: 0,
|
|
376
|
+
archivedSessions: 0,
|
|
377
|
+
bytes: 0,
|
|
378
|
+
workspaces,
|
|
379
|
+
scannedAt: Date.now(),
|
|
380
|
+
};
|
|
381
|
+
for (const w of workspaces) {
|
|
382
|
+
snapshot.totalSessions += w.totalSessions;
|
|
383
|
+
snapshot.archivedSessions += w.archivedSessions;
|
|
384
|
+
snapshot.bytes += w.bytes;
|
|
385
|
+
}
|
|
386
|
+
this._snapshot = snapshot;
|
|
387
|
+
}
|
|
388
|
+
async _scanWorkspaces(sessionsRoot) {
|
|
389
|
+
let workspaceDirs = [];
|
|
390
|
+
try {
|
|
391
|
+
workspaceDirs = await readdir(sessionsRoot, { withFileTypes: true }).then((ents) => ents.filter((e) => e.isDirectory()).map((e) => e.name));
|
|
392
|
+
}
|
|
393
|
+
catch (_e) {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
const registry = getWorkspaceRegistry();
|
|
397
|
+
let archivedIds = [];
|
|
398
|
+
try {
|
|
399
|
+
if (registry && typeof registry.archivedSessionIds === 'function')
|
|
400
|
+
archivedIds = registry.archivedSessionIds();
|
|
401
|
+
else if (registry && Array.isArray(registry.archivedSessionIds))
|
|
402
|
+
archivedIds = registry.archivedSessionIds;
|
|
403
|
+
}
|
|
404
|
+
catch (_e) { /* keep empty */ }
|
|
405
|
+
const result = [];
|
|
406
|
+
for (const ws of workspaceDirs) {
|
|
407
|
+
const wsPath = join(sessionsRoot, ws);
|
|
408
|
+
let sessionNames = [];
|
|
409
|
+
try {
|
|
410
|
+
sessionNames = await readdir(wsPath, { withFileTypes: true }).then((ents) => ents.filter((e) => e.isDirectory() && /^session-/.test(e.name)).map((e) => e.name));
|
|
411
|
+
}
|
|
412
|
+
catch (_e) {
|
|
413
|
+
sessionNames = [];
|
|
414
|
+
}
|
|
415
|
+
const namesSet = new Set(sessionNames);
|
|
416
|
+
let bytes = 0;
|
|
417
|
+
for (const s of sessionNames) {
|
|
418
|
+
bytes += await dirSize(join(wsPath, s));
|
|
419
|
+
}
|
|
420
|
+
result.push({
|
|
421
|
+
workspace: ws,
|
|
422
|
+
totalSessions: sessionNames.length,
|
|
423
|
+
archivedSessions: countArchivedSessions(archivedIds, namesSet),
|
|
424
|
+
bytes,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/** Recursively sum the on-disk size of a session directory. */
|
|
431
|
+
async function dirSize(dir) {
|
|
432
|
+
let total = 0;
|
|
433
|
+
let entries = [];
|
|
434
|
+
try {
|
|
435
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
436
|
+
}
|
|
437
|
+
catch (_e) {
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
for (const e of entries) {
|
|
441
|
+
const p = join(dir, e.name);
|
|
442
|
+
try {
|
|
443
|
+
if (e.isDirectory()) {
|
|
444
|
+
total += await dirSize(p);
|
|
445
|
+
}
|
|
446
|
+
else if (e.isSymbolicLink()) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
const s = await stat(p);
|
|
451
|
+
total += s.size;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch (_e) { /* skip */ }
|
|
455
|
+
}
|
|
456
|
+
return total;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Resolve the DSH sessions root the same way the harness seeds `$DSH_HOME`:
|
|
460
|
+
* configured override > `$DSH_HOME` env > `~/.dsh` (opened home). `$DSH_HOME`
|
|
461
|
+
* may itself contain a `~` or trailing slashes, so expand the home and join the
|
|
462
|
+
* well-known `sessions` sub-directory explicitly. Returns `''` if a home could
|
|
463
|
+
* not be resolved at all (the scan then simply reports zero workspaces).
|
|
464
|
+
*/
|
|
465
|
+
function resolveSessionsRoot() {
|
|
466
|
+
const env = process.env.DSH_HOME;
|
|
467
|
+
const home = env && env.trim()
|
|
468
|
+
? env.replace(/^~(?=\/|\\)/, homedir())
|
|
469
|
+
: join(homedir(), '.dsh');
|
|
470
|
+
return join(home, 'sessions');
|
|
471
|
+
}
|
|
472
|
+
let _footprint = null;
|
|
473
|
+
let _footprintOwner = null;
|
|
474
|
+
// Cached reference to the DSH workspace-registry service, resolved lazily via
|
|
475
|
+
// ctx.inject. When it is unavailable (or the plugin was applied without it),
|
|
476
|
+
// archived-session counting degrades to whatever the registry can provide —
|
|
477
|
+
// if the registry is truly absent the archived count is reported as 0 rather
|
|
478
|
+
// than guessing from the disk (disk directories carry no archive flag).
|
|
479
|
+
let _workspaceRegistry = null;
|
|
480
|
+
function setWorkspaceRegistry(wsCtx) {
|
|
481
|
+
const reg = wsCtx?.workspaceRegistry;
|
|
482
|
+
if (reg != null)
|
|
483
|
+
_workspaceRegistry = reg;
|
|
484
|
+
}
|
|
485
|
+
function getWorkspaceRegistry() {
|
|
486
|
+
return _workspaceRegistry ?? null;
|
|
487
|
+
}
|
|
488
|
+
function startFootprint(ctx) {
|
|
489
|
+
if (_footprintOwner && _footprintOwner !== ctx)
|
|
490
|
+
return;
|
|
491
|
+
if (!_footprint)
|
|
492
|
+
_footprint = new SessionFootprintCollector();
|
|
493
|
+
_footprintOwner = ctx;
|
|
494
|
+
_footprint.start();
|
|
495
|
+
}
|
|
496
|
+
function stopFootprint(ctx) {
|
|
497
|
+
if (_footprintOwner !== ctx)
|
|
498
|
+
return;
|
|
499
|
+
_footprintOwner = null;
|
|
500
|
+
_footprint?.stop();
|
|
501
|
+
}
|
|
310
502
|
// ── Private HTTP helpers ────────────────────────────────────────────────
|
|
311
503
|
/** Send a JSON response with no-store cache control. */
|
|
312
504
|
function sendJson(res, status, payload) {
|
|
@@ -321,6 +513,26 @@ export function apply(ctx, config) {
|
|
|
321
513
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
322
514
|
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
|
|
323
515
|
});
|
|
516
|
+
// ── Workspace-registry capture ───────────────────────────────────────
|
|
517
|
+
// Grab the runtime's archived-session id list so the footprint collector can
|
|
518
|
+
// intersect it with on-disk session dirs. The registry is injected lazily;
|
|
519
|
+
// if it is unavailable at apply-time the footprint still scans the disk (and
|
|
520
|
+
// simply reports the archived count from whatever the registry later yields,
|
|
521
|
+
// or 0 if it never appears).
|
|
522
|
+
ctx.inject(['workspaceRegistry'], (regCtx) => {
|
|
523
|
+
setWorkspaceRegistry(regCtx);
|
|
524
|
+
// The registry may arrive after the collector's first scan already ran
|
|
525
|
+
// with no archived ids → refresh it immediately so the count is correct.
|
|
526
|
+
_footprint?.rescan();
|
|
527
|
+
});
|
|
528
|
+
// ── Session-footprint collector bootstrap ────────────────────────────
|
|
529
|
+
// Low-frequency disk scan (5-minute cadence by default), cached on the host.
|
|
530
|
+
// Owned exclusively by this ctx; overlapping apply/dispose cycles are safe.
|
|
531
|
+
startFootprint(ctx);
|
|
532
|
+
ctx.effect(() => {
|
|
533
|
+
const owner = ctx;
|
|
534
|
+
return () => { stopFootprint(owner); };
|
|
535
|
+
}, 'dsh-flash-mem-mon: session-footprint collector lifecycle');
|
|
324
536
|
// ── Memory trend collector bootstrap ──────────────────────────────────
|
|
325
537
|
// One bounded ring buffer per process, owned exclusively by the active ctx.
|
|
326
538
|
// startTrend/stopTrend are idempotent and owner-guarded, so overlapping
|
|
@@ -387,5 +599,98 @@ export function apply(ctx, config) {
|
|
|
387
599
|
}
|
|
388
600
|
},
|
|
389
601
|
}), 'dsh-flash-mem-mon: GET /plugins/dsh-flash-mem-mon/memory-trend');
|
|
602
|
+
// GET /plugins/dsh-flash-mem-mon/session-footprint
|
|
603
|
+
// Returns the host's cached session-footprint snapshot (total / archived
|
|
604
|
+
// session counts + per-workspace and total on-disk bytes). Serves only the
|
|
605
|
+
// cached result of the low-frequency disk scan — never triggers a scan on
|
|
606
|
+
// demand, so repeated client polls (or a reload while the modal is open)
|
|
607
|
+
// cannot hammer the filesystem. `scannedAt` is the unix-ms time of the last
|
|
608
|
+
// completed scan (0 before the first scan finishes).
|
|
609
|
+
wsCtx.effect(() => ws.register({
|
|
610
|
+
kind: 'exact',
|
|
611
|
+
path: '/plugins/dsh-flash-mem-mon/session-footprint',
|
|
612
|
+
handler: async (req, res) => {
|
|
613
|
+
if (req.method !== 'GET') {
|
|
614
|
+
res.statusCode = 405;
|
|
615
|
+
res.setHeader('allow', 'GET');
|
|
616
|
+
res.end();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const snap = _footprint?.snapshot() ?? null;
|
|
620
|
+
sendJson(res, 200, snap ?? {
|
|
621
|
+
totalSessions: 0,
|
|
622
|
+
archivedSessions: 0,
|
|
623
|
+
bytes: 0,
|
|
624
|
+
workspaces: [],
|
|
625
|
+
scannedAt: 0,
|
|
626
|
+
});
|
|
627
|
+
},
|
|
628
|
+
}), 'dsh-flash-mem-mon: GET /plugins/dsh-flash-mem-mon/session-footprint');
|
|
629
|
+
// POST /plugins/dsh-flash-mem-mon/dump-heap
|
|
630
|
+
// Triggered by the "立即转储堆快照" disposal button attached to a memory
|
|
631
|
+
// alert (see lib/client.js createHostMemoryAlertProvider → actions).
|
|
632
|
+
// Synchronously writes a .heapsnapshot into the OS temp dir (the one
|
|
633
|
+
// acceptable place to block the main thread: the user asked for it), then
|
|
634
|
+
// records it as the latest snapshot and returns its name+size so the
|
|
635
|
+
// client can confirm and pull it down. Auto-dump is deliberately NOT
|
|
636
|
+
// offered anywhere — a runaway gleaning loop must never stall the host.
|
|
637
|
+
wsCtx.effect(() => ws.register({
|
|
638
|
+
kind: 'exact',
|
|
639
|
+
path: '/plugins/dsh-flash-mem-mon/dump-heap',
|
|
640
|
+
handler: async (_req, res) => {
|
|
641
|
+
if (_req.method !== 'POST') {
|
|
642
|
+
res.statusCode = 405;
|
|
643
|
+
res.setHeader('allow', 'POST');
|
|
644
|
+
res.end();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const name = `dsh-flash-mem-mon-${Date.now()}.${process.pid}.heapsnapshot`;
|
|
648
|
+
const path = join(tmpdir(), name);
|
|
649
|
+
try {
|
|
650
|
+
writeHeapSnapshot(path);
|
|
651
|
+
const info = await stat(path);
|
|
652
|
+
_lastSnapshot = { name, path, size: info.size };
|
|
653
|
+
sendJson(res, 200, { ok: true, name, size: info.size, path });
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
sendJson(res, 500, { ok: false, error: (err && err.message) || 'heap snapshot failed' });
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
}), 'dsh-flash-mem-mon: POST /plugins/dsh-flash-mem-mon/dump-heap');
|
|
660
|
+
// GET /plugins/dsh-flash-mem-mon/heap-dump
|
|
661
|
+
// Streams the most recent heap snapshot back to the client as an
|
|
662
|
+
// attachment. One latest-at-a-time is fine here: dumps are user-paced and
|
|
663
|
+
// downloading is immediate, so keeping a whole archive is unnecessary
|
|
664
|
+
// (and the tmp files are already pruned by the OS over time).
|
|
665
|
+
wsCtx.effect(() => ws.register({
|
|
666
|
+
kind: 'exact',
|
|
667
|
+
path: '/plugins/dsh-flash-mem-mon/heap-dump',
|
|
668
|
+
handler: async (_req, res) => {
|
|
669
|
+
if (_req.method !== 'GET') {
|
|
670
|
+
res.statusCode = 405;
|
|
671
|
+
res.setHeader('allow', 'GET');
|
|
672
|
+
res.end();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const snap = _lastSnapshot;
|
|
676
|
+
if (!snap) {
|
|
677
|
+
sendJson(res, 404, { error: 'No heap snapshot available yet' });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
await stat(snap.path); // ensure it still exists
|
|
682
|
+
res.statusCode = 200;
|
|
683
|
+
res.setHeader('content-type', 'application/octet-stream');
|
|
684
|
+
res.setHeader('content-length', String(snap.size));
|
|
685
|
+
res.setHeader('content-disposition', `attachment; filename="${snap.name}"`);
|
|
686
|
+
res.setHeader('cache-control', 'no-store');
|
|
687
|
+
createReadStream(snap.path).pipe(res);
|
|
688
|
+
}
|
|
689
|
+
catch (_e) {
|
|
690
|
+
_lastSnapshot = null; // stale entry; drop it
|
|
691
|
+
sendJson(res, 404, { error: 'Heap snapshot no longer available' });
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
}), 'dsh-flash-mem-mon: GET /plugins/dsh-flash-mem-mon/heap-dump');
|
|
390
695
|
});
|
|
391
696
|
}
|
package/dist/pure.js
CHANGED
|
@@ -6,7 +6,7 @@ export const DEFAULT_ALERT_CONFIG = {
|
|
|
6
6
|
memThresholdWarning: 512,
|
|
7
7
|
memThresholdError: 1024,
|
|
8
8
|
memThresholdCritical: 1536,
|
|
9
|
-
memPollBase:
|
|
9
|
+
memPollBase: 60000,
|
|
10
10
|
memPollMin: 2000,
|
|
11
11
|
gcThresholdInfo: 2,
|
|
12
12
|
gcThresholdWarning: 5,
|
|
@@ -64,3 +64,56 @@ export function rssSlopePerMin(samples, tailCount = 20) {
|
|
|
64
64
|
const samplesPerMinute = 60000 / avgIntervalMs;
|
|
65
65
|
return Math.round((slope * samplesPerMinute / 1048576) * 100) / 100;
|
|
66
66
|
}
|
|
67
|
+
// ── Session-footprint helpers ─────────────────────────────────────────────
|
|
68
|
+
// These are derived purely from collected inputs so the aggregate math and the
|
|
69
|
+
// human-readable formatting can be unit tested without touching the filesystem.
|
|
70
|
+
/**
|
|
71
|
+
* Derive the number of ARCHIVED sessions that are still physically present on
|
|
72
|
+
* disk. The DSH runtime's `workspaceRegistry.archivedSessionIds` lists archived
|
|
73
|
+
* session ids, but some may have already been pruned from disk (so the disk
|
|
74
|
+
* `sessions/<workspace>/session-<uuid>` directories can be fewer than the ids).
|
|
75
|
+
* The truth source for "archived" is the runtime id list; the disk constrains
|
|
76
|
+
* it to what still exists. Pass `sessionNames` as the on-disk directory names
|
|
77
|
+
* (e.g. `new Set(['session-a', 'session-b', ...])`).
|
|
78
|
+
*/
|
|
79
|
+
export function countArchivedSessions(archivedIds, sessionNames) {
|
|
80
|
+
let count = 0;
|
|
81
|
+
for (const id of archivedIds) {
|
|
82
|
+
if (sessionNames.has(id) || sessionNames.has(`session-${id}`))
|
|
83
|
+
count++;
|
|
84
|
+
}
|
|
85
|
+
return count;
|
|
86
|
+
}
|
|
87
|
+
/** Format a byte count for display, e.g. 1536 → '1.5 KB', 7340032 → '7.0 MB'. */
|
|
88
|
+
export function formatBytes(bytes) {
|
|
89
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
90
|
+
return '—';
|
|
91
|
+
if (bytes < 1024)
|
|
92
|
+
return `${bytes} B`;
|
|
93
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
94
|
+
let value = bytes / 1024;
|
|
95
|
+
let unit = 0;
|
|
96
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
97
|
+
value /= 1024;
|
|
98
|
+
unit++;
|
|
99
|
+
}
|
|
100
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
101
|
+
}
|
|
102
|
+
/** Format a duration (ms) as a human freshness string, e.g. '5 分钟前'. */
|
|
103
|
+
export function formatDurationAgo(nowMs, thenMs) {
|
|
104
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(thenMs) || thenMs <= 0 || nowMs < thenMs)
|
|
105
|
+
return null;
|
|
106
|
+
const sec = Math.floor((nowMs - thenMs) / 1000);
|
|
107
|
+
if (sec < 60)
|
|
108
|
+
return sec <= 1 ? '刚刚' : `${sec} 秒前`;
|
|
109
|
+
if (sec < 3600)
|
|
110
|
+
return `${Math.floor(sec / 60)} 分钟前`;
|
|
111
|
+
return `${Math.floor(sec / 3600)} 小时前`;
|
|
112
|
+
}
|
|
113
|
+
/** Sum an array of byte counts (per-workspace disk usage) into a single total. */
|
|
114
|
+
export function sumBytes(parts) {
|
|
115
|
+
let total = 0;
|
|
116
|
+
for (const p of parts)
|
|
117
|
+
total += p;
|
|
118
|
+
return total;
|
|
119
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -60,7 +60,7 @@ window.__ModuleLoader__.load({
|
|
|
60
60
|
//#region i18n ────────────────────────────────────────────────────────────
|
|
61
61
|
|
|
62
62
|
var zh = {
|
|
63
|
-
alertMemCluster: '
|
|
63
|
+
alertMemCluster: '性能监控',
|
|
64
64
|
monitorOn: '已开启',
|
|
65
65
|
monitorOff: '已关闭',
|
|
66
66
|
monitorConfig: '配置',
|
|
@@ -108,6 +108,11 @@ window.__ModuleLoader__.load({
|
|
|
108
108
|
alertHostGcWarning: 'GC 频率异常',
|
|
109
109
|
alertHostGcError: 'GC 频率极高',
|
|
110
110
|
alertHostGcMsg: 'Major {v} 次/min | 暂停 {pause} ms/min | Minor {minor} 次 | RSS {rss} MB | 堆 {heap}/{heapTotal} MB ({pct}%)',
|
|
111
|
+
// Alert disposal buttons (registered via `actions` — see dsh-flash contract)
|
|
112
|
+
alertDisposeDumpHeap: '立即转储堆快照',
|
|
113
|
+
alertDumping: '正在转储堆快照…',
|
|
114
|
+
alertDumpSaved: '已保存快照 {name}({size}),可在 DevTools → Memory → Load 中载入分析',
|
|
115
|
+
alertDumpFailed: '堆快照失败:{error}',
|
|
111
116
|
alertSevInfo: '信息',
|
|
112
117
|
alertSevWarning: '警告',
|
|
113
118
|
alertSevError: '错误',
|
|
@@ -153,10 +158,16 @@ window.__ModuleLoader__.load({
|
|
|
153
158
|
memTrendAxisHeap: '堆',
|
|
154
159
|
memTrendAxisTime: '时间',
|
|
155
160
|
memTrendInfo: '趋势信息',
|
|
161
|
+
// Session footprint
|
|
162
|
+
footprintTitle: '会话足迹',
|
|
163
|
+
footprintTotalSessions: '总会话数',
|
|
164
|
+
footprintArchivedSessions: '归档会话数',
|
|
165
|
+
footprintOnDisk: '磁盘占用',
|
|
166
|
+
footprintNoData: '暂无足迹数据',
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
var en = {
|
|
159
|
-
alertMemCluster: '
|
|
170
|
+
alertMemCluster: 'Performance Monitor',
|
|
160
171
|
monitorOn: 'Enabled',
|
|
161
172
|
monitorOff: 'Disabled',
|
|
162
173
|
monitorConfig: 'Configure',
|
|
@@ -204,6 +215,11 @@ window.__ModuleLoader__.load({
|
|
|
204
215
|
alertHostGcWarning: 'GC frequency abnormal',
|
|
205
216
|
alertHostGcError: 'GC frequency critical',
|
|
206
217
|
alertHostGcMsg: 'Major {v}/min | Pause {pause} ms/min | Minor {minor} | RSS {rss} MB | Heap {heap}/{heapTotal} MB ({pct}%)',
|
|
218
|
+
// Alert disposal buttons (registered via `actions` — see dsh-flash contract)
|
|
219
|
+
alertDisposeDumpHeap: 'Dump heap now',
|
|
220
|
+
alertDumping: 'Dumping heap snapshot…',
|
|
221
|
+
alertDumpSaved: 'Saved snapshot {name} ({size}). Load it in DevTools → Memory to analyze.',
|
|
222
|
+
alertDumpFailed: 'Heap dump failed: {error}',
|
|
207
223
|
alertSevInfo: 'Info',
|
|
208
224
|
alertSevWarning: 'Warning',
|
|
209
225
|
alertSevError: 'Error',
|
|
@@ -249,6 +265,12 @@ window.__ModuleLoader__.load({
|
|
|
249
265
|
memTrendAxisHeap: 'Heap',
|
|
250
266
|
memTrendAxisTime: 'Time',
|
|
251
267
|
memTrendInfo: 'Trend Info',
|
|
268
|
+
// Session footprint
|
|
269
|
+
footprintTitle: 'Session Footprint',
|
|
270
|
+
footprintTotalSessions: 'Total Sessions',
|
|
271
|
+
footprintArchivedSessions: 'Archived Sessions',
|
|
272
|
+
footprintOnDisk: 'On Disk',
|
|
273
|
+
footprintNoData: 'No footprint data yet',
|
|
252
274
|
}
|
|
253
275
|
|
|
254
276
|
// ── i18n: a proxy over DSH's official locale framework ─────────────────
|
|
@@ -438,7 +460,7 @@ window.__ModuleLoader__.load({
|
|
|
438
460
|
memThresholdWarning: 512,
|
|
439
461
|
memThresholdError: 1024,
|
|
440
462
|
memThresholdCritical: 1536,
|
|
441
|
-
memPollBase:
|
|
463
|
+
memPollBase: 60000,
|
|
442
464
|
memPollMin: 2000,
|
|
443
465
|
gcThresholdInfo: 2,
|
|
444
466
|
gcThresholdWarning: 5,
|
|
@@ -696,6 +718,52 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
696
718
|
disarmAbsent()
|
|
697
719
|
}
|
|
698
720
|
|
|
721
|
+
/** Disposal actions attached to every memory/GC alert. dsh-flash renders
|
|
722
|
+
* one button per entry and calls onClick(alert, api). Contract in
|
|
723
|
+
* dsh-flash/lib/client.js `_showAlertDetail` → plugin-registered action
|
|
724
|
+
* buttons. Returning a Promise keeps the button disabled until settled;
|
|
725
|
+
* returning {message} keeps the detail modal open so the saved location
|
|
726
|
+
* stays visible (the user pulls down the .heapsnapshot browser download
|
|
727
|
+
* we trigger programmatically — the status line is plain text, not HTML,
|
|
728
|
+
* so we cannot render a live link into it). */
|
|
729
|
+
function _dumpHeapActions() {
|
|
730
|
+
return [{
|
|
731
|
+
id: 'dump-heap',
|
|
732
|
+
label: function () { return t('alertDisposeDumpHeap') },
|
|
733
|
+
primary: true,
|
|
734
|
+
onClick: function (_alert, api) {
|
|
735
|
+
api.status(t('alertDumping'), 'info')
|
|
736
|
+
return fetch('/plugins/dsh-flash-mem-mon/dump-heap', { method: 'POST' })
|
|
737
|
+
.then(function (r) { return r.json().catch(function () { return null }) })
|
|
738
|
+
.then(function (d) {
|
|
739
|
+
if (!d || !d.ok || !d.name) {
|
|
740
|
+
var err = (d && d.error) || 'unknown'
|
|
741
|
+
api.status(t('alertDumpFailed').replace('{error}', err), 'error')
|
|
742
|
+
return { message: t('alertDumpFailed').replace('{error}', err), level: 'error' }
|
|
743
|
+
}
|
|
744
|
+
// Stream the snapshot down as a browser download (same origin).
|
|
745
|
+
var a = document.createElement('a')
|
|
746
|
+
a.href = '/plugins/dsh-flash-mem-mon/heap-dump'
|
|
747
|
+
a.download = d.name
|
|
748
|
+
document.body.appendChild(a)
|
|
749
|
+
a.click()
|
|
750
|
+
a.remove()
|
|
751
|
+
var size = d.size >= 1048576
|
|
752
|
+
? (d.size / 1048576).toFixed(1) + ' MB'
|
|
753
|
+
: Math.max(1, Math.round(d.size / 1024)) + ' KB'
|
|
754
|
+
var msg = t('alertDumpSaved').replace('{name}', d.name).replace('{size}', size)
|
|
755
|
+
api.status(msg, 'info')
|
|
756
|
+
return { message: msg, level: 'info' }
|
|
757
|
+
})
|
|
758
|
+
.catch(function (err) {
|
|
759
|
+
var msg = t('alertDumpFailed').replace('{error}', (err && err.message) || 'network')
|
|
760
|
+
api.status(msg, 'error')
|
|
761
|
+
return { message: msg, level: 'error' }
|
|
762
|
+
})
|
|
763
|
+
},
|
|
764
|
+
}]
|
|
765
|
+
}
|
|
766
|
+
|
|
699
767
|
function poll() {
|
|
700
768
|
if (!callback) return
|
|
701
769
|
var alerts = []
|
|
@@ -757,6 +825,7 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
757
825
|
icon: '🔴',
|
|
758
826
|
timestamp: Date.now(),
|
|
759
827
|
dismissible: true,
|
|
828
|
+
actions: _dumpHeapActions(),
|
|
760
829
|
})
|
|
761
830
|
} else if (rssMB >= tErr) {
|
|
762
831
|
emitIfChanged(alerts, {
|
|
@@ -767,6 +836,7 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
767
836
|
icon: '🟠',
|
|
768
837
|
timestamp: Date.now(),
|
|
769
838
|
dismissible: true,
|
|
839
|
+
actions: _dumpHeapActions(),
|
|
770
840
|
})
|
|
771
841
|
} else if (rssMB >= tWarn) {
|
|
772
842
|
emitIfChanged(alerts, {
|
|
@@ -777,6 +847,7 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
777
847
|
icon: '🟡',
|
|
778
848
|
timestamp: Date.now(),
|
|
779
849
|
dismissible: true,
|
|
850
|
+
actions: _dumpHeapActions(),
|
|
780
851
|
})
|
|
781
852
|
} else if (rssMB >= tInfo) {
|
|
782
853
|
emitIfChanged(alerts, {
|
|
@@ -787,6 +858,7 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
787
858
|
icon: '🔵',
|
|
788
859
|
timestamp: Date.now(),
|
|
789
860
|
dismissible: true,
|
|
861
|
+
actions: _dumpHeapActions(),
|
|
790
862
|
})
|
|
791
863
|
}
|
|
792
864
|
|
|
@@ -816,6 +888,7 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
816
888
|
icon: '📈',
|
|
817
889
|
timestamp: Date.now(),
|
|
818
890
|
dismissible: true,
|
|
891
|
+
actions: _dumpHeapActions(),
|
|
819
892
|
})
|
|
820
893
|
}
|
|
821
894
|
}
|
|
@@ -853,6 +926,7 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
853
926
|
icon: '🟠',
|
|
854
927
|
timestamp: Date.now(),
|
|
855
928
|
dismissible: true,
|
|
929
|
+
actions: _dumpHeapActions(),
|
|
856
930
|
})
|
|
857
931
|
} else if (mpm >= gcWarn) {
|
|
858
932
|
emitIfChanged(alerts, {
|
|
@@ -863,6 +937,7 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
863
937
|
icon: '🟡',
|
|
864
938
|
timestamp: Date.now(),
|
|
865
939
|
dismissible: true,
|
|
940
|
+
actions: _dumpHeapActions(),
|
|
866
941
|
})
|
|
867
942
|
} else if (mpm >= gcInfo) {
|
|
868
943
|
emitIfChanged(alerts, {
|
|
@@ -873,6 +948,7 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
873
948
|
icon: '🔵',
|
|
874
949
|
timestamp: Date.now(),
|
|
875
950
|
dismissible: true,
|
|
951
|
+
actions: _dumpHeapActions(),
|
|
876
952
|
})
|
|
877
953
|
}
|
|
878
954
|
}
|
|
@@ -1388,6 +1464,11 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
1388
1464
|
var _gcEvents = useState(null)
|
|
1389
1465
|
var gcEvents = _gcEvents[0], setGcEvents = _gcEvents[1]
|
|
1390
1466
|
|
|
1467
|
+
// Session footprint — total/archived session counts and on-disk usage
|
|
1468
|
+
// from our own /session-footprint route (cached host snapshot).
|
|
1469
|
+
var _footprint = useState(null)
|
|
1470
|
+
var footprint = _footprint[0], setFootprint = _footprint[1]
|
|
1471
|
+
|
|
1391
1472
|
// Sparkline hover index (null = not hovering).
|
|
1392
1473
|
var _hoverIdx = useState(null)
|
|
1393
1474
|
var hoverIdx = _hoverIdx[0], setHoverIdx = _hoverIdx[1]
|
|
@@ -1459,6 +1540,22 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
1459
1540
|
return function () { clearInterval(sampleTimer) }
|
|
1460
1541
|
}, [])
|
|
1461
1542
|
|
|
1543
|
+
// Session footprint — the host collector rescans no more often than every
|
|
1544
|
+
// ~5 min and serves a cached snapshot with the routes' last `scannedAt`, so
|
|
1545
|
+
// the panel is fetched only occasionally; a low-frequency fallback keeps
|
|
1546
|
+
// the panel live even if the shared cache never lands here.
|
|
1547
|
+
useEffect(function () {
|
|
1548
|
+
function fetchFootprint() {
|
|
1549
|
+
fetch('/plugins/dsh-flash-mem-mon/session-footprint')
|
|
1550
|
+
.then(function (r) { return r.json() })
|
|
1551
|
+
.then(function (d) { if (d) setFootprint(d) })
|
|
1552
|
+
.catch(function () {})
|
|
1553
|
+
}
|
|
1554
|
+
fetchFootprint()
|
|
1555
|
+
var footprintTimer = setInterval(fetchFootprint, 60000)
|
|
1556
|
+
return function () { clearInterval(footprintTimer) }
|
|
1557
|
+
}, [])
|
|
1558
|
+
|
|
1462
1559
|
// Render a single slider field row.
|
|
1463
1560
|
function renderSliderField(f) {
|
|
1464
1561
|
var val = Number(_alertPref(f.key)) || 0
|
|
@@ -2039,30 +2136,44 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
2039
2136
|
if (gcEvents && gcEvents.length > 0) {
|
|
2040
2137
|
var tsFirst = trendSamples[0].ts
|
|
2041
2138
|
var tsLast = trendSamples[trendSamples.length - 1].ts
|
|
2139
|
+
// Track which sample RSS-drop segments have already been attributed to a
|
|
2140
|
+
// Full GC event. Without this, two Full GCs that fire around the same
|
|
2141
|
+
// memory release can both grab the same -XX MB drop and double count it.
|
|
2142
|
+
var sampleDropClaimed = {}
|
|
2042
2143
|
for (var ei = 0; ei < gcEvents.length; ei++) {
|
|
2043
2144
|
var ge = gcEvents[ei]
|
|
2044
2145
|
if (ge.ts < tsFirst || ge.ts > tsLast) continue
|
|
2045
2146
|
// Try to find the RSS drop around this GC event from samples.
|
|
2046
2147
|
var rssDropMB = null
|
|
2148
|
+
var merged = false
|
|
2047
2149
|
// First: check if it merges with an existing sample-sourced RSS drop event (within 5s).
|
|
2048
|
-
var duped = false
|
|
2049
2150
|
for (var di = 0; di < dropEvents.length; di++) {
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2151
|
+
var dropItem = dropEvents[di]
|
|
2152
|
+
if (dropItem.source === 'sample' && Math.abs(dropItem.ts - ge.ts) < 5000) {
|
|
2153
|
+
if (!sampleDropClaimed[dropItem.sampleIdx]) {
|
|
2154
|
+
sampleDropClaimed[dropItem.sampleIdx] = true
|
|
2155
|
+
rssDropMB = dropItem.rssDropMB
|
|
2156
|
+
dropItem.source = 'gc'
|
|
2157
|
+
dropItem.duration = ge.duration
|
|
2158
|
+
dropItem.gcIdx = ei
|
|
2159
|
+
merged = true
|
|
2160
|
+
}
|
|
2161
|
+
// If this drop was already claimed by an earlier GC, don't
|
|
2162
|
+
// attribute it again — fall through and add this GC as its own
|
|
2163
|
+
// row without reusing the same memory-release number.
|
|
2056
2164
|
break
|
|
2057
2165
|
}
|
|
2058
2166
|
}
|
|
2059
|
-
// Second: if no existing RSS drop
|
|
2060
|
-
|
|
2167
|
+
// Second: if no existing RSS drop merged, scan nearby samples for a
|
|
2168
|
+
// drop, skipping segments already claimed by an earlier event.
|
|
2169
|
+
if (!merged) {
|
|
2061
2170
|
for (var si2 = 1; si2 < trendSamples.length; si2++) {
|
|
2062
2171
|
var timeDiff = Math.abs(trendSamples[si2].ts - ge.ts)
|
|
2063
2172
|
if (timeDiff > 10000) continue // only look within ±10s
|
|
2173
|
+
if (sampleDropClaimed[si2]) continue // drop already attributed elsewhere
|
|
2064
2174
|
var nearDrop = trendSamples[si2 - 1].rss - trendSamples[si2].rss
|
|
2065
2175
|
if (nearDrop > 0) {
|
|
2176
|
+
sampleDropClaimed[si2] = true
|
|
2066
2177
|
rssDropMB = (nearDrop / 1048576).toFixed(1)
|
|
2067
2178
|
break
|
|
2068
2179
|
}
|
|
@@ -2078,6 +2189,10 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
2078
2189
|
}
|
|
2079
2190
|
}
|
|
2080
2191
|
}
|
|
2192
|
+
// Merge both sources back into strict chronological order. A Full GC event
|
|
2193
|
+
// found outside any sample's RSS-drop window is appended after the sample
|
|
2194
|
+
// events, which may leave the combined list out of order.
|
|
2195
|
+
dropEvents.sort(function (a, b) { return a.ts - b.ts })
|
|
2081
2196
|
if (dropEvents.length === 0 && (!trendData.gc || trendData.gc.majorCount === 0)) {
|
|
2082
2197
|
return h('div', null,
|
|
2083
2198
|
h('span', { style: S.monitorConfigGroupTitle }, t('memGcEventList')),
|
|
@@ -2170,11 +2285,58 @@ var _PREFS_MAX_RETRIES = 8
|
|
|
2170
2285
|
)
|
|
2171
2286
|
}
|
|
2172
2287
|
|
|
2288
|
+
var footprintSectionEl = (function () {
|
|
2289
|
+
function formatBytesSync(n) {
|
|
2290
|
+
if (!isFinite(n) || n < 0) return '—'
|
|
2291
|
+
if (n < 1024) return n + ' B'
|
|
2292
|
+
var units = ['KB', 'MB', 'GB', 'TB', 'PB']
|
|
2293
|
+
var v = n
|
|
2294
|
+
var u = -1
|
|
2295
|
+
do { v /= 1024; u++ } while (v >= 1024 && u < units.length - 1)
|
|
2296
|
+
return v.toFixed(v >= 100 ? 0 : 1) + ' ' + units[u]
|
|
2297
|
+
}
|
|
2298
|
+
var fp = footprint
|
|
2299
|
+
var totalBytes = 0
|
|
2300
|
+
var totalSessions = 0
|
|
2301
|
+
var totalArchived = 0
|
|
2302
|
+
var archivedKnown = false
|
|
2303
|
+
if (fp && Array.isArray(fp.workspaces)) {
|
|
2304
|
+
fp.workspaces.forEach(function (w) {
|
|
2305
|
+
totalSessions += Number(w.totalSessions) || 0
|
|
2306
|
+
totalArchived += Number(w.archivedSessions) || 0
|
|
2307
|
+
totalBytes += Number(w.bytes) || 0
|
|
2308
|
+
if (typeof w.archivedSessions === 'number') archivedKnown = true
|
|
2309
|
+
})
|
|
2310
|
+
}
|
|
2311
|
+
if (!fp) {
|
|
2312
|
+
return h('div', { style: Object.assign({}, S.memColumnCard, { marginTop: '8px' }) },
|
|
2313
|
+
h('span', { style: S.memSubSectionLabel }, t('footprintTitle')),
|
|
2314
|
+
h('span', { style: S.memLiveDataLabel }, t('footprintNoData')),
|
|
2315
|
+
)
|
|
2316
|
+
}
|
|
2317
|
+
return h('div', { style: Object.assign({}, S.memColumnCard, { marginTop: '8px' }) },
|
|
2318
|
+
h('span', { style: S.memSubSectionLabel }, t('footprintTitle')),
|
|
2319
|
+
h('div', { style: S.memLiveDataGrid },
|
|
2320
|
+
h('span', { style: S.memLiveDataLabel }, t('footprintTotalSessions')),
|
|
2321
|
+
h('span', { style: S.memLiveDataValue }, String(totalSessions)),
|
|
2322
|
+
h('span', { style: S.memLiveDataLabel }, t('footprintArchivedSessions')),
|
|
2323
|
+
h('span', { style: S.memLiveDataValue },
|
|
2324
|
+
archivedKnown
|
|
2325
|
+
? String(totalArchived)
|
|
2326
|
+
: '—',
|
|
2327
|
+
),
|
|
2328
|
+
h('span', { style: S.memLiveDataLabel }, t('footprintOnDisk')),
|
|
2329
|
+
h('span', { style: S.memLiveDataValue }, formatBytesSync(totalBytes)),
|
|
2330
|
+
),
|
|
2331
|
+
)
|
|
2332
|
+
})()
|
|
2333
|
+
|
|
2173
2334
|
memLiveDataEl = h('div', { key: 'live-data' },
|
|
2174
2335
|
h('div', { style: S.monitorModalDivider }),
|
|
2175
2336
|
h('span', { style: S.monitorModalSectionTitle }, t('memLiveData')),
|
|
2176
2337
|
hostSectionEl,
|
|
2177
2338
|
trendSectionEl,
|
|
2339
|
+
footprintSectionEl,
|
|
2178
2340
|
)
|
|
2179
2341
|
|
|
2180
2342
|
return h('div', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-flash-mem-mon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Memory monitor provider for the `dsh-flash` Quick Control panel — registers a host-memory-alert provider (RSS absolute, RSS growth rate, Major GC frequency) via ctx.get('dockFlashAlerts') and exposes /memory-trend for trend sparklines.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|