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/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
+ }