dsh-flash-mem-mon 0.1.5 → 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 +39 -38
- package/README.zh-CN.md +38 -38
- package/cordis.patch.yml +11 -11
- package/dist/index.js +440 -116
- package/dist/pure.js +119 -0
- package/lib/client.js +2595 -2071
- package/package.json +23 -14
package/dist/pure.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Pure, dependency-free helpers extracted from index.ts so they can be unit
|
|
2
|
+
// tested without pulling in cordis / node HTTP / GC-entry types. Everything
|
|
3
|
+
// here is a pure function of its inputs.
|
|
4
|
+
export const DEFAULT_ALERT_CONFIG = {
|
|
5
|
+
memThresholdInfo: 256,
|
|
6
|
+
memThresholdWarning: 512,
|
|
7
|
+
memThresholdError: 1024,
|
|
8
|
+
memThresholdCritical: 1536,
|
|
9
|
+
memPollBase: 60000,
|
|
10
|
+
memPollMin: 2000,
|
|
11
|
+
gcThresholdInfo: 2,
|
|
12
|
+
gcThresholdWarning: 5,
|
|
13
|
+
gcThresholdError: 10,
|
|
14
|
+
memGrowthAlertPerMin: 1,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Classify a V8 GC `kind` bitmask as a full (major) cycle.
|
|
18
|
+
*
|
|
19
|
+
* V8 GC entry `kind` is a bitmask:
|
|
20
|
+
* 1 << 0 = Scavenge (minor / young-gen)
|
|
21
|
+
* 1 << 1 = MarkSweepCompact (major / full)
|
|
22
|
+
* 1 << 2 = IncrementalMarking
|
|
23
|
+
* 1 << 3 = ProcessWeakCallbacks
|
|
24
|
+
*
|
|
25
|
+
* In practice modern V8/Node reports a full MarkSweepCompact cycle through the
|
|
26
|
+
* IncrementalMarking (4) and ProcessWeakCallbacks (8) phases — the standalone
|
|
27
|
+
* "major" bit (2) is almost never emitted on its own. Classifying Full GC by
|
|
28
|
+
* `kind === 2` alone therefore misses (nearly) every real full collection.
|
|
29
|
+
* The robust test: a Full GC is any NON-scavenge cycle, i.e. the Scavenge bit
|
|
30
|
+
* (1) is NOT set. This captures 2, 4, 6, 8, 10, 12, 14 while excluding minor
|
|
31
|
+
* (1) scavenges.
|
|
32
|
+
*/
|
|
33
|
+
export function isFullGc(kind) {
|
|
34
|
+
return (kind & 1) === 0 && kind > 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Linear-regression slope of `rss` over the last `tailCount` samples, expressed
|
|
38
|
+
* in MB/min. Returns 0 when there are not enough samples (fewer than 3) or the
|
|
39
|
+
* tail time span is not positive.
|
|
40
|
+
*
|
|
41
|
+
* The average sample interval MUST be derived from the tail's OWN first/last
|
|
42
|
+
* timestamps, NOT from the whole buffer span (see index.ts summary()). Using a
|
|
43
|
+
* larger span inflates the interval, deflates samplesPerMin, and under-reports
|
|
44
|
+
* MB/min — a monotonic-leak false negative.
|
|
45
|
+
*/
|
|
46
|
+
export function rssSlopePerMin(samples, tailCount = 20) {
|
|
47
|
+
const tail = samples.slice(-tailCount);
|
|
48
|
+
if (tail.length < 3)
|
|
49
|
+
return 0;
|
|
50
|
+
const n = tail.length;
|
|
51
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
|
|
52
|
+
for (let i = 0; i < n; i++) {
|
|
53
|
+
sumX += i;
|
|
54
|
+
sumY += tail[i].rss;
|
|
55
|
+
sumXY += i * tail[i].rss;
|
|
56
|
+
sumXX += i * i;
|
|
57
|
+
}
|
|
58
|
+
// slope = bytes per sample-index step.
|
|
59
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
60
|
+
const tailSpanMs = Math.max(tail[tail.length - 1].ts - tail[0].ts, 0);
|
|
61
|
+
if (tailSpanMs <= 0)
|
|
62
|
+
return 0;
|
|
63
|
+
const avgIntervalMs = tailSpanMs / (n - 1);
|
|
64
|
+
const samplesPerMinute = 60000 / avgIntervalMs;
|
|
65
|
+
return Math.round((slope * samplesPerMinute / 1048576) * 100) / 100;
|
|
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
|
+
}
|