@fanboynz/network-scanner 2.0.56 → 2.0.57
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/.github/workflows/npm-publish.yml +3 -4
- package/lib/browserhealth.js +207 -179
- package/lib/ignore_similar.js +78 -209
- package/lib/post-processing.js +282 -356
- package/lib/smart-cache.js +347 -267
- package/nwss.js +7 -1
- package/package.json +3 -2
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
name: Publish to NPM
|
|
2
2
|
on:
|
|
3
|
-
|
|
4
|
-
branches: [ main, master ]
|
|
3
|
+
workflow_dispatch:
|
|
5
4
|
|
|
6
5
|
jobs:
|
|
7
6
|
publish:
|
|
8
7
|
runs-on: ubuntu-latest
|
|
9
8
|
permissions:
|
|
10
|
-
contents: write
|
|
9
|
+
contents: write
|
|
11
10
|
|
|
12
11
|
steps:
|
|
13
12
|
- uses: actions/checkout@v4
|
|
@@ -39,4 +38,4 @@ jobs:
|
|
|
39
38
|
- name: Push changes
|
|
40
39
|
run: git push --follow-tags
|
|
41
40
|
env:
|
|
42
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
41
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
package/lib/browserhealth.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { formatLogMessage, messageColors } = require('./colorize');
|
|
7
|
-
const { execSync } = require('child_process');
|
|
7
|
+
const { execSync, execFile } = require('child_process');
|
|
8
8
|
|
|
9
9
|
// Window cleanup delay constant
|
|
10
10
|
const WINDOW_CLEANUP_DELAY_MS = 15000;
|
|
@@ -20,15 +20,35 @@ const pageCreationTracker = new Map(); // Maps page -> creation timestamp
|
|
|
20
20
|
const pageUsageTracker = new Map(); // Maps page -> { lastActivity: timestamp, isProcessing: boolean }
|
|
21
21
|
const PAGE_IDLE_THRESHOLD = 25000; // 25 seconds of inactivity before considering page safe to clean
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Race a promise against a timeout, clearing the timer when the promise resolves/rejects.
|
|
25
|
+
* Prevents leaked setTimeout handles that hold closure references until they fire.
|
|
26
|
+
* @param {Promise} promise - The operation to race
|
|
27
|
+
* @param {number} ms - Timeout in milliseconds
|
|
28
|
+
* @param {string} msg - Error message on timeout
|
|
29
|
+
* @returns {Promise} Resolves/rejects with the operation result, or rejects on timeout
|
|
30
|
+
*/
|
|
31
|
+
function raceWithTimeout(promise, ms, msg) {
|
|
32
|
+
let timeoutId;
|
|
33
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
34
|
+
timeoutId = setTimeout(() => reject(new Error(msg)), ms);
|
|
35
|
+
});
|
|
36
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const BYTES_GB = 1073741824; // 1024^3
|
|
40
|
+
const BYTES_MB = 1048576; // 1024^2
|
|
41
|
+
const BYTES_KB = 1024;
|
|
42
|
+
|
|
23
43
|
/**
|
|
24
44
|
* Format bytes to human readable string
|
|
25
45
|
* @param {number} bytes
|
|
26
46
|
* @returns {string}
|
|
27
47
|
*/
|
|
28
48
|
function formatMemory(bytes) {
|
|
29
|
-
if (bytes >=
|
|
30
|
-
if (bytes >=
|
|
31
|
-
if (bytes >=
|
|
49
|
+
if (bytes >= BYTES_GB) return `${(bytes / BYTES_GB).toFixed(1)}GB`;
|
|
50
|
+
if (bytes >= BYTES_MB) return `${(bytes / BYTES_MB).toFixed(1)}MB`;
|
|
51
|
+
if (bytes >= BYTES_KB) return `${(bytes / BYTES_KB).toFixed(1)}KB`;
|
|
32
52
|
return `${bytes}B`;
|
|
33
53
|
}
|
|
34
54
|
|
|
@@ -80,9 +100,6 @@ async function performGroupWindowCleanup(browserInstance, groupDescription, forc
|
|
|
80
100
|
}
|
|
81
101
|
} else {
|
|
82
102
|
// Any page with actual content should be evaluated for closure
|
|
83
|
-
// Cache page state checks to avoid multiple browser calls
|
|
84
|
-
const isPageClosed = page.isClosed();
|
|
85
|
-
|
|
86
103
|
if (cleanupMode === "all") {
|
|
87
104
|
// Aggressive mode: close all content pages
|
|
88
105
|
pagesToClose.push(page);
|
|
@@ -119,49 +136,31 @@ async function performGroupWindowCleanup(browserInstance, groupDescription, forc
|
|
|
119
136
|
return result;
|
|
120
137
|
}
|
|
121
138
|
|
|
122
|
-
// Estimate memory usage before closing
|
|
139
|
+
// Estimate memory usage before closing (parallel for performance)
|
|
123
140
|
let totalEstimatedMemory = 0;
|
|
124
|
-
const
|
|
141
|
+
const DEFAULT_PAGE_MEMORY = 8 * 1024 * 1024; // 8MB default estimate
|
|
125
142
|
|
|
126
|
-
|
|
127
|
-
const page = pagesToClose[i];
|
|
128
|
-
let pageMemoryEstimate = 0;
|
|
129
|
-
|
|
143
|
+
const pageMemoryEstimates = await Promise.all(pagesToClose.map(async (page) => {
|
|
130
144
|
try {
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Puppeteer metrics provide various memory-related values
|
|
143
|
-
pageMemoryEstimate = (
|
|
144
|
-
(metrics.JSHeapUsedSize || 0) + // JavaScript heap
|
|
145
|
-
(metrics.JSHeapTotalSize || 0) * 0.1 + // Estimated overhead
|
|
146
|
-
(metrics.Nodes || 0) * 100 + // DOM nodes (rough estimate)
|
|
147
|
-
(metrics.JSEventListeners || 0) * 50 // Event listeners
|
|
148
|
-
);
|
|
149
|
-
} else {
|
|
150
|
-
// Fallback: rough estimate based on page complexity
|
|
151
|
-
pageMemoryEstimate = 8 * 1024 * 1024; // 8MB default estimate per page
|
|
152
|
-
}
|
|
145
|
+
if (page.isClosed()) return 0;
|
|
146
|
+
const metrics = await raceWithTimeout(
|
|
147
|
+
page.metrics(), 1000, 'metrics timeout'
|
|
148
|
+
);
|
|
149
|
+
if (metrics) {
|
|
150
|
+
return (
|
|
151
|
+
(metrics.JSHeapUsedSize || 0) +
|
|
152
|
+
(metrics.JSHeapTotalSize || 0) * 0.1 +
|
|
153
|
+
(metrics.Nodes || 0) * 100 +
|
|
154
|
+
(metrics.JSEventListeners || 0) * 50
|
|
155
|
+
);
|
|
153
156
|
}
|
|
157
|
+
return DEFAULT_PAGE_MEMORY;
|
|
154
158
|
} catch (metricsErr) {
|
|
155
|
-
|
|
156
|
-
pageMemoryEstimate = 8 * 1024 * 1024; // 8MB default
|
|
157
|
-
if (forceDebug) {
|
|
158
|
-
console.log(formatLogMessage('debug', `[group_window_cleanup] Could not get metrics for page ${i + 1}, using default estimate: ${metricsErr.message}`));
|
|
159
|
-
}
|
|
159
|
+
return DEFAULT_PAGE_MEMORY;
|
|
160
160
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
totalEstimatedMemory = pageMemoryEstimates.reduce((sum, mem) => sum + mem, 0);
|
|
165
164
|
|
|
166
165
|
// Close identified old/unused pages
|
|
167
166
|
const closePromises = pagesToClose.map(async (page, index) => {
|
|
@@ -188,10 +187,15 @@ async function performGroupWindowCleanup(browserInstance, groupDescription, forc
|
|
|
188
187
|
});
|
|
189
188
|
|
|
190
189
|
const closeResults = await Promise.all(closePromises);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
// Single-pass count + sum (avoids 2 intermediate array allocations from .filter())
|
|
191
|
+
let successfulCloses = 0;
|
|
192
|
+
let actualMemoryFreed = 0;
|
|
193
|
+
for (let i = 0; i < closeResults.length; i++) {
|
|
194
|
+
if (closeResults[i].success === true) {
|
|
195
|
+
successfulCloses++;
|
|
196
|
+
actualMemoryFreed += closeResults[i].estimatedMemory || 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
195
199
|
|
|
196
200
|
if (forceDebug) {
|
|
197
201
|
console.log(formatLogMessage('debug', `[group_window_cleanup] Closed ${successfulCloses}/${pagesToClose.length} old windows for completed group: ${groupDescription} after ${WINDOW_CLEANUP_DELAY_MS}ms delay`));
|
|
@@ -241,39 +245,33 @@ async function performGroupWindowCleanup(browserInstance, groupDescription, forc
|
|
|
241
245
|
*/
|
|
242
246
|
async function isPageSafeToClose(page, forceDebug) {
|
|
243
247
|
try {
|
|
244
|
-
|
|
245
|
-
const isPageClosed = page.isClosed();
|
|
246
|
-
if (isPageClosed) {
|
|
248
|
+
if (page.isClosed()) {
|
|
247
249
|
return true; // Already closed
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
// EXTRA SAFETY: Never close pages that might be in injection process
|
|
253
|
+
const now = Date.now();
|
|
251
254
|
try {
|
|
252
|
-
// Cache page.url() for safety checks
|
|
253
255
|
const pageUrl = page.url();
|
|
254
|
-
if (pageUrl && pageUrl !== 'about:blank' &&
|
|
256
|
+
if (pageUrl && pageUrl !== 'about:blank' && now - (pageCreationTracker.get(page) || 0) < 30000) {
|
|
255
257
|
return false; // Don't close recently created pages (within 30 seconds)
|
|
256
258
|
}
|
|
257
259
|
} catch (err) { /* ignore */ }
|
|
258
260
|
|
|
259
261
|
const usage = pageUsageTracker.get(page);
|
|
260
262
|
if (!usage) {
|
|
261
|
-
// No usage data - assume safe if page exists for a while
|
|
262
263
|
return true;
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
// Check if page is actively processing
|
|
266
266
|
if (usage.isProcessing) {
|
|
267
267
|
if (forceDebug) {
|
|
268
|
-
// Cache URL for debug logging
|
|
269
268
|
const pageUrl = page.url();
|
|
270
269
|
console.log(formatLogMessage('debug', `[realtime_cleanup] Page still processing: ${pageUrl.substring(0, 50)}...`));
|
|
271
270
|
}
|
|
272
271
|
return false;
|
|
273
272
|
}
|
|
274
273
|
|
|
275
|
-
|
|
276
|
-
const idleTime = Date.now() - usage.lastActivity;
|
|
274
|
+
const idleTime = now - usage.lastActivity;
|
|
277
275
|
const isSafe = idleTime >= PAGE_IDLE_THRESHOLD;
|
|
278
276
|
|
|
279
277
|
if (!isSafe && forceDebug) {
|
|
@@ -296,13 +294,15 @@ async function isPageSafeToClose(page, forceDebug) {
|
|
|
296
294
|
*/
|
|
297
295
|
function updatePageUsage(page, isProcessing = false) {
|
|
298
296
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
lastActivity
|
|
304
|
-
isProcessing
|
|
305
|
-
}
|
|
297
|
+
if (!page.isClosed()) {
|
|
298
|
+
const existing = pageUsageTracker.get(page);
|
|
299
|
+
if (existing) {
|
|
300
|
+
// Mutate in place -- same hidden class, no GC pressure
|
|
301
|
+
existing.lastActivity = Date.now();
|
|
302
|
+
existing.isProcessing = isProcessing;
|
|
303
|
+
} else {
|
|
304
|
+
pageUsageTracker.set(page, { lastActivity: Date.now(), isProcessing: isProcessing });
|
|
305
|
+
}
|
|
306
306
|
}
|
|
307
307
|
} catch (err) {
|
|
308
308
|
// Ignore errors in usage tracking
|
|
@@ -320,8 +320,6 @@ function updatePageUsage(page, isProcessing = false) {
|
|
|
320
320
|
*/
|
|
321
321
|
async function performRealtimeWindowCleanup(browserInstance, threshold = REALTIME_CLEANUP_THRESHOLD, forceDebug, totalDelay = 19000) {
|
|
322
322
|
try {
|
|
323
|
-
const allPages = await browserInstance.pages();
|
|
324
|
-
|
|
325
323
|
// Initialize result object with consistent shape
|
|
326
324
|
const result = {
|
|
327
325
|
success: false,
|
|
@@ -334,13 +332,22 @@ async function performRealtimeWindowCleanup(browserInstance, threshold = REALTIM
|
|
|
334
332
|
error: null
|
|
335
333
|
};
|
|
336
334
|
|
|
335
|
+
// Quick count check before waiting (avoid the expensive delay if unnecessary)
|
|
336
|
+
let quickPages;
|
|
337
|
+
try {
|
|
338
|
+
quickPages = await browserInstance.pages();
|
|
339
|
+
} catch (e) {
|
|
340
|
+
result.error = e.message;
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
337
344
|
// Skip cleanup if we don't have enough pages to warrant it
|
|
338
|
-
if (
|
|
345
|
+
if (quickPages.length <= Math.max(threshold, REALTIME_CLEANUP_MIN_PAGES)) {
|
|
339
346
|
if (forceDebug) {
|
|
340
|
-
console.log(formatLogMessage('debug', `[realtime_cleanup] Only ${
|
|
347
|
+
console.log(formatLogMessage('debug', `[realtime_cleanup] Only ${quickPages.length} pages open, threshold is ${threshold} - no cleanup needed`));
|
|
341
348
|
}
|
|
342
349
|
result.success = true;
|
|
343
|
-
result.totalPages =
|
|
350
|
+
result.totalPages = quickPages.length;
|
|
344
351
|
result.reason = 'below_threshold';
|
|
345
352
|
return result;
|
|
346
353
|
}
|
|
@@ -509,23 +516,35 @@ async function isPageFromPreviousScan(page, forceDebug) {
|
|
|
509
516
|
return false; // Don't close blank pages here, handled separately
|
|
510
517
|
}
|
|
511
518
|
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
if (title.includes('404') ||
|
|
518
|
-
title.includes('Error') ||
|
|
519
|
-
title.includes('Not Found') ||
|
|
520
|
-
title === '') {
|
|
521
|
-
return true;
|
|
522
|
-
}
|
|
523
|
-
} catch (titleErr) {
|
|
524
|
-
// If we can't get title, page might be in bad state
|
|
519
|
+
// Use tracker timestamp instead of expensive page.title() CDP call
|
|
520
|
+
const now = Date.now();
|
|
521
|
+
const createdAt = pageCreationTracker.get(page);
|
|
522
|
+
if (createdAt && now - createdAt > 120000) {
|
|
523
|
+
// Page is older than 2 minutes -- likely from a previous scan
|
|
525
524
|
return true;
|
|
526
525
|
}
|
|
527
526
|
|
|
528
|
-
//
|
|
527
|
+
// Check usage tracker -- idle pages are likely old
|
|
528
|
+
const usage = pageUsageTracker.get(page);
|
|
529
|
+
if (usage && !usage.isProcessing && now - usage.lastActivity > 60000) {
|
|
530
|
+
return true; // Idle for over 60 seconds
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Fallback: only use page.title() if trackers have no data
|
|
534
|
+
if (!createdAt && !usage) {
|
|
535
|
+
try {
|
|
536
|
+
const title = await page.title();
|
|
537
|
+
if (title.includes('404') ||
|
|
538
|
+
title.includes('Error') ||
|
|
539
|
+
title.includes('Not Found') ||
|
|
540
|
+
title === '') {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
} catch (titleErr) {
|
|
544
|
+
return true; // Can't get title = bad state
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
529
548
|
return false; // Conservative - don't close unless we're sure
|
|
530
549
|
} catch (err) {
|
|
531
550
|
if (forceDebug) {
|
|
@@ -550,6 +569,25 @@ function trackPageForRealtime(page) {
|
|
|
550
569
|
updatePageUsage(page, false); // Initialize usage tracking
|
|
551
570
|
}
|
|
552
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Purges stale entries from tracking Maps (pages that were closed without cleanup)
|
|
574
|
+
* Should be called periodically to prevent memory leaks
|
|
575
|
+
*/
|
|
576
|
+
function purgeStaleTrackers() {
|
|
577
|
+
for (const [page] of pageCreationTracker) {
|
|
578
|
+
try {
|
|
579
|
+
if (page.isClosed()) {
|
|
580
|
+
pageCreationTracker.delete(page);
|
|
581
|
+
pageUsageTracker.delete(page);
|
|
582
|
+
}
|
|
583
|
+
} catch (e) {
|
|
584
|
+
// Page reference is invalid, remove it
|
|
585
|
+
pageCreationTracker.delete(page);
|
|
586
|
+
pageUsageTracker.delete(page);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
553
591
|
/**
|
|
554
592
|
* Quick browser responsiveness test for use during page setup
|
|
555
593
|
* Designed to catch browser degradation between operations
|
|
@@ -559,12 +597,11 @@ function trackPageForRealtime(page) {
|
|
|
559
597
|
*/
|
|
560
598
|
async function isQuicklyResponsive(browserInstance, timeout = 3000) {
|
|
561
599
|
try {
|
|
562
|
-
await
|
|
563
|
-
browserInstance.version(),
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
]);
|
|
600
|
+
await raceWithTimeout(
|
|
601
|
+
browserInstance.version(),
|
|
602
|
+
timeout,
|
|
603
|
+
'Quick responsiveness timeout'
|
|
604
|
+
);
|
|
568
605
|
return true;
|
|
569
606
|
} catch (error) {
|
|
570
607
|
return false;
|
|
@@ -590,20 +627,18 @@ async function testNetworkCapability(browserInstance, timeout = 10000) {
|
|
|
590
627
|
|
|
591
628
|
try {
|
|
592
629
|
// Create test page
|
|
593
|
-
testPage = await
|
|
630
|
+
testPage = await raceWithTimeout(
|
|
594
631
|
browserInstance.newPage(),
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
]);
|
|
632
|
+
timeout,
|
|
633
|
+
'Test page creation timeout'
|
|
634
|
+
);
|
|
599
635
|
|
|
600
636
|
// Test network operations (the critical operation that's failing)
|
|
601
|
-
await
|
|
637
|
+
await raceWithTimeout(
|
|
602
638
|
testPage.setRequestInterception(true),
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
]);
|
|
639
|
+
timeout,
|
|
640
|
+
'Network.enable test timeout'
|
|
641
|
+
);
|
|
607
642
|
|
|
608
643
|
// Turn off interception and close
|
|
609
644
|
await testPage.setRequestInterception(false);
|
|
@@ -662,12 +697,11 @@ async function checkBrowserHealth(browserInstance, timeout = 8000) {
|
|
|
662
697
|
}
|
|
663
698
|
|
|
664
699
|
// Test 2: Try to get pages list with timeout
|
|
665
|
-
const pages = await
|
|
700
|
+
const pages = await raceWithTimeout(
|
|
666
701
|
browserInstance.pages(),
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
]);
|
|
702
|
+
timeout,
|
|
703
|
+
'Browser unresponsive - pages() timeout'
|
|
704
|
+
);
|
|
671
705
|
|
|
672
706
|
healthResult.pageCount = pages.length;
|
|
673
707
|
healthResult.responseTime = Date.now() - startTime;
|
|
@@ -677,23 +711,38 @@ async function checkBrowserHealth(browserInstance, timeout = 8000) {
|
|
|
677
711
|
healthResult.recommendations.push('Too many open pages - consider browser restart');
|
|
678
712
|
}
|
|
679
713
|
|
|
680
|
-
// Test 4:
|
|
714
|
+
// Test 4: Create a single test page to verify both browser functionality AND network capability
|
|
681
715
|
let testPage = null;
|
|
682
716
|
try {
|
|
683
|
-
testPage = await
|
|
717
|
+
testPage = await raceWithTimeout(
|
|
684
718
|
browserInstance.newPage(),
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
]);
|
|
719
|
+
timeout,
|
|
720
|
+
'Page creation timeout'
|
|
721
|
+
);
|
|
689
722
|
|
|
690
723
|
// Quick test navigation to about:blank
|
|
691
|
-
await
|
|
724
|
+
await raceWithTimeout(
|
|
692
725
|
testPage.goto('about:blank'),
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
726
|
+
timeout,
|
|
727
|
+
'Navigation timeout'
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
// Test 5: Network capability test on the same page (avoids creating a second test page)
|
|
731
|
+
try {
|
|
732
|
+
await raceWithTimeout(
|
|
733
|
+
testPage.setRequestInterception(true),
|
|
734
|
+
Math.min(timeout, 5000),
|
|
735
|
+
'Network.enable test timeout'
|
|
736
|
+
);
|
|
737
|
+
await testPage.setRequestInterception(false);
|
|
738
|
+
healthResult.networkCapable = true;
|
|
739
|
+
} catch (networkErr) {
|
|
740
|
+
healthResult.networkCapable = false;
|
|
741
|
+
healthResult.recommendations.push(`Network operations failing: ${networkErr.message}`);
|
|
742
|
+
if (networkErr.message.includes('Network.enable')) {
|
|
743
|
+
healthResult.criticalError = true;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
697
746
|
|
|
698
747
|
await testPage.close();
|
|
699
748
|
|
|
@@ -711,24 +760,13 @@ async function checkBrowserHealth(browserInstance, timeout = 8000) {
|
|
|
711
760
|
return healthResult;
|
|
712
761
|
}
|
|
713
762
|
|
|
714
|
-
// Test 5: Network capability test (critical for Network.enable issues)
|
|
715
|
-
const networkTest = await testNetworkCapability(browserInstance, Math.min(timeout, 5000));
|
|
716
|
-
healthResult.networkCapable = networkTest.capable;
|
|
717
|
-
|
|
718
|
-
if (!networkTest.capable) {
|
|
719
|
-
healthResult.recommendations.push(`Network operations failing: ${networkTest.error}`);
|
|
720
|
-
if (networkTest.error && networkTest.error.includes('Network.enable')) {
|
|
721
|
-
healthResult.criticalError = true;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
763
|
// Test 6: Check response time performance
|
|
726
764
|
if (healthResult.responseTime > 5000) {
|
|
727
765
|
healthResult.recommendations.push('Slow browser response - consider restart');
|
|
728
766
|
}
|
|
729
767
|
|
|
730
768
|
// If all tests pass (including network capability)
|
|
731
|
-
healthResult.healthy =
|
|
769
|
+
healthResult.healthy = healthResult.networkCapable; // Network capability is now critical for health
|
|
732
770
|
|
|
733
771
|
|
|
734
772
|
} catch (error) {
|
|
@@ -782,7 +820,12 @@ async function checkBrowserMemory(browserInstance) {
|
|
|
782
820
|
|
|
783
821
|
// Try to get process memory info (Linux/Unix)
|
|
784
822
|
try {
|
|
785
|
-
const memInfo =
|
|
823
|
+
const memInfo = await new Promise((resolve, reject) => {
|
|
824
|
+
execFile('ps', ['-p', String(browserProcess.pid), '-o', 'rss='], { encoding: 'utf8', timeout: 2000 }, (err, stdout) => {
|
|
825
|
+
if (err) reject(err);
|
|
826
|
+
else resolve(stdout);
|
|
827
|
+
});
|
|
828
|
+
});
|
|
786
829
|
const memoryKB = parseInt(memInfo.trim());
|
|
787
830
|
|
|
788
831
|
if (!isNaN(memoryKB)) {
|
|
@@ -811,40 +854,22 @@ async function checkBrowserMemory(browserInstance) {
|
|
|
811
854
|
return memoryResult;
|
|
812
855
|
}
|
|
813
856
|
|
|
814
|
-
|
|
857
|
+
/**
|
|
858
|
+
* Precompiled regex for critical protocol error detection (avoids array allocation per call)
|
|
859
|
+
*/
|
|
860
|
+
const CRITICAL_ERROR_REGEX = /Runtime\.callFunctionOn timed out|Protocol error|Target closed|Session closed|Connection closed|Browser has been closed|Runtime\.evaluate timed out|WebSocket is not open|WebSocket connection lost|Connection terminated|Network service crashed|Browser disconnected|CDP session invalid|Browser process exited|Navigation timeout of|Page crashed|Renderer process crashed|Network\.enable timed out|Network\.disable timed out|Network service not available/;
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Precompiled regex for restart recommendation detection
|
|
864
|
+
*/
|
|
865
|
+
const RESTART_RECOMMENDATION_REGEX = /restart required|High memory usage/;
|
|
866
|
+
|
|
867
|
+
/**
|
|
815
868
|
* Detects critical protocol errors that require immediate browser restart
|
|
816
869
|
*/
|
|
817
870
|
function isCriticalProtocolError(error) {
|
|
818
871
|
if (!error || !error.message) return false;
|
|
819
|
-
|
|
820
|
-
const criticalErrors = [
|
|
821
|
-
'Runtime.callFunctionOn timed out',
|
|
822
|
-
'Protocol error',
|
|
823
|
-
'Target closed',
|
|
824
|
-
'Session closed',
|
|
825
|
-
'Connection closed',
|
|
826
|
-
'Browser has been closed',
|
|
827
|
-
'Runtime.evaluate timed out',
|
|
828
|
-
// New Puppeteer 23.x critical errors
|
|
829
|
-
'WebSocket is not open',
|
|
830
|
-
'WebSocket connection lost',
|
|
831
|
-
'Connection terminated',
|
|
832
|
-
'Network service crashed',
|
|
833
|
-
'Browser disconnected',
|
|
834
|
-
'CDP session invalid',
|
|
835
|
-
'Browser process exited',
|
|
836
|
-
'Navigation timeout of',
|
|
837
|
-
'Page crashed',
|
|
838
|
-
'Renderer process crashed',
|
|
839
|
-
// Network-specific critical errors
|
|
840
|
-
'Network.enable timed out',
|
|
841
|
-
'Network.disable timed out',
|
|
842
|
-
'Network service not available'
|
|
843
|
-
];
|
|
844
|
-
|
|
845
|
-
return criticalErrors.some(criticalError =>
|
|
846
|
-
error.message.includes(criticalError)
|
|
847
|
-
);
|
|
872
|
+
return CRITICAL_ERROR_REGEX.test(error.message);
|
|
848
873
|
}
|
|
849
874
|
|
|
850
875
|
/**
|
|
@@ -871,12 +896,11 @@ async function testBrowserConnectivity(browserInstance, timeout = 2500) {
|
|
|
871
896
|
|
|
872
897
|
// Test 2: CDP responsiveness with version check
|
|
873
898
|
try {
|
|
874
|
-
const version = await
|
|
899
|
+
const version = await raceWithTimeout(
|
|
875
900
|
browserInstance.version(),
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
]);
|
|
901
|
+
timeout,
|
|
902
|
+
'CDP version check timeout'
|
|
903
|
+
);
|
|
880
904
|
|
|
881
905
|
connectivityResult.cdpResponsive = true;
|
|
882
906
|
connectivityResult.websocketHealthy = true; // If version works, WebSocket is healthy
|
|
@@ -936,12 +960,16 @@ async function performHealthAssessment(browserInstance, options = {}) {
|
|
|
936
960
|
assessment.memory = await checkBrowserMemory(browserInstance);
|
|
937
961
|
}
|
|
938
962
|
|
|
939
|
-
// Combine recommendations
|
|
940
|
-
assessment.recommendations =
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
963
|
+
// Combine recommendations (push avoids spread operator intermediate arrays)
|
|
964
|
+
assessment.recommendations = assessment.browser.recommendations.slice();
|
|
965
|
+
if (assessment.connectivity.error) {
|
|
966
|
+
assessment.recommendations.push(`Connectivity issue: ${assessment.connectivity.error}`);
|
|
967
|
+
}
|
|
968
|
+
if (assessment.memory.recommendations) {
|
|
969
|
+
for (let i = 0; i < assessment.memory.recommendations.length; i++) {
|
|
970
|
+
assessment.recommendations.push(assessment.memory.recommendations[i]);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
945
973
|
|
|
946
974
|
// Determine overall health and restart necessity
|
|
947
975
|
if (!assessment.browser.healthy) {
|
|
@@ -955,10 +983,7 @@ async function performHealthAssessment(browserInstance, options = {}) {
|
|
|
955
983
|
assessment.needsRestart = true;
|
|
956
984
|
} else if (assessment.recommendations.length > 0) {
|
|
957
985
|
assessment.overall = 'degraded';
|
|
958
|
-
assessment.needsRestart = assessment.recommendations.
|
|
959
|
-
rec.includes('restart required') ||
|
|
960
|
-
rec.includes('High memory usage')
|
|
961
|
-
);
|
|
986
|
+
assessment.needsRestart = RESTART_RECOMMENDATION_REGEX.test(assessment.recommendations.join('|'));
|
|
962
987
|
} else {
|
|
963
988
|
assessment.overall = 'healthy';
|
|
964
989
|
assessment.needsRestart = false;
|
|
@@ -1132,17 +1157,19 @@ async function cleanupPageBeforeReload(page, forceDebug = false) {
|
|
|
1132
1157
|
} catch(e) {}
|
|
1133
1158
|
});
|
|
1134
1159
|
|
|
1135
|
-
// Clear
|
|
1160
|
+
// Clear recent timers and intervals (cap to last 1000 to avoid massive loops)
|
|
1136
1161
|
const highestId = setTimeout(() => {}, 0);
|
|
1137
|
-
|
|
1162
|
+
const clearFrom = Math.max(0, highestId - 1000);
|
|
1163
|
+
for (let i = highestId; i >= clearFrom; i--) {
|
|
1138
1164
|
clearTimeout(i);
|
|
1139
1165
|
clearInterval(i);
|
|
1140
1166
|
}
|
|
1141
1167
|
|
|
1142
|
-
// Stop
|
|
1168
|
+
// Stop recent animations
|
|
1143
1169
|
if (typeof cancelAnimationFrame !== 'undefined') {
|
|
1144
1170
|
const highestRAF = requestAnimationFrame(() => {});
|
|
1145
|
-
|
|
1171
|
+
const clearRAFFrom = Math.max(0, highestRAF - 200);
|
|
1172
|
+
for (let i = highestRAF; i >= clearRAFFrom; i--) {
|
|
1146
1173
|
cancelAnimationFrame(i);
|
|
1147
1174
|
}
|
|
1148
1175
|
}
|
|
@@ -1198,5 +1225,6 @@ module.exports = {
|
|
|
1198
1225
|
isBrowserHealthy,
|
|
1199
1226
|
isCriticalProtocolError,
|
|
1200
1227
|
updatePageUsage,
|
|
1201
|
-
cleanupPageBeforeReload
|
|
1228
|
+
cleanupPageBeforeReload,
|
|
1229
|
+
purgeStaleTrackers
|
|
1202
1230
|
};
|