claw-dashboard 1.9.0 → 2.0.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/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5236 -566
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -5
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1941 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1057 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- package/tests/worker-pool.test.js +522 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Pressure Detection Module
|
|
3
|
+
* Monitors dashboard process memory for long-running sessions
|
|
4
|
+
* Detects memory trends, sustained pressure, and potential leaks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import logger from './logger.js';
|
|
8
|
+
import config from './config.js';
|
|
9
|
+
import { RateLimiter } from './alerts.js';
|
|
10
|
+
|
|
11
|
+
const { MEMORY_PRESSURE } = config;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Memory pressure levels
|
|
15
|
+
*/
|
|
16
|
+
export const PressureLevel = {
|
|
17
|
+
NONE: 'none',
|
|
18
|
+
ELEVATED: 'elevated',
|
|
19
|
+
WARNING: 'warning',
|
|
20
|
+
CRITICAL: 'critical',
|
|
21
|
+
EMERGENCY: 'emergency',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Memory trend directions
|
|
26
|
+
*/
|
|
27
|
+
export const TrendDirection = {
|
|
28
|
+
STABLE: 'stable',
|
|
29
|
+
GROWING: 'growing',
|
|
30
|
+
SHRINKING: 'shrinking',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Memory sample for trend analysis
|
|
35
|
+
* @typedef {Object} MemorySample
|
|
36
|
+
* @property {number} timestamp - Unix timestamp
|
|
37
|
+
* @property {number} heapUsed - Heap used in MB
|
|
38
|
+
* @property {number} heapTotal - Heap total in MB
|
|
39
|
+
* @property {number} rss - RSS memory in MB
|
|
40
|
+
* @property {number} external - External memory in MB
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Memory pressure state
|
|
45
|
+
* @typedef {Object} PressureState
|
|
46
|
+
* @property {string} level - Current pressure level
|
|
47
|
+
* @property {number} heapUsedMB - Current heap usage in MB
|
|
48
|
+
* @property {number} heapTotalMB - Current heap total in MB
|
|
49
|
+
* @property {number} usagePercent - Current usage percentage
|
|
50
|
+
* @property {string} trend - Memory trend direction
|
|
51
|
+
* @property {number} trendRateMB - Memory growth rate in MB/minute
|
|
52
|
+
* @property {boolean} sustained - Whether pressure has been sustained
|
|
53
|
+
* @property {number} sustainedDurationMs - How long pressure has been sustained
|
|
54
|
+
* @property {string[]} recommendations - Action recommendations
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
class MemoryPressureDetector {
|
|
58
|
+
constructor() {
|
|
59
|
+
/** @type {MemorySample[]} */
|
|
60
|
+
this.samples = [];
|
|
61
|
+
this.maxSamples = MEMORY_PRESSURE.TREND.SAMPLE_COUNT * 2;
|
|
62
|
+
|
|
63
|
+
// Pressure state tracking
|
|
64
|
+
this.currentLevel = PressureLevel.NONE;
|
|
65
|
+
this.sustainedSince = null;
|
|
66
|
+
this.lastCheck = Date.now();
|
|
67
|
+
|
|
68
|
+
// Trend tracking
|
|
69
|
+
this.lastTrend = TrendDirection.STABLE;
|
|
70
|
+
this.lastTrendRate = 0;
|
|
71
|
+
|
|
72
|
+
// Alert rate limiter (separate from threshold alerts)
|
|
73
|
+
this.rateLimiter = new RateLimiter({
|
|
74
|
+
enabled: true,
|
|
75
|
+
windowMs: 300000, // 5 minutes between pressure alerts
|
|
76
|
+
maxAlerts: 3,
|
|
77
|
+
alwaysAllowCritical: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Statistics
|
|
81
|
+
this.stats = {
|
|
82
|
+
peakHeapMB: 0,
|
|
83
|
+
pressureEvents: 0,
|
|
84
|
+
sustainedEvents: 0,
|
|
85
|
+
lastPressureTime: null,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Callbacks
|
|
89
|
+
this.onPressureChange = null;
|
|
90
|
+
this.onSustainedPressure = null;
|
|
91
|
+
this.onEmergency = null;
|
|
92
|
+
|
|
93
|
+
this.isRunning = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Start memory pressure monitoring
|
|
98
|
+
*/
|
|
99
|
+
start() {
|
|
100
|
+
if (this.isRunning) return;
|
|
101
|
+
this.isRunning = true;
|
|
102
|
+
logger.debug('Memory pressure monitoring started');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stop memory pressure monitoring
|
|
107
|
+
*/
|
|
108
|
+
stop() {
|
|
109
|
+
this.isRunning = false;
|
|
110
|
+
logger.debug('Memory pressure monitoring stopped');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Record a memory sample
|
|
115
|
+
* @returns {MemorySample}
|
|
116
|
+
*/
|
|
117
|
+
recordSample() {
|
|
118
|
+
const usage = process.memoryUsage();
|
|
119
|
+
const sample = {
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
heapUsed: Math.round(usage.heapUsed / 1024 / 1024),
|
|
122
|
+
heapTotal: Math.round(usage.heapTotal / 1024 / 1024),
|
|
123
|
+
rss: Math.round(usage.rss / 1024 / 1024),
|
|
124
|
+
external: Math.round((usage.external || 0) / 1024 / 1024),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
this.samples.push(sample);
|
|
128
|
+
if (this.samples.length > this.maxSamples) {
|
|
129
|
+
this.samples.shift();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Update peak stats
|
|
133
|
+
if (sample.heapUsed > this.stats.peakHeapMB) {
|
|
134
|
+
this.stats.peakHeapMB = sample.heapUsed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return sample;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Analyze memory trend from samples
|
|
142
|
+
* @returns {{direction: string, rateMBPerMin: number}}
|
|
143
|
+
*/
|
|
144
|
+
analyzeTrend() {
|
|
145
|
+
if (this.samples.length < MEMORY_PRESSURE.TREND.SAMPLE_COUNT) {
|
|
146
|
+
return { direction: TrendDirection.STABLE, rateMBPerMin: 0 };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const recentSamples = this.samples.slice(-MEMORY_PRESSURE.TREND.SAMPLE_COUNT);
|
|
150
|
+
const first = recentSamples[0];
|
|
151
|
+
const last = recentSamples[recentSamples.length - 1];
|
|
152
|
+
const durationMinutes = (last.timestamp - first.timestamp) / 60000;
|
|
153
|
+
|
|
154
|
+
if (durationMinutes < 0.5) {
|
|
155
|
+
return { direction: TrendDirection.STABLE, rateMBPerMin: 0 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const growthMB = last.heapUsed - first.heapUsed;
|
|
159
|
+
const rateMBPerMin = growthMB / durationMinutes;
|
|
160
|
+
|
|
161
|
+
// Determine direction based on threshold
|
|
162
|
+
const threshold = MEMORY_PRESSURE.TREND.GROWTH_THRESHOLD_MB;
|
|
163
|
+
if (growthMB > threshold) {
|
|
164
|
+
return { direction: TrendDirection.GROWING, rateMBPerMin };
|
|
165
|
+
} else if (growthMB < -threshold) {
|
|
166
|
+
return { direction: TrendDirection.SHRINKING, rateMBPerMin };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { direction: TrendDirection.STABLE, rateMBPerMin };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Determine pressure level from heap usage
|
|
174
|
+
* @param {number} heapUsedMB
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
177
|
+
getPressureLevel(heapUsedMB) {
|
|
178
|
+
const { THRESHOLDS } = MEMORY_PRESSURE;
|
|
179
|
+
if (heapUsedMB >= THRESHOLDS.EMERGENCY_MB) return PressureLevel.EMERGENCY;
|
|
180
|
+
if (heapUsedMB >= THRESHOLDS.CRITICAL_MB) return PressureLevel.CRITICAL;
|
|
181
|
+
if (heapUsedMB >= THRESHOLDS.WARNING_MB) return PressureLevel.WARNING;
|
|
182
|
+
// Elevated is 75% of warning threshold
|
|
183
|
+
if (heapUsedMB >= THRESHOLDS.WARNING_MB * 0.75) return PressureLevel.ELEVATED;
|
|
184
|
+
return PressureLevel.NONE;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check for sustained pressure
|
|
189
|
+
* @param {string} level - Current pressure level
|
|
190
|
+
* @returns {{sustained: boolean, durationMs: number}}
|
|
191
|
+
*/
|
|
192
|
+
checkSustainedPressure(level) {
|
|
193
|
+
const isElevated = level !== PressureLevel.NONE && level !== PressureLevel.ELEVATED;
|
|
194
|
+
|
|
195
|
+
if (!isElevated) {
|
|
196
|
+
this.sustainedSince = null;
|
|
197
|
+
return { sustained: false, durationMs: 0 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!this.sustainedSince) {
|
|
201
|
+
this.sustainedSince = Date.now();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const durationMs = Date.now() - this.sustainedSince;
|
|
205
|
+
const sustained = durationMs >= MEMORY_PRESSURE.SUSTAINED.DURATION_MS;
|
|
206
|
+
|
|
207
|
+
return { sustained, durationMs };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get recommendations based on pressure state
|
|
212
|
+
* @param {PressureState} state
|
|
213
|
+
* @returns {string[]}
|
|
214
|
+
*/
|
|
215
|
+
getRecommendations(state) {
|
|
216
|
+
const recommendations = [];
|
|
217
|
+
|
|
218
|
+
if (state.level === PressureLevel.EMERGENCY) {
|
|
219
|
+
recommendations.push('Consider restarting the dashboard immediately');
|
|
220
|
+
recommendations.push('Check for memory leaks in custom widgets/plugins');
|
|
221
|
+
} else if (state.level === PressureLevel.CRITICAL) {
|
|
222
|
+
recommendations.push('Enable performance metrics to identify resource-heavy widgets');
|
|
223
|
+
recommendations.push('Consider disabling unused widgets');
|
|
224
|
+
if (state.trend === TrendDirection.GROWING) {
|
|
225
|
+
recommendations.push(`Memory growing at ${state.trendRateMB.toFixed(1)}MB/min - possible leak detected`);
|
|
226
|
+
}
|
|
227
|
+
} else if (state.level === PressureLevel.WARNING) {
|
|
228
|
+
if (state.trend === TrendDirection.GROWING) {
|
|
229
|
+
recommendations.push('Memory trend indicates potential leak - monitor closely');
|
|
230
|
+
}
|
|
231
|
+
recommendations.push('Dashboard memory is elevated but stable');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.samples.length > MEMORY_PRESSURE.TREND.SAMPLE_COUNT) {
|
|
235
|
+
const ageMs = Date.now() - this.samples[0].timestamp;
|
|
236
|
+
const ageHours = ageMs / (1000 * 60 * 60);
|
|
237
|
+
if (ageHours > 24) {
|
|
238
|
+
recommendations.push(`Dashboard has been running for ${ageHours.toFixed(1)} hours - consider periodic restarts`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return recommendations;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Perform memory pressure check
|
|
247
|
+
* @returns {PressureState}
|
|
248
|
+
*/
|
|
249
|
+
check() {
|
|
250
|
+
if (!this.isRunning) {
|
|
251
|
+
this.start();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const sample = this.recordSample();
|
|
255
|
+
const trend = this.analyzeTrend();
|
|
256
|
+
const level = this.getPressureLevel(sample.heapUsed);
|
|
257
|
+
const { sustained, durationMs } = this.checkSustainedPressure(level);
|
|
258
|
+
const usagePercent = sample.heapTotal > 0
|
|
259
|
+
? Math.round((sample.heapUsed / sample.heapTotal) * 100)
|
|
260
|
+
: 0;
|
|
261
|
+
|
|
262
|
+
const state = {
|
|
263
|
+
level,
|
|
264
|
+
heapUsedMB: sample.heapUsed,
|
|
265
|
+
heapTotalMB: sample.heapTotal,
|
|
266
|
+
usagePercent,
|
|
267
|
+
trend: trend.direction,
|
|
268
|
+
trendRateMB: trend.rateMBPerMin,
|
|
269
|
+
sustained,
|
|
270
|
+
sustainedDurationMs: durationMs,
|
|
271
|
+
recommendations: [],
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
state.recommendations = this.getRecommendations(state);
|
|
275
|
+
|
|
276
|
+
// Track state changes
|
|
277
|
+
if (level !== this.currentLevel) {
|
|
278
|
+
this.handleLevelChange(this.currentLevel, level, state);
|
|
279
|
+
this.currentLevel = level;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Handle sustained pressure
|
|
283
|
+
if (sustained && durationMs >= MEMORY_PRESSURE.SUSTAINED.DURATION_MS) {
|
|
284
|
+
this.handleSustainedPressure(state);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.lastTrend = trend.direction;
|
|
288
|
+
this.lastTrendRate = trend.rateMBPerMin;
|
|
289
|
+
this.lastCheck = Date.now();
|
|
290
|
+
|
|
291
|
+
return state;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Handle pressure level change
|
|
296
|
+
* @param {string} oldLevel
|
|
297
|
+
* @param {string} newLevel
|
|
298
|
+
* @param {PressureState} state
|
|
299
|
+
*/
|
|
300
|
+
handleLevelChange(oldLevel, newLevel, state) {
|
|
301
|
+
const escalation = [
|
|
302
|
+
PressureLevel.NONE,
|
|
303
|
+
PressureLevel.ELEVATED,
|
|
304
|
+
PressureLevel.WARNING,
|
|
305
|
+
PressureLevel.CRITICAL,
|
|
306
|
+
PressureLevel.EMERGENCY,
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const oldIndex = escalation.indexOf(oldLevel);
|
|
310
|
+
const newIndex = escalation.indexOf(newLevel);
|
|
311
|
+
|
|
312
|
+
if (newIndex > oldIndex) {
|
|
313
|
+
// Escalating
|
|
314
|
+
logger.warn(`Memory pressure escalating: ${oldLevel} -> ${newLevel} (${state.heapUsedMB}MB)`);
|
|
315
|
+
this.stats.pressureEvents++;
|
|
316
|
+
this.stats.lastPressureTime = Date.now();
|
|
317
|
+
|
|
318
|
+
if (newLevel === PressureLevel.EMERGENCY && this.onEmergency) {
|
|
319
|
+
this.onEmergency(state);
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
// De-escalating
|
|
323
|
+
logger.info(`Memory pressure de-escalating: ${oldLevel} -> ${newLevel} (${state.heapUsedMB}MB)`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (this.onPressureChange) {
|
|
327
|
+
this.onPressureChange(oldLevel, newLevel, state);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Handle sustained pressure
|
|
333
|
+
* @param {PressureState} state
|
|
334
|
+
*/
|
|
335
|
+
handleSustainedPressure(state) {
|
|
336
|
+
const rateLimitResult = this.rateLimiter.checkAndRecord('sustained-pressure', state.level);
|
|
337
|
+
if (!rateLimitResult.allowed) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
logger.warn(`Sustained memory pressure detected: ${state.level} for ${(state.sustainedDurationMs / 1000).toFixed(0)}s`);
|
|
342
|
+
this.stats.sustainedEvents++;
|
|
343
|
+
|
|
344
|
+
if (this.onSustainedPressure) {
|
|
345
|
+
this.onSustainedPressure(state);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Auto-actions
|
|
349
|
+
if (MEMORY_PRESSURE.ACTIONS.REQUEST_GC && global.gc) {
|
|
350
|
+
logger.debug('Requesting garbage collection');
|
|
351
|
+
try {
|
|
352
|
+
global.gc();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
logger.debug('GC request failed:', error.message);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get memory pressure status for display
|
|
361
|
+
* @returns {Object}
|
|
362
|
+
*/
|
|
363
|
+
getStatus() {
|
|
364
|
+
const latest = this.samples[this.samples.length - 1];
|
|
365
|
+
return {
|
|
366
|
+
isRunning: this.isRunning,
|
|
367
|
+
currentLevel: this.currentLevel,
|
|
368
|
+
samples: this.samples.length,
|
|
369
|
+
peakHeapMB: this.stats.peakHeapMB,
|
|
370
|
+
pressureEvents: this.stats.pressureEvents,
|
|
371
|
+
sustainedEvents: this.stats.sustainedEvents,
|
|
372
|
+
lastPressureTime: this.stats.lastPressureTime,
|
|
373
|
+
latest: latest || null,
|
|
374
|
+
trend: {
|
|
375
|
+
direction: this.lastTrend,
|
|
376
|
+
rateMBPerMin: this.lastTrendRate,
|
|
377
|
+
},
|
|
378
|
+
thresholds: MEMORY_PRESSURE.THRESHOLDS,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get formatted status string for display
|
|
384
|
+
* @returns {string}
|
|
385
|
+
*/
|
|
386
|
+
getStatusString() {
|
|
387
|
+
const status = this.getStatus();
|
|
388
|
+
const latest = status.latest;
|
|
389
|
+
|
|
390
|
+
if (!latest) {
|
|
391
|
+
return 'Memory pressure monitoring inactive';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const colors = {
|
|
395
|
+
[PressureLevel.NONE]: 'green-fg',
|
|
396
|
+
[PressureLevel.ELEVATED]: 'cyan-fg',
|
|
397
|
+
[PressureLevel.WARNING]: 'yellow-fg',
|
|
398
|
+
[PressureLevel.CRITICAL]: 'red-fg',
|
|
399
|
+
[PressureLevel.EMERGENCY]: 'red-fg',
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const color = colors[this.currentLevel] || 'white-fg';
|
|
403
|
+
const trendIcon = this.lastTrend === TrendDirection.GROWING ? '↑' :
|
|
404
|
+
this.lastTrend === TrendDirection.SHRINKING ? '↓' : '→';
|
|
405
|
+
|
|
406
|
+
return `{${color}}MEM:${latest.heapUsed}MB ${trendIcon}{/${color}}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Check if memory pressure is currently elevated
|
|
411
|
+
* @returns {boolean}
|
|
412
|
+
*/
|
|
413
|
+
isElevated() {
|
|
414
|
+
return this.currentLevel === PressureLevel.WARNING ||
|
|
415
|
+
this.currentLevel === PressureLevel.CRITICAL ||
|
|
416
|
+
this.currentLevel === PressureLevel.EMERGENCY;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Check if memory pressure is critical
|
|
421
|
+
* @returns {boolean}
|
|
422
|
+
*/
|
|
423
|
+
isCritical() {
|
|
424
|
+
return this.currentLevel === PressureLevel.CRITICAL ||
|
|
425
|
+
this.currentLevel === PressureLevel.EMERGENCY;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Reset all statistics and samples
|
|
430
|
+
*/
|
|
431
|
+
reset() {
|
|
432
|
+
this.samples = [];
|
|
433
|
+
this.currentLevel = PressureLevel.NONE;
|
|
434
|
+
this.sustainedSince = null;
|
|
435
|
+
this.lastTrend = TrendDirection.STABLE;
|
|
436
|
+
this.lastTrendRate = 0;
|
|
437
|
+
this.stats = {
|
|
438
|
+
peakHeapMB: 0,
|
|
439
|
+
pressureEvents: 0,
|
|
440
|
+
sustainedEvents: 0,
|
|
441
|
+
lastPressureTime: null,
|
|
442
|
+
};
|
|
443
|
+
this.rateLimiter.reset();
|
|
444
|
+
logger.debug('Memory pressure detector reset');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Get recommendations for current state
|
|
449
|
+
* @returns {string[]}
|
|
450
|
+
*/
|
|
451
|
+
getCurrentRecommendations() {
|
|
452
|
+
if (this.samples.length === 0) {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
const state = {
|
|
456
|
+
level: this.currentLevel,
|
|
457
|
+
trend: this.lastTrend,
|
|
458
|
+
trendRateMB: this.lastTrendRate,
|
|
459
|
+
heapUsedMB: this.samples[this.samples.length - 1]?.heapUsed || 0,
|
|
460
|
+
};
|
|
461
|
+
return this.getRecommendations(state);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Export singleton instance
|
|
466
|
+
export default new MemoryPressureDetector();
|
|
467
|
+
export { MemoryPressureDetector };
|