dsh-flash-mem-mon 0.1.4 → 0.1.7
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 +391 -372
- package/dist/pure.js +66 -0
- package/lib/client.js +2433 -2071
- package/package.json +25 -16
package/dist/index.js
CHANGED
|
@@ -1,372 +1,391 @@
|
|
|
1
|
-
// Default export only (`export default Schema`); there is no named `Schema`.
|
|
2
|
-
import Schema from '@deepseek-ai/schemastery';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
class MemoryTrendCollector {
|
|
43
|
-
_samples = [];
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this._timer
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
gcPauseMs
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1
|
+
// Default export only (`export default Schema`); there is no named `Schema`.
|
|
2
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
3
|
+
import { DEFAULT_ALERT_CONFIG, isFullGc, rssSlopePerMin } from './pure.js';
|
|
4
|
+
export const name = 'dsh-flash-mem-mon';
|
|
5
|
+
// No host-side service dependencies; all services are injected lazily.
|
|
6
|
+
export const inject = [];
|
|
7
|
+
// ── Memory alert thresholds (RSS MB) and GC thresholds (major GC/min) ──
|
|
8
|
+
// All values come from the single canonical DEFAULT_ALERT_CONFIG in
|
|
9
|
+
// ./pure.js. The browser client half mirrors these (see lib/client.js
|
|
10
|
+
// _MEM_ALERT_DEFAULTS); test/defaults-drift.test.ts guarantees they never
|
|
11
|
+
// drift from this source of truth.
|
|
12
|
+
const DEFAULT_MEM_THRESHOLD_INFO = DEFAULT_ALERT_CONFIG.memThresholdInfo;
|
|
13
|
+
const DEFAULT_MEM_THRESHOLD_WARNING = DEFAULT_ALERT_CONFIG.memThresholdWarning;
|
|
14
|
+
const DEFAULT_MEM_THRESHOLD_ERROR = DEFAULT_ALERT_CONFIG.memThresholdError;
|
|
15
|
+
const DEFAULT_MEM_THRESHOLD_CRITICAL = DEFAULT_ALERT_CONFIG.memThresholdCritical;
|
|
16
|
+
const DEFAULT_MEM_POLL_BASE = DEFAULT_ALERT_CONFIG.memPollBase;
|
|
17
|
+
const DEFAULT_MEM_POLL_MIN = DEFAULT_ALERT_CONFIG.memPollMin;
|
|
18
|
+
const DEFAULT_GC_THRESHOLD_INFO = DEFAULT_ALERT_CONFIG.gcThresholdInfo;
|
|
19
|
+
const DEFAULT_GC_THRESHOLD_WARNING = DEFAULT_ALERT_CONFIG.gcThresholdWarning;
|
|
20
|
+
const DEFAULT_GC_THRESHOLD_ERROR = DEFAULT_ALERT_CONFIG.gcThresholdError;
|
|
21
|
+
const DEFAULT_MEM_GROWTH_ALERT_PER_MIN = DEFAULT_ALERT_CONFIG.memGrowthAlertPerMin;
|
|
22
|
+
/**
|
|
23
|
+
* Sentinel passed to `query()` / `gcEvents()` as the `since` lower bound to
|
|
24
|
+
* mean "no lower bound — return ALL stored records". These methods treat any
|
|
25
|
+
* `since <= 0` (0 or undefined) as "all", and a positive value as a unix-ms
|
|
26
|
+
* timestamp to filter from. Keeping one explicit convention prevents callers
|
|
27
|
+
* from mixing up `0` vs `undefined` (both mean "all").
|
|
28
|
+
*/
|
|
29
|
+
const SINCE_ALL = 0;
|
|
30
|
+
export const Config = Schema.object({
|
|
31
|
+
memThresholdInfo: Schema.number().default(DEFAULT_MEM_THRESHOLD_INFO).volatile(),
|
|
32
|
+
memThresholdWarning: Schema.number().default(DEFAULT_MEM_THRESHOLD_WARNING).volatile(),
|
|
33
|
+
memThresholdError: Schema.number().default(DEFAULT_MEM_THRESHOLD_ERROR).volatile(),
|
|
34
|
+
memThresholdCritical: Schema.number().default(DEFAULT_MEM_THRESHOLD_CRITICAL).volatile(),
|
|
35
|
+
memPollBase: Schema.number().default(DEFAULT_MEM_POLL_BASE).volatile(),
|
|
36
|
+
memPollMin: Schema.number().default(DEFAULT_MEM_POLL_MIN).volatile(),
|
|
37
|
+
gcThresholdInfo: Schema.number().default(DEFAULT_GC_THRESHOLD_INFO).volatile(),
|
|
38
|
+
gcThresholdWarning: Schema.number().default(DEFAULT_GC_THRESHOLD_WARNING).volatile(),
|
|
39
|
+
gcThresholdError: Schema.number().default(DEFAULT_GC_THRESHOLD_ERROR).volatile(),
|
|
40
|
+
memGrowthAlertPerMin: Schema.number().default(DEFAULT_MEM_GROWTH_ALERT_PER_MIN).volatile(),
|
|
41
|
+
});
|
|
42
|
+
class MemoryTrendCollector {
|
|
43
|
+
_samples = [];
|
|
44
|
+
// The poll loop is self-scheduling via setInterval-like recursion with an
|
|
45
|
+
// ADAPTIVE delay (higher heap usage → shorter interval), so each cycle holds
|
|
46
|
+
// a fresh setTimeout handle rather than one fixed-interval setInterval.
|
|
47
|
+
_timer = null;
|
|
48
|
+
/** Monotonic loop generation. Bumped every start()/stop(); a poll() tick
|
|
49
|
+
* whose captured generation no longer matches the current one is a stale
|
|
50
|
+
* callback from a superseded loop and must NOT re-schedule itself. This
|
|
51
|
+
* closes the start/stop restart race where a queued pre-restart tick could
|
|
52
|
+
* otherwise clobber the new loop's timer handle and leak a second loop. */
|
|
53
|
+
_generation = 0;
|
|
54
|
+
_cap;
|
|
55
|
+
// GC monitoring via PerformanceObserver
|
|
56
|
+
_gcObserver = null; // PerformanceObserver | null
|
|
57
|
+
_gcEvents = [];
|
|
58
|
+
_gcEventsCap = 2000; // ~5 min at worst-case GC frequency
|
|
59
|
+
/** Separate cap for major GC events only — they are rare and valuable for diagnostics. */
|
|
60
|
+
_gcMajorEvents = [];
|
|
61
|
+
_gcMajorEventsCap = 500; // Full GC events are rare; keep up to 500 (~hours)
|
|
62
|
+
constructor(cap = 720) {
|
|
63
|
+
this._cap = Math.max(10, cap);
|
|
64
|
+
}
|
|
65
|
+
start(baseInterval = 30_000, minInterval = 5_000) {
|
|
66
|
+
this.stop();
|
|
67
|
+
const gen = ++this._generation;
|
|
68
|
+
// Fire-and-forget: GC observer can start asynchronously; the poll loop
|
|
69
|
+
// below begins immediately regardless, since it does not depend on GC.
|
|
70
|
+
this._startGcObserver().catch(() => { });
|
|
71
|
+
const poll = () => {
|
|
72
|
+
// Bail if this tick belongs to a superseded loop (a restart has bumped
|
|
73
|
+
// the generation since we were scheduled). The stopped loop then winds
|
|
74
|
+
// down on its own instead of fighting the fresh loop for this._timer.
|
|
75
|
+
if (gen !== this._generation)
|
|
76
|
+
return;
|
|
77
|
+
const mu = process.memoryUsage();
|
|
78
|
+
this._push({
|
|
79
|
+
ts: Date.now(),
|
|
80
|
+
rss: mu.rss,
|
|
81
|
+
heapTotal: mu.heapTotal,
|
|
82
|
+
heapUsed: mu.heapUsed,
|
|
83
|
+
external: mu.external,
|
|
84
|
+
arrayBuffers: mu.arrayBuffers,
|
|
85
|
+
});
|
|
86
|
+
// Adaptive: higher heap usage → shorter interval (sample more densely).
|
|
87
|
+
const ratio = mu.heapUsed / mu.heapTotal;
|
|
88
|
+
const next = Math.max(minInterval, Math.round(baseInterval * Math.pow(1 - Math.min(ratio, 1), 2) + minInterval));
|
|
89
|
+
this._timer = setTimeout(poll, next);
|
|
90
|
+
// Unref so the timer never keeps the process alive on its own.
|
|
91
|
+
if (this._timer && typeof this._timer === 'object' && 'unref' in this._timer) {
|
|
92
|
+
this._timer.unref();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
// First sample immediately.
|
|
96
|
+
poll();
|
|
97
|
+
}
|
|
98
|
+
stop() {
|
|
99
|
+
// Bump the generation BEFORE clearing so any in-flight/stale poll tick that
|
|
100
|
+
// is already queued (past clearTimeout's reach) recognises itself as stale
|
|
101
|
+
// and refuses to re-schedule, never resurrecting a stopped loop.
|
|
102
|
+
this._generation++;
|
|
103
|
+
if (this._timer !== null) {
|
|
104
|
+
clearTimeout(this._timer);
|
|
105
|
+
this._timer = null;
|
|
106
|
+
}
|
|
107
|
+
this._stopGcObserver();
|
|
108
|
+
}
|
|
109
|
+
/** Memory samples at or after `since` (unix ms). A falsy/absent `since`
|
|
110
|
+
* (`SINCE_ALL` / undefined) means "return all stored samples". */
|
|
111
|
+
query(since = SINCE_ALL) {
|
|
112
|
+
if (since <= 0)
|
|
113
|
+
return this._samples.slice();
|
|
114
|
+
return this._samples.filter(s => s.ts >= since);
|
|
115
|
+
}
|
|
116
|
+
/** Return Full (major) GC events for chart rendering.
|
|
117
|
+
* Each returned event is one real full-GC cycle (phases already coalesced
|
|
118
|
+
* in `_startGcObserver`) and is normalized to kind=2 so the client can
|
|
119
|
+
* always treat `gcEvents` as "Full GC events". A falsy/absent `since`
|
|
120
|
+
* (`SINCE_ALL` / undefined) means "return all stored events". */
|
|
121
|
+
gcEvents(since = SINCE_ALL) {
|
|
122
|
+
if (this._gcMajorEvents.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
const events = since > 0
|
|
125
|
+
? this._gcMajorEvents.filter(e => e.ts >= since)
|
|
126
|
+
: this._gcMajorEvents;
|
|
127
|
+
return events
|
|
128
|
+
.map(e => ({ ts: e.ts, kind: 2, duration: e.duration }))
|
|
129
|
+
.sort((a, b) => a.ts - b.ts);
|
|
130
|
+
}
|
|
131
|
+
summary(since) {
|
|
132
|
+
const data = this.query(since);
|
|
133
|
+
if (data.length === 0) {
|
|
134
|
+
return { current: null, peak: null, trend: 'stable', sampleCount: 0, heapRatio: 0, spanMs: 0, rssSlopePerMin: 0, gc: null };
|
|
135
|
+
}
|
|
136
|
+
const current = data[data.length - 1];
|
|
137
|
+
let peak = { heapUsed: 0, rss: 0, ts: 0 };
|
|
138
|
+
for (const s of data) {
|
|
139
|
+
if (s.heapUsed > peak.heapUsed)
|
|
140
|
+
peak = { heapUsed: s.heapUsed, rss: s.rss, ts: s.ts };
|
|
141
|
+
}
|
|
142
|
+
// Simple linear regression on heapUsed over the last 20 samples (or fewer).
|
|
143
|
+
const tail = data.slice(-20);
|
|
144
|
+
let trend = 'stable';
|
|
145
|
+
if (tail.length >= 3) {
|
|
146
|
+
const n = tail.length;
|
|
147
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
|
|
148
|
+
for (let i = 0; i < n; i++) {
|
|
149
|
+
sumX += i;
|
|
150
|
+
sumY += tail[i].heapUsed;
|
|
151
|
+
sumXY += i * tail[i].heapUsed;
|
|
152
|
+
sumXX += i * i;
|
|
153
|
+
}
|
|
154
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
155
|
+
// Threshold: ±0.5% of current heapUsed per sample-index step.
|
|
156
|
+
const threshold = current.heapUsed * 0.005;
|
|
157
|
+
if (slope > threshold)
|
|
158
|
+
trend = 'up';
|
|
159
|
+
else if (slope < -threshold)
|
|
160
|
+
trend = 'down';
|
|
161
|
+
}
|
|
162
|
+
const heapRatio = current.heapTotal > 0 ? current.heapUsed / current.heapTotal : 0;
|
|
163
|
+
const spanMs = data.length >= 2 ? data[data.length - 1].ts - data[0].ts : 0;
|
|
164
|
+
// RSS linear-regression slope per minute (MB/min); positive = growing.
|
|
165
|
+
// Delegated to the pure helper (src/pure.ts) so the same logic is unit-
|
|
166
|
+
// tested. The average interval comes from the tail's own time span — see
|
|
167
|
+
// rssSlopePerMin for why using the whole buffer span would under-report.
|
|
168
|
+
const rssSlope = rssSlopePerMin(data);
|
|
169
|
+
// GC stats over the last ~5 minutes.
|
|
170
|
+
const gc = this._gcSummary(5 * 60 * 1000);
|
|
171
|
+
return { current, peak, trend, sampleCount: data.length, heapRatio, spanMs, rssSlopePerMin: rssSlope, gc };
|
|
172
|
+
}
|
|
173
|
+
/** Aggregate GC events in the last `windowMs` milliseconds. */
|
|
174
|
+
_gcSummary(windowMs) {
|
|
175
|
+
if (this._gcEvents.length === 0 && this._gcMajorEvents.length === 0)
|
|
176
|
+
return null;
|
|
177
|
+
const cutoff = Date.now() - windowMs;
|
|
178
|
+
// Major (Full) count comes from the coalesced major-GC buffer so each
|
|
179
|
+
// real cycle counts once (not once per phase entry).
|
|
180
|
+
const majorRecent = this._gcMajorEvents.filter(e => e.ts >= cutoff);
|
|
181
|
+
const majorCount = majorRecent.length;
|
|
182
|
+
// Minor count + total pause come from the general ring buffer (which holds
|
|
183
|
+
// EVERY PerformanceObserver entry). A minor cycle is a Scavenge (kind bit 1
|
|
184
|
+
// set). NOTE: gcPauseMs is the SUM of every phase-entry duration in the
|
|
185
|
+
// window — one real Full GC is reported as multiple phases (kind 4 + 8), and
|
|
186
|
+
// each contributes its own duration, so gcPauseMs is the TOTAL stop-the-world
|
|
187
|
+
// pause across all cycles (minor + full), NOT per-cycle. majorCount, by
|
|
188
|
+
// contrast, comes from the separate coalesced buffer and counts each real
|
|
189
|
+
// Full GC cycle ONCE. The two buffers intentionally have different semantics:
|
|
190
|
+
// majorCount = distinct full collections; gcPauseMs = aggregate pauses.
|
|
191
|
+
let minorCount = 0, gcPauseMs = 0;
|
|
192
|
+
for (const e of this._gcEvents) {
|
|
193
|
+
if (e.ts < cutoff)
|
|
194
|
+
continue;
|
|
195
|
+
gcPauseMs += e.duration;
|
|
196
|
+
if ((e.kind & 1) !== 0)
|
|
197
|
+
minorCount++;
|
|
198
|
+
}
|
|
199
|
+
if (majorCount === 0 && minorCount === 0 && gcPauseMs === 0)
|
|
200
|
+
return null;
|
|
201
|
+
const windowMin = windowMs / 60000;
|
|
202
|
+
return {
|
|
203
|
+
minorCount,
|
|
204
|
+
majorCount,
|
|
205
|
+
majorPerMin: Math.round((majorCount / windowMin) * 100) / 100,
|
|
206
|
+
gcPauseMs: Math.round(gcPauseMs * 100) / 100,
|
|
207
|
+
gcPausePerMin: Math.round((gcPauseMs / windowMin) * 100) / 100,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async _startGcObserver() {
|
|
211
|
+
try {
|
|
212
|
+
// Dynamic import — perf_hooks may not be available in all environments.
|
|
213
|
+
// MUST use import(), not require(): this half is ESM, so require() throws
|
|
214
|
+
// ReferenceError into the catch — silently disabling GC monitoring forever.
|
|
215
|
+
const { PerformanceObserver } = await import('perf_hooks');
|
|
216
|
+
this._gcObserver = new PerformanceObserver((list) => {
|
|
217
|
+
for (const entry of list.getEntries()) {
|
|
218
|
+
// GC entry kinds form a bitmask (see isFullGc): 1=Scavenge(minor),
|
|
219
|
+
// 2/4/8 = MarkSweepCompact/IncrementalMarking/ProcessWeakCallbacks.
|
|
220
|
+
// A single real Full GC is usually reported as several phase entries
|
|
221
|
+
// (e.g. kind 4 + kind 8) within a few ms, so we coalesce them into
|
|
222
|
+
// one Full GC event below.
|
|
223
|
+
const kind = entry.kind ?? 0;
|
|
224
|
+
const event = { ts: Date.now(), kind, duration: entry.duration };
|
|
225
|
+
this._gcEvents.push(event);
|
|
226
|
+
// Also store full (major) GC events in a separate long-lived buffer,
|
|
227
|
+
// because they are rare and the general ring buffer may evict them.
|
|
228
|
+
if (isFullGc(kind)) {
|
|
229
|
+
const last = this._gcMajorEvents[this._gcMajorEvents.length - 1];
|
|
230
|
+
if (last && event.ts - last.ts < 500) {
|
|
231
|
+
// Same full-GC cycle (phase entries arriving back-to-back):
|
|
232
|
+
// merge into the existing event, keeping the worst-case pause.
|
|
233
|
+
last.duration = Math.max(last.duration, event.duration);
|
|
234
|
+
last.ts = event.ts;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
this._gcMajorEvents.push(event);
|
|
238
|
+
while (this._gcMajorEvents.length > this._gcMajorEventsCap)
|
|
239
|
+
this._gcMajorEvents.shift();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Cap the ring buffer.
|
|
244
|
+
while (this._gcEvents.length > this._gcEventsCap)
|
|
245
|
+
this._gcEvents.shift();
|
|
246
|
+
});
|
|
247
|
+
this._gcObserver.observe({ type: 'gc', buffered: true });
|
|
248
|
+
}
|
|
249
|
+
catch (_) {
|
|
250
|
+
// GC observation not available — gc will remain null in summaries.
|
|
251
|
+
this._gcObserver = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
_stopGcObserver() {
|
|
255
|
+
if (this._gcObserver) {
|
|
256
|
+
try {
|
|
257
|
+
this._gcObserver.disconnect();
|
|
258
|
+
}
|
|
259
|
+
catch (_) { }
|
|
260
|
+
this._gcObserver = null;
|
|
261
|
+
}
|
|
262
|
+
this._gcEvents = [];
|
|
263
|
+
this._gcMajorEvents = [];
|
|
264
|
+
}
|
|
265
|
+
_push(s) {
|
|
266
|
+
this._samples.push(s);
|
|
267
|
+
if (this._samples.length > this._cap)
|
|
268
|
+
this._samples.shift();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// ── Process-scoped singleton with an exclusive active owner ─────────────
|
|
272
|
+
//
|
|
273
|
+
// A single MemoryTrendCollector is shared across the whole process so all
|
|
274
|
+
// consumers see one coherent ring buffer (each sample carries its own ts).
|
|
275
|
+
// However, Cordis may apply() this plugin more than once (multiple profiles,
|
|
276
|
+
// hot reload) and dispose() them in any order. To avoid two collectors racing
|
|
277
|
+
// on the same module state — and to stop one stray dispose() from killing a
|
|
278
|
+
// collector another apply() is still using — ownership is explicit:
|
|
279
|
+
//
|
|
280
|
+
// _memoryTrend : the live collector (or null)
|
|
281
|
+
// _ownerCtx : the Context that currently owns it (or null)
|
|
282
|
+
//
|
|
283
|
+
// startTrend()/stopTrend() are idempotent and fail softly when the caller is
|
|
284
|
+
// not the current owner, so overlapping apply/dispose cycles stay safe.
|
|
285
|
+
let _memoryTrend = null;
|
|
286
|
+
let _ownerCtx = null;
|
|
287
|
+
/** Adopt the process-wide collector for `ctx`; no-op if it is already owned. */
|
|
288
|
+
function startTrend(ctx, config) {
|
|
289
|
+
if (_ownerCtx && _ownerCtx !== ctx)
|
|
290
|
+
return; // someone else owns it
|
|
291
|
+
if (!_memoryTrend)
|
|
292
|
+
_memoryTrend = new MemoryTrendCollector();
|
|
293
|
+
_ownerCtx = ctx;
|
|
294
|
+
_memoryTrend.start((config.memPollBase?.get?.() ?? config.memPollBase) || DEFAULT_MEM_POLL_BASE, (config.memPollMin?.get?.() ?? config.memPollMin) || DEFAULT_MEM_POLL_MIN);
|
|
295
|
+
}
|
|
296
|
+
function restartTrend(config) {
|
|
297
|
+
if (!_memoryTrend || !_ownerCtx)
|
|
298
|
+
return;
|
|
299
|
+
_memoryTrend.stop();
|
|
300
|
+
_memoryTrend.start((config.memPollBase?.get?.() ?? config.memPollBase) || DEFAULT_MEM_POLL_BASE, (config.memPollMin?.get?.() ?? config.memPollMin) || DEFAULT_MEM_POLL_MIN);
|
|
301
|
+
}
|
|
302
|
+
/** Relinquish ownership of `ctx`. Only the active owner may stop the shared
|
|
303
|
+
* collector; other ctx instances (already superseded) must not touch it. */
|
|
304
|
+
function stopTrend(ctx) {
|
|
305
|
+
if (_ownerCtx !== ctx)
|
|
306
|
+
return; // not the owner → leave the running collector alone
|
|
307
|
+
_ownerCtx = null;
|
|
308
|
+
_memoryTrend?.stop();
|
|
309
|
+
}
|
|
310
|
+
// ── Private HTTP helpers ────────────────────────────────────────────────
|
|
311
|
+
/** Send a JSON response with no-store cache control. */
|
|
312
|
+
function sendJson(res, status, payload) {
|
|
313
|
+
res.statusCode = status;
|
|
314
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
315
|
+
res.setHeader('cache-control', 'no-store');
|
|
316
|
+
res.end(JSON.stringify(payload));
|
|
317
|
+
}
|
|
318
|
+
// ── apply() ─────────────────────────────────────────────────────────────
|
|
319
|
+
export function apply(ctx, config) {
|
|
320
|
+
// ── Settings namespace registration ──────────────────────────────────
|
|
321
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
322
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
|
|
323
|
+
});
|
|
324
|
+
// ── Memory trend collector bootstrap ──────────────────────────────────
|
|
325
|
+
// One bounded ring buffer per process, owned exclusively by the active ctx.
|
|
326
|
+
// startTrend/stopTrend are idempotent and owner-guarded, so overlapping
|
|
327
|
+
// apply/dispose cycles never double-start or kill a still-live collector.
|
|
328
|
+
startTrend(ctx, config);
|
|
329
|
+
ctx.effect(() => {
|
|
330
|
+
const owner = ctx;
|
|
331
|
+
return () => { stopTrend(owner); };
|
|
332
|
+
}, 'dsh-flash-mem-mon: memory-trend collector lifecycle');
|
|
333
|
+
// ── Volatile-update handler for poll intervals ────────────────────────
|
|
334
|
+
// Restart the collector when memPollBase or memPollMin change.
|
|
335
|
+
ctx.on('loader/volatile-update', (paths) => {
|
|
336
|
+
const memPaths = ['memPollBase', 'memPollMin'];
|
|
337
|
+
if (!paths.some((pp) => pp.length && memPaths.includes(pp[pp.length - 1])))
|
|
338
|
+
return;
|
|
339
|
+
restartTrend(config);
|
|
340
|
+
});
|
|
341
|
+
// ── HTTP API route for client-side trend data ────────────────────────
|
|
342
|
+
// Uses a structural HostWebServer type instead of `any`. cordis's inject()
|
|
343
|
+
// callback is statically typed as Context (the real webServer augmentation
|
|
344
|
+
// lives in @deepseek-ai/dsh-host-webserver, not a compile-time dep here), so
|
|
345
|
+
// the injected service is read through one narrow cast inside the callback —
|
|
346
|
+
// when inject(keys) fires, the service is guaranteed present — then used
|
|
347
|
+
// under the strong HostWebServer type for the whole register/effect block.
|
|
348
|
+
ctx.inject(['webServer'], (wsCtx) => {
|
|
349
|
+
const ws = wsCtx.webServer;
|
|
350
|
+
// GET /plugins/dsh-flash-mem-mon/memory-trend
|
|
351
|
+
// Returns memory trend data for the client's config popup.
|
|
352
|
+
// `mode=summary` returns a lightweight snapshot (current, peak, trend direction);
|
|
353
|
+
// `mode=full` returns the raw samples for rendering a chart.
|
|
354
|
+
// Optional `since` (unix ms) limits the range.
|
|
355
|
+
wsCtx.effect(() => ws.register({
|
|
356
|
+
kind: 'exact',
|
|
357
|
+
path: '/plugins/dsh-flash-mem-mon/memory-trend',
|
|
358
|
+
handler: async (req, res) => {
|
|
359
|
+
if (req.method !== 'GET') {
|
|
360
|
+
res.statusCode = 405;
|
|
361
|
+
res.setHeader('allow', 'GET');
|
|
362
|
+
res.end();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!_memoryTrend) {
|
|
366
|
+
sendJson(res, 200, { samples: [], summary: null });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const u = new URL(req.url || '/', 'http://localhost');
|
|
370
|
+
const since = parseInt(u.searchParams.get('since') || '0', 10) || 0;
|
|
371
|
+
const mode = u.searchParams.get('mode') || 'summary';
|
|
372
|
+
if (mode === 'full') {
|
|
373
|
+
// `since` is already >= 0 here; 0 means "entire range" (SINCE_ALL),
|
|
374
|
+
// so both `query` and `gcEvents` share the same bounds convention.
|
|
375
|
+
// Only full (major) GC events are sent — minor GC is too frequent
|
|
376
|
+
// and clutters the chart. These are already coalesced into one
|
|
377
|
+
// event per real cycle by `_startGcObserver`. Minor GC stats are
|
|
378
|
+
// still available via mode=summary (majorPerMin, gcPausePerMin).
|
|
379
|
+
const majorGcEvents = _memoryTrend.gcEvents(since);
|
|
380
|
+
sendJson(res, 200, {
|
|
381
|
+
samples: _memoryTrend.query(since),
|
|
382
|
+
gcEvents: majorGcEvents,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
sendJson(res, 200, _memoryTrend.summary(since));
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
}), 'dsh-flash-mem-mon: GET /plugins/dsh-flash-mem-mon/memory-trend');
|
|
390
|
+
});
|
|
391
|
+
}
|