diff --git a/src/js/main.js b/src/js/main.js
index 7ef8d245..cebd1212 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -18,6 +18,16 @@ import { updateTabList } from "./utils/updateTabList.js";
import * as THREE from "three";
import NotificationManager from "./utils/notifications.js";
+import("./msp/msp_debug_tools.js")
+ .then(() => {
+ console.log("π§ MSP Debug Tools loaded for development environment");
+ console.log("β’ Press Ctrl+Shift+M to toggle debug dashboard");
+ console.log("β’ Use MSPTestRunner.help() for all commands");
+ })
+ .catch((err) => {
+ console.warn("Failed to load MSP debug tools:", err);
+ });
+
if (typeof String.prototype.replaceAll === "undefined") {
String.prototype.replaceAll = function (match, replace) {
return this.replace(new RegExp(match, "g"), () => replace);
@@ -118,7 +128,8 @@ function startProcess() {
console.log(`Libraries: jQuery - ${$.fn.jquery}, three.js - ${THREE.REVISION}`);
// Check if this is the first visit
- if (getConfig("firstRun").firstRun === undefined) {
+ const firstRunCfg = getConfig("firstRun") ?? {};
+ if (firstRunCfg.firstRun === undefined) {
setConfig({ firstRun: true });
import("./tabs/static_tab.js").then(({ staticTab }) => {
staticTab.initialize("options", () => {
diff --git a/src/js/msp.js b/src/js/msp.js
index 9a980a91..5886ce85 100644
--- a/src/js/msp.js
+++ b/src/js/msp.js
@@ -51,16 +51,13 @@ const MSP = {
message_buffer: null,
message_buffer_uint8_view: null,
message_checksum: 0,
- messageIsJumboFrame: false,
crcError: false,
callbacks: [],
packet_error: 0,
unsupported: 0,
- MIN_TIMEOUT: 200,
- MAX_TIMEOUT: 2000,
- timeout: 200,
+ TIMEOUT: 1000,
last_received_timestamp: null,
listeners: [],
@@ -374,28 +371,19 @@ const MSP = {
serial.send(bufferOut);
},
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
- const connected = serial.connected;
-
- if (code === undefined || !connected || CONFIGURATOR.virtualMode) {
+ if (code === undefined || !serial.connected || CONFIGURATOR.virtualMode) {
if (callback_msp) {
callback_msp();
}
return false;
}
- let requestExists = false;
- for (const instance of this.callbacks) {
- if (instance.code === code) {
- requestExists = true;
-
- break;
- }
- }
+ const requestExists = this.callbacks.some((instance) => instance.code === code);
const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
const obj = {
- code: code,
+ code,
requestBuffer: bufferOut,
callback: callback_msp,
callbackOnError: doCallbackOnError,
@@ -412,31 +400,31 @@ const MSP = {
serial.send(bufferOut, (_sendInfo) => {
obj.stop = performance.now();
const executionTime = Math.round(obj.stop - obj.start);
- this.timeout = Math.max(this.MIN_TIMEOUT, Math.min(executionTime, this.MAX_TIMEOUT));
+ // We should probably give up connection if the request takes too long ?
+ if (executionTime > 5000) {
+ console.warn(
+ `MSP: data request took too long: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} EXECUTION TIME: ${executionTime}ms`,
+ );
+ }
+
+ clearTimeout(obj.timer); // prevent leaks
});
- }, this.timeout);
+ }, this.TIMEOUT);
}
this.callbacks.push(obj);
// always send messages with data payload (even when there is a message already in the queue)
if (data || !requestExists) {
- if (this.timeout > this.MIN_TIMEOUT) {
- this.timeout--;
- }
-
serial.send(bufferOut, (sendInfo) => {
- if (sendInfo.bytesSent === bufferOut.byteLength) {
- if (callback_sent) {
- callback_sent();
- }
+ if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) {
+ callback_sent();
}
});
}
return true;
},
-
/**
* resolves: {command: code, data: data, length: message_length}
*/
diff --git a/src/js/msp/MSP_DEBUG_README.md b/src/js/msp/MSP_DEBUG_README.md
new file mode 100644
index 00000000..687a23b4
--- /dev/null
+++ b/src/js/msp/MSP_DEBUG_README.md
@@ -0,0 +1,359 @@
+# MSP Debug Tools
+
+Comprehensive monitoring and stress testing tools for the MSP (MultiWii Serial Protocol) implementation in Betaflight Configurator.
+
+## Features
+
+π **Real-time Queue Monitoring**
+- Track queue size, response times, and success rates
+- Detect memory leaks and performance bottlenecks
+- Alert system for potential issues
+
+π§ͺ **Comprehensive Stress Testing**
+- Queue flooding tests
+- Timeout recovery validation
+- Memory leak detection
+- Performance under load testing
+
+π **Visual Dashboard**
+- Real-time metrics display with smart updates
+- Live charts and graphs
+- Queue analysis tools
+- Test result visualization
+- **Interactive-friendly updates**: Dashboard pauses updates during user interactions
+- **Clickable test results**: Click on any test result for detailed information
+- Visual pause indicators when updates are suspended
+
+β‘ **Easy-to-use API**
+- Console commands for quick testing
+- Programmable test scenarios
+- Detailed reporting and export
+
+## Quick Start
+
+### 1. Load the Debug Tools
+
+Include the debug tools in your page:
+
+```javascript
+import './src/js/msp_debug_tools.js';
+```
+
+Or in development, load via console:
+```javascript
+import('./src/js/msp_debug_tools.js');
+```
+
+### 2. Basic Usage
+
+**Start monitoring:**
+```javascript
+MSPDebug.startMonitoring();
+```
+
+**Show visual dashboard:**
+```javascript
+MSPDebug.show();
+// Or press Ctrl+Shift+M
+```
+
+**Quick test of alert system:**
+```javascript
+MSPDebug.testAlerts();
+```
+
+**Run stress tests:**
+```javascript
+// Run specific test
+MSPDebug.runTests();
+
+// Run complete stress test suite with detailed console output
+MSPDebug.runFullSuite();
+
+// Run individual test by name
+MSPDebug.runTest('queue-flooding');
+
+// Quick health check
+MSPDebug.quickHealthCheck();
+
+// Run stress scenario
+MSPDebug.stressScenario('high-frequency');
+```
+
+## Available Commands
+
+The MSP Debug Tools provide two APIs:
+- **MSPDebug**: Modern, simplified API (recommended)
+- **MSPTestRunner**: Legacy API with additional methods
+
+Both APIs provide the same core functionality. Use `MSPDebug` for new code.
+
+### Monitoring Commands
+
+| Command | Description |
+|---------|-------------|
+| `MSPDebug.startMonitoring()` | Start monitoring with console output |
+| `MSPDebug.stopMonitoring()` | Stop monitoring |
+| `MSPDebug.getStatus()` | Get current MSP status |
+| `MSPDebug.monitor.getStatus()` | Get current MSP status (alternative) |
+| `MSPDebug.analyze()` | Analyze current queue contents |
+
+### Testing Commands
+
+| Command | Description |
+|---------|-------------|
+| `MSPDebug.runTests()` | Run stress test suite |
+| `MSPDebug.runFullSuite()` | Run complete stress test suite with detailed output |
+| `MSPDebug.runTest('test-name')` | Run a specific test by name |
+| `MSPDebug.quickHealthCheck()` | Run a quick MSP health check |
+| `MSPDebug.stressScenario('scenario')` | Run specific stress test scenario |
+| `MSPDebug.testAlerts()` | Test alert system |
+| `MSPDebug.triggerTestAlerts()` | Manually trigger alerts |
+
+### Alert Testing
+
+| Command | Description |
+|---------|-------------|
+| `MSPDebug.setTestThresholds()` | Lower thresholds for easier testing |
+| `MSPDebug.setNormalThresholds()` | Restore normal thresholds |
+| `MSPDebug.testAlerts()` | Complete alert system test |
+
+### Visual Tools
+
+| Command | Description |
+|---------|-------------|
+| `MSPDebug.show()` | Show visual debug dashboard |
+| `MSPDebug.report()` | Generate and download report |
+
+## Dashboard Interactions
+
+The visual dashboard includes smart interaction handling to ensure a smooth user experience:
+
+### Automatic Update Pausing
+
+- **Mouse hover**: Updates pause for 3 seconds when hovering over interactive elements
+- **Click events**: Updates pause for 5 seconds when clicking buttons or test results
+- **Focus events**: Updates pause for 10 seconds when focusing on input elements
+- **Visual indicator**: Orange "Updates Paused" indicator appears when updates are suspended
+
+### Clickable Test Results
+
+- Click on any test result item to see detailed information including:
+ - Full error messages
+ - Performance metrics
+ - JSON response data
+ - Test duration and status
+- Details remain stable and clickable while displayed
+- Use the "Close Details" button to dismiss and resume normal updates
+
+### Interactive Elements
+
+- All buttons remain stable during interactions
+- Queue analysis results are preserved during examination
+- Export functionality works without interference from updates
+
+### Keyboard Shortcuts
+
+- **Ctrl+Shift+M**: Toggle dashboard visibility
+- Use console commands for programmatic control
+
+## Available Tests
+
+### Individual Test Names (for `runTest`)
+
+1. **queue-flooding** - Tests queue limits with many simultaneous requests
+2. **rapid-fire** - Tests high-frequency request handling
+3. **duplicates** - Validates duplicate request management
+4. **timeout-recovery** - Tests timeout and retry mechanisms
+5. **memory-leaks** - Checks for proper cleanup of completed requests
+6. **concurrent-mixed** - Tests various request types simultaneously
+7. **queue-overflow** - Tests behavior when queue reaches capacity
+8. **connection-disruption** - Simulates connection issues
+9. **performance-load** - Tests sustained load performance
+
+### Stress Scenarios (for `stressScenario`)
+
+- **high-frequency** - High-frequency requests every 10ms for 5 seconds
+- **queue-overflow** - Floods queue beyond capacity
+- **mixed-load** - Various request types and sizes
+
+### Full Test Suite
+
+The `runFullSuite()` command runs all individual tests in sequence with detailed console output and generates a comprehensive report.
+
+## Monitoring Metrics
+
+The tools track various metrics:
+
+- **Queue Size**: Current number of pending requests
+- **Response Times**: Average, minimum, and maximum response times
+- **Success Rate**: Percentage of successful requests
+- **Timeout Rate**: Percentage of requests that timeout
+- **Request Distribution**: Breakdown by MSP command codes
+- **Error Tracking**: Categorized error types and frequencies
+
+## Alert System
+
+The monitoring system provides alerts for:
+
+- π¨ **Queue Full**: Queue approaching capacity
+- β±οΈ **High Timeout Rate**: Excessive request timeouts
+- π **Slow Responses**: Average response time too high
+- πΎ **Memory Leak**: Callbacks not being cleaned up properly
+
+## Dashboard Features
+
+The visual dashboard provides:
+
+- **Real-time Status**: Current queue state and metrics
+- **Live Charts**: Queue size and response time trends
+- **Queue Analysis**: Detailed breakdown of pending requests
+- **Alert Display**: Active alerts and warnings
+- **Test Integration**: Run tests directly from the UI
+- **Export Tools**: Generate and download reports
+
+## Keyboard Shortcuts
+
+- `Ctrl+Shift+M` - Toggle debug dashboard
+- Dashboard is draggable and resizable
+
+## Example Usage Scenarios
+
+### Development Testing
+```javascript
+// Start monitoring during development
+MSPDebug.startMonitoring();
+
+// Quick health check
+MSPDebug.quickHealthCheck();
+
+// Test the alert system
+MSPDebug.testAlerts();
+
+// Show visual dashboard
+MSPDebug.show();
+```
+
+### Performance Analysis
+```javascript
+// Show dashboard for visual monitoring
+MSPDebug.show();
+
+// Run complete stress tests with detailed output
+MSPDebug.runFullSuite();
+
+// Test specific scenarios
+MSPDebug.stressScenario('high-frequency');
+
+// Generate detailed report
+MSPDebug.report();
+```
+
+### Issue Debugging
+```javascript
+// Get current status
+MSPDebug.getStatus();
+
+// Analyze current queue state
+MSPDebug.analyze();
+
+// Test specific problematic scenario
+MSPDebug.runTest('memory-leaks');
+
+// Check for alerts with low thresholds
+MSPDebug.setTestThresholds();
+MSPDebug.triggerTestAlerts();
+
+// Generate diagnostic report
+MSPDebug.report();
+```
+
+## Integration with Existing Code
+
+The debug tools are designed to be non-intrusive:
+
+- They hook into existing MSP methods without modifying core functionality
+- Monitoring can be enabled/disabled at runtime
+- No performance impact when not actively monitoring
+- Original MSP behavior is preserved
+
+### Auto-loading
+The debug tools auto-load when `msp_debug_tools.js` is imported. They detect the presence of the global MSP object and initialize automatically.
+
+### Keyboard Shortcuts
+- `Ctrl+Shift+M`: Toggle debug dashboard
+
+## Implementation Status
+
+### β
Current Features
+
+#### Alert System
+- Enhanced debug logging with reduced console noise
+- Test infrastructure: `triggerTestAlerts()`, `setTestThresholds()`, `setNormalThresholds()`
+- Visual alert display in dashboard
+- Smart threshold management for testing
+
+#### Interactive Dashboard
+- Smart update pausing during user interactions
+- Clickable test results with detailed information
+- Enhanced interaction handling for all UI elements
+- Visual feedback with updates pause indicator
+
+#### Complete API
+- Dual API support: `MSPDebug` (modern) and `MSPTestRunner` (legacy)
+- All documented commands implemented and verified
+- Comprehensive testing methods (9 test types + 3 stress scenarios)
+- Real-time monitoring with alert detection
+
+### β
Verified Working
+- Alert system triggers correctly when thresholds exceeded
+- Dashboard displays alerts visually without update interference
+- Test results provide comprehensive detailed information
+- All API commands function as documented
+- Auto-loading works in development environment
+
+## File Structure
+
+```
+src/js/
+βββ msp_queue_monitor.js # Core monitoring functionality
+βββ msp_stress_test.js # Stress testing framework
+βββ msp_debug_dashboard.js # Visual dashboard UI
+βββ msp_test_runner.js # Console command interface
+βββ msp_debug_tools.js # Integration and auto-loading
+```
+
+## Requirements
+
+- Modern browser with ES6 module support
+- Access to the global `MSP` object
+- Console access for command-line interface
+
+## Troubleshooting
+
+**Tools not loading:**
+- Ensure MSP object is available globally
+- Check browser console for import errors
+
+**Tests failing unexpectedly:**
+- Verify serial connection is active
+- Check that flight controller is responding
+- Review console for specific error messages
+
+**Dashboard not appearing:**
+- Try `MSPTestRunner.showDashboard()` from console
+- Check for CSS conflicts
+- Verify no popup blockers are interfering
+
+## Contributing
+
+When adding new tests or monitoring features:
+
+1. Add test methods to `MSPStressTest` class
+2. Update monitor metrics in `MSPQueueMonitor`
+3. Extend dashboard UI as needed
+4. Update this documentation
+
+## License
+
+Same as Betaflight Configurator project.
diff --git a/src/js/msp/msp_debug_dashboard.js b/src/js/msp/msp_debug_dashboard.js
new file mode 100644
index 00000000..e0c90810
--- /dev/null
+++ b/src/js/msp/msp_debug_dashboard.js
@@ -0,0 +1,1192 @@
+/**
+ * MSP Debug Dashboard - Visual interface for monitoring MSP queue and running stress tests
+ */
+
+import { mspQueueMonitor } from "./msp_queue_monitor.js";
+import { mspStressTest } from "./msp_stress_test.js";
+
+export class MSPDebugDashboard {
+ constructor() {
+ this.isVisible = false;
+ this.updateInterval = null;
+ this.chartData = {
+ queueSize: [],
+ responseTime: [],
+ timestamps: [],
+ };
+ this.maxDataPoints = 50;
+ this.updatesPaused = false;
+ this.pauseTimeout = null;
+ this.lastUpdateData = {};
+
+ // Performance optimization properties
+ this.lastChartUpdate = 0;
+ this.chartUpdatePending = false;
+ this.elementCache = new Map(); // Cache DOM elements to avoid repeated queries
+
+ this.createDashboard();
+ this.setupEventListeners();
+ }
+
+ /**
+ * Escape HTML characters to prevent XSS
+ */
+ escapeHtml(text) {
+ if (typeof text !== "string") {
+ return text;
+ }
+ const div = document.createElement("div");
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ /**
+ * Create the dashboard HTML structure
+ */
+ createDashboard() {
+ // Create dashboard container
+ this.container = document.createElement("div");
+ this.container.id = "msp-debug-dashboard";
+ this.container.innerHTML = `
+
+
+
+
+
+
π Status Overview
+
+
+
+ 0
+ / 50
+
+
+
+ 100%
+
+
+
+ 0ms
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
π Queue Analysis
+
+
+
+
+
+
+
+
+
+
π Live Metrics
+
+
+
+
+
+
π Current Queue Contents
+
+
+
+
+
+
+ `;
+
+ // Add CSS styles
+ this.addStyles();
+
+ // Add to document but keep hidden initially
+ document.body.appendChild(this.container);
+ this.container.style.display = "none";
+ }
+
+ /**
+ * Add CSS styles for the dashboard
+ */
+ addStyles() {
+ const style = document.createElement("style");
+ style.textContent = `
+ #msp-debug-dashboard {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 500px;
+ max-height: 80vh;
+ background: #1e1e1e;
+ color: #ffffff;
+ border: 1px solid #444;
+ border-radius: 8px;
+ font-family: 'Courier New', monospace;
+ font-size: 12px;
+ z-index: 10000;
+ overflow-y: auto;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.5);
+ }
+
+ .msp-dashboard-header {
+ background: #2d2d2d;
+ padding: 10px 15px;
+ border-bottom: 1px solid #444;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .msp-dashboard-header h3 {
+ margin: 0;
+ font-size: 14px;
+ }
+
+ .dashboard-controls {
+ display: flex;
+ gap: 5px;
+ }
+
+ .dashboard-controls button {
+ padding: 4px 8px;
+ font-size: 11px;
+ background: #444;
+ color: white;
+ border: 1px solid #666;
+ border-radius: 3px;
+ cursor: pointer;
+ }
+
+ .dashboard-controls button:hover {
+ background: #555;
+ }
+
+ .updates-status {
+ position: absolute;
+ top: 50%;
+ right: 160px;
+ transform: translateY(-50%);
+ font-size: 11px;
+ color: #ffaa00;
+ background: rgba(255, 170, 0, 0.15);
+ padding: 3px 8px;
+ border-radius: 3px;
+ border: 1px solid #ffaa00;
+ font-weight: bold;
+ z-index: 10001;
+ }
+
+ .msp-dashboard-content {
+ padding: 15px;
+ }
+
+ .status-section, .alerts-section, .queue-section, .chart-section, .details-section, .test-section {
+ margin-bottom: 20px;
+ border: 1px solid #333;
+ border-radius: 4px;
+ padding: 10px;
+ }
+
+ .status-section h4, .alerts-section h4, .queue-section h4, .chart-section h4, .details-section h4, .test-section h4 {
+ margin: 0 0 10px 0;
+ font-size: 13px;
+ color: #ffd700;
+ }
+
+ .status-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ }
+
+ .status-item {
+ background: #2a2a2a;
+ padding: 8px;
+ border-radius: 3px;
+ }
+
+ .status-item label {
+ display: block;
+ font-size: 11px;
+ color: #ccc;
+ margin-bottom: 3px;
+ }
+
+ .status-item .value {
+ font-weight: bold;
+ color: #00ff00;
+ }
+
+ .max-value {
+ color: #888;
+ font-size: 10px;
+ }
+
+ .alerts-container {
+ min-height: 30px;
+ }
+
+ .alert-item {
+ background: #ff4444;
+ color: white;
+ padding: 5px 10px;
+ border-radius: 3px;
+ margin-bottom: 5px;
+ font-size: 11px;
+ }
+
+ .alert-item.warning {
+ background: #ffaa00;
+ }
+
+ .no-alerts {
+ color: #888;
+ font-style: italic;
+ text-align: center;
+ padding: 10px;
+ }
+
+ .queue-controls {
+ margin-bottom: 10px;
+ }
+
+ .queue-controls button {
+ padding: 5px 10px;
+ margin-right: 5px;
+ background: #0066cc;
+ color: white;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 11px;
+ }
+
+ .queue-controls button:hover {
+ background: #0088ff;
+ }
+
+ .queue-analysis {
+ background: #2a2a2a;
+ padding: 10px;
+ border-radius: 3px;
+ font-size: 11px;
+ max-height: 200px;
+ overflow-y: auto;
+ pointer-events: auto; /* Ensure clickability */
+ }
+
+ .queue-contents {
+ background: #2a2a2a;
+ padding: 10px;
+ border-radius: 3px;
+ max-height: 150px;
+ overflow-y: auto;
+ pointer-events: auto; /* Ensure clickability */
+ }
+
+ .queue-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 3px 0;
+ border-bottom: 1px solid #333;
+ font-size: 11px;
+ }
+
+ .queue-item:last-child {
+ border-bottom: none;
+ }
+
+ .queue-item-empty {
+ opacity: 0.3;
+ font-style: italic;
+ }
+
+ .test-results {
+ background: #2a2a2a;
+ padding: 10px;
+ border-radius: 3px;
+ max-height: 200px;
+ overflow-y: auto;
+ font-size: 11px;
+ pointer-events: auto; /* Ensure clickability */
+ }
+
+ .test-result-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 5px 0;
+ border-bottom: 1px solid #333;
+ cursor: pointer;
+ min-height: 20px; /* Prevent height changes during updates */
+ }
+
+ .test-result-item:hover {
+ background: rgba(255, 255, 255, 0.1);
+ }
+
+ .test-result-item:last-child {
+ border-bottom: none;
+ }
+
+ .test-passed {
+ color: #00ff00;
+ }
+
+ .test-failed {
+ color: #ff4444;
+ }
+
+ #msp-metrics-chart {
+ width: 100%;
+ height: 150px;
+ background: #2a2a2a;
+ border-radius: 3px;
+ display: block;
+ }
+
+ .test-result-item {
+ padding: 5px 10px;
+ margin: 2px 0;
+ border-radius: 3px;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: #1a1a1a;
+ border: 1px solid #444;
+ transition: all 0.2s ease;
+ user-select: none;
+ }
+
+ .test-result-item:hover {
+ background: #333 !important;
+ border-color: #666;
+ transform: translateX(2px);
+ }
+
+ .test-result-item:active {
+ background: #444 !important;
+ transform: translateX(0px);
+ }
+
+ .queue-item {
+ padding: 5px;
+ margin: 2px 0;
+ background: #1a1a1a;
+ border-radius: 3px;
+ display: flex;
+ justify-content: space-between;
+ font-size: 11px;
+ border: 1px solid #333;
+ }
+
+ .alert-item {
+ padding: 5px 10px;
+ margin: 2px 0;
+ background: #4a2a2a;
+ border-radius: 3px;
+ border-left: 3px solid #ff4444;
+ color: #ffcccc;
+ }
+
+ #updates-status {
+ position: absolute;
+ top: 5px;
+ right: 50px;
+ background: rgba(255, 165, 0, 0.9);
+ color: #000;
+ padding: 2px 8px;
+ border-radius: 3px;
+ font-size: 10px;
+ font-weight: bold;
+ display: none;
+ z-index: 1001;
+ }
+ `;
+
+ document.head.appendChild(style);
+ }
+
+ /**
+ * Setup event listeners
+ */
+ setupEventListeners() {
+ // Toggle monitoring - scoped to dashboard container
+ this.container.addEventListener("click", (e) => {
+ if (e.target.id === "msp-toggle-monitoring") {
+ this.toggleMonitoring();
+ } else if (e.target.id === "msp-run-stress-test") {
+ this.runStressTest();
+ } else if (e.target.id === "msp-clear-metrics") {
+ this.clearMetrics();
+ } else if (e.target.id === "clear-alerts") {
+ this.clearAlerts();
+ } else if (e.target.id === "msp-close-dashboard") {
+ this.hide();
+ } else if (e.target.id === "analyze-queue") {
+ this.analyzeQueue();
+ } else if (e.target.id === "export-report") {
+ this.exportReport();
+ } else {
+ // Handle test result item clicks
+ const testResultItem = e.target.closest(".test-result-item");
+ if (testResultItem) {
+ const testIndex = parseInt(testResultItem.getAttribute("data-test-index"), 10);
+ if (!isNaN(testIndex)) {
+ this.showTestDetails(testIndex);
+ }
+ }
+
+ // Handle close details button clicks
+ if (e.target.classList.contains("close-details-btn")) {
+ const testDetails = e.target.closest(".test-details");
+ if (testDetails) {
+ testDetails.remove();
+ this.pauseUpdates(1000);
+ }
+ }
+ }
+ });
+
+ // Enhanced interaction handling
+ this.setupInteractionHandlers();
+
+ // Listen to monitor events
+ mspQueueMonitor.addListener((status) => {
+ this.updateDisplay(status);
+ });
+
+ // Keyboard shortcut to toggle dashboard
+ document.addEventListener("keydown", (e) => {
+ if (e.ctrlKey && e.shiftKey && e.key === "M") {
+ this.toggle();
+ }
+ });
+
+ // Handle window resize to redraw canvas with correct dimensions
+ window.addEventListener("resize", () => {
+ if (this.isVisible) {
+ // Delay redraw to ensure layout is updated
+ setTimeout(() => this.drawChart(), 100);
+ }
+ });
+ }
+
+ /**
+ * Show the dashboard
+ */
+ show() {
+ this.container.style.display = "block";
+ this.isVisible = true;
+ this.updateDisplay();
+ }
+
+ /**
+ * Hide the dashboard
+ */
+ hide() {
+ this.container.style.display = "none";
+ this.isVisible = false;
+ }
+
+ /**
+ * Toggle dashboard visibility
+ */
+ toggle() {
+ if (this.isVisible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ /**
+ * Setup enhanced interaction handlers
+ */
+ setupInteractionHandlers() {
+ // Pause updates on any hover over interactive elements
+ const interactiveSelectors = [
+ ".test-results",
+ ".queue-analysis",
+ ".test-result-item",
+ "button",
+ "select",
+ "input",
+ ".queue-item",
+ ".alert-item",
+ ];
+
+ interactiveSelectors.forEach((selector) => {
+ this.container.addEventListener(
+ "mouseenter",
+ (e) => {
+ if (e.target.matches(selector) || e.target.closest(selector)) {
+ this.pauseUpdates(3000);
+ }
+ },
+ true,
+ );
+ });
+
+ // Extended pause on clicks
+ this.container.addEventListener("click", (e) => {
+ const isInteractive = interactiveSelectors.some(
+ (selector) => e.target.matches(selector) || e.target.closest(selector),
+ );
+
+ if (isInteractive) {
+ this.pauseUpdates(5000); // Longer pause for clicks
+ }
+ });
+
+ // Pause on focus for input elements
+ this.container.addEventListener("focusin", (e) => {
+ if (e.target.matches("input, select, textarea, button")) {
+ this.pauseUpdates(10000); // Long pause for focused elements
+ }
+ });
+
+ // Resume updates when focus is lost
+ this.container.addEventListener("focusout", (e) => {
+ // Check if focus moved to another element within the dashboard
+ if (!this.container.contains(e.relatedTarget)) {
+ this.pauseUpdates(1000); // Short pause before resuming
+ }
+ });
+
+ // Special handling for test result items
+ this.container.addEventListener(
+ "mouseenter",
+ (e) => {
+ const testResultItem = e.target.closest(".test-result-item");
+ if (testResultItem) {
+ testResultItem.style.backgroundColor = "#333";
+ this.pauseUpdates(2000);
+ }
+ },
+ true,
+ );
+
+ this.container.addEventListener(
+ "mouseleave",
+ (e) => {
+ const testResultItem = e.target.closest(".test-result-item");
+ if (testResultItem) {
+ testResultItem.style.backgroundColor = "";
+ }
+ },
+ true,
+ );
+ }
+
+ /**
+ * Toggle monitoring
+ */
+ toggleMonitoring() {
+ const button = document.getElementById("msp-toggle-monitoring");
+
+ if (mspQueueMonitor.isMonitoring) {
+ mspQueueMonitor.stopMonitoring();
+ button.textContent = "Start Monitoring";
+ button.style.background = "#444";
+ } else {
+ mspQueueMonitor.startMonitoring(500);
+ button.textContent = "Stop Monitoring";
+ button.style.background = "#00aa00";
+ }
+ }
+
+ /**
+ * Run stress test
+ */
+ async runStressTest() {
+ const button = document.getElementById("msp-run-stress-test");
+ const originalText = button.textContent;
+
+ button.textContent = "Running Tests...";
+ button.disabled = true;
+
+ try {
+ const results = await mspStressTest.runStressTestSuite();
+ this.displayTestResults(results);
+ } catch (error) {
+ console.error("Stress test failed:", error);
+ this.displayTestResults({
+ summary: { failed: 1, error: error.message },
+ detailedResults: [],
+ });
+ } finally {
+ button.textContent = originalText;
+ button.disabled = false;
+ }
+ }
+
+ /**
+ * Clear metrics
+ */
+ clearMetrics() {
+ mspQueueMonitor.resetMetrics();
+ this.chartData = {
+ queueSize: [],
+ responseTime: [],
+ timestamps: [],
+ };
+ this.updateDisplay();
+ }
+
+ /**
+ * Clear alerts only
+ */
+ clearAlerts() {
+ mspQueueMonitor.clearAlerts();
+ }
+
+ /**
+ * Update display with current status
+ */
+ updateDisplay(status = null) {
+ if (!this.isVisible || this.updatesPaused) {
+ return;
+ }
+
+ status = status || mspQueueMonitor.getStatus();
+
+ // Only update if data has actually changed to avoid unnecessary DOM manipulation
+ if (this._hasDataChanged(status)) {
+ this._updateStatusMetrics(status);
+ this._updateAlertsIfChanged(status.alerts);
+ this._updateQueueContentsIfChanged(status.queueContents);
+ this._updateChart(status);
+
+ // Store current data for comparison
+ this.lastUpdateData = {
+ currentQueueSize: status.currentQueueSize,
+ totalRequests: status.metrics.totalRequests,
+ successRate: status.metrics.successRate,
+ avgResponseTime: status.metrics.avgResponseTime,
+ alerts: JSON.stringify(status.alerts),
+ queueContents: JSON.stringify(status.queueContents),
+ };
+ }
+ }
+
+ /**
+ * Check if data has changed to avoid unnecessary updates
+ */
+ _hasDataChanged(status) {
+ const lastData = this.lastUpdateData;
+ if (!lastData) return true;
+
+ // Quick primitive checks first (fastest)
+ if (
+ lastData.currentQueueSize !== status.currentQueueSize ||
+ lastData.totalRequests !== status.metrics.totalRequests ||
+ lastData.successRate !== status.metrics.successRate ||
+ lastData.avgResponseTime !== status.metrics.avgResponseTime
+ ) {
+ return true;
+ }
+
+ // More expensive object comparisons only if needed
+ try {
+ const currentAlertsStr = JSON.stringify(status.alerts);
+ const currentQueueStr = JSON.stringify(status.queueContents);
+
+ return lastData.alerts !== currentAlertsStr || lastData.queueContents !== currentQueueStr;
+ } catch (e) {
+ // If JSON.stringify fails, assume data changed
+ console.warn("JSON stringify failed in dashboard update check:", e);
+ return true;
+ }
+ }
+
+ /**
+ * Update status metrics only
+ */
+ _updateStatusMetrics(status) {
+ this.updateElement("queue-size", status.currentQueueSize);
+ this.updateElement("max-queue-size", status.maxQueueSize);
+ this.updateElement("success-rate", `${Math.round((status.metrics.successRate || 0) * 100)}%`);
+ this.updateElement("avg-response-time", `${Math.round(status.metrics.avgResponseTime || 0)}ms`);
+ this.updateElement("total-requests", status.metrics.totalRequests);
+ }
+
+ /**
+ * Update alerts only if they've changed
+ */
+ _updateAlertsIfChanged(alerts) {
+ const currentAlerts = JSON.stringify(alerts);
+ if (this.lastUpdateData.alerts !== currentAlerts) {
+ this.updateAlerts(alerts);
+ }
+ }
+
+ /**
+ * Update queue contents only if they've changed
+ */
+ _updateQueueContentsIfChanged(queueContents) {
+ const currentQueue = JSON.stringify(queueContents);
+ if (this.lastUpdateData.queueContents !== currentQueue) {
+ this.updateQueueContents(queueContents);
+ }
+ }
+
+ /**
+ * Pause updates for a specified duration
+ */
+ pauseUpdates(duration = 2000) {
+ this.updatesPaused = true;
+
+ // Show pause indicator
+ const pauseIndicator = document.getElementById("updates-status");
+ if (pauseIndicator) {
+ pauseIndicator.style.display = "block";
+ }
+
+ if (this.pauseTimeout) {
+ clearTimeout(this.pauseTimeout);
+ }
+
+ this.pauseTimeout = setTimeout(() => {
+ this.updatesPaused = false;
+
+ // Hide pause indicator
+ if (pauseIndicator) {
+ pauseIndicator.style.display = "none";
+ }
+
+ // Force an update when resuming
+ this.updateDisplay();
+ }, duration);
+ }
+
+ /**
+ * Update element text content with caching
+ */
+ updateElement(id, value) {
+ let element = this.elementCache.get(id);
+ if (!element) {
+ element = document.getElementById(id);
+ if (element) {
+ this.elementCache.set(id, element);
+ }
+ }
+
+ if (element && element.textContent !== value) {
+ element.textContent = value;
+ }
+ }
+
+ /**
+ * Update alerts display
+ */
+ updateAlerts(alerts) {
+ const container = document.getElementById("alerts-container");
+ if (!container) {
+ return;
+ }
+
+ const activeAlerts = Object.entries(alerts).filter(([_, active]) => active);
+
+ if (activeAlerts.length === 0) {
+ container.innerHTML = 'No active alerts
';
+ return;
+ }
+
+ const alertMessages = {
+ queueFull: "Queue is near capacity",
+ highTimeout: "High timeout rate detected",
+ slowResponses: "Slow response times detected",
+ memoryLeak: "Potential memory leak detected",
+ };
+
+ container.innerHTML = activeAlerts
+ .map(
+ ([alertType, _]) =>
+ `${alertMessages[alertType] || this.escapeHtml(alertType)}
`,
+ )
+ .join("");
+ }
+
+ /**
+ * Update queue contents display
+ */
+ updateQueueContents(queueContents) {
+ const container = document.getElementById("queue-contents");
+ if (!container) {
+ return;
+ }
+
+ // Always show exactly 5 slots to prevent layout shifts
+ const maxSlots = 5;
+ const items = queueContents || [];
+ const slotsHtml = [];
+
+ // Add actual queue items
+ for (let i = 0; i < maxSlots; i++) {
+ if (i < items.length) {
+ const item = items[i];
+ slotsHtml.push(`
+
+ Code: ${this.escapeHtml(item.code)}
+ Age: ${Math.round(item.age)}ms
+ Attempts: ${this.escapeHtml(item.attempts)}
+ ${item.hasTimer ? "β" : "β"}
+
+ `);
+ } else {
+ // Add empty slot placeholder
+ slotsHtml.push(`
+
+ β
+ β
+ β
+ β
+
+ `);
+ }
+ }
+
+ container.innerHTML = slotsHtml.join("");
+ }
+
+ /**
+ * Update chart with new data
+ */
+ _updateChart(status) {
+ this.updateChart(status);
+ }
+
+ /**
+ * Update chart with new data
+ */
+ updateChart(status) {
+ const now = Date.now();
+
+ // Throttle chart updates to reduce performance impact
+ if (this.lastChartUpdate && now - this.lastChartUpdate < 200) {
+ return; // Skip update if less than 200ms since last update
+ }
+
+ this.chartData.timestamps.push(now);
+ this.chartData.queueSize.push(status.currentQueueSize);
+ this.chartData.responseTime.push(status.metrics.avgResponseTime || 0);
+
+ // Keep only recent data points
+ if (this.chartData.timestamps.length > this.maxDataPoints) {
+ this.chartData.timestamps.shift();
+ this.chartData.queueSize.shift();
+ this.chartData.responseTime.shift();
+ }
+
+ this.lastChartUpdate = now;
+
+ // Use requestAnimationFrame for smoother chart updates
+ if (!this.chartUpdatePending) {
+ this.chartUpdatePending = true;
+ requestAnimationFrame(() => {
+ this.drawChart();
+ this.chartUpdatePending = false;
+ });
+ }
+ }
+
+ /**
+ * Draw the metrics chart
+ */
+ drawChart() {
+ const canvas = document.getElementById("msp-metrics-chart");
+ if (!canvas) {
+ return;
+ }
+
+ const ctx = canvas.getContext("2d");
+
+ // Get the display size (CSS pixels)
+ const rect = canvas.getBoundingClientRect();
+ const displayWidth = rect.width;
+ const displayHeight = rect.height;
+
+ // Get the device pixel ratio, falling back to 1
+ const devicePixelRatio = window.devicePixelRatio || 1;
+
+ // Set the internal canvas size to actual pixels for Hi-DPI displays
+ canvas.width = displayWidth * devicePixelRatio;
+ canvas.height = displayHeight * devicePixelRatio;
+
+ // Scale the canvas back down using CSS
+ canvas.style.width = `${displayWidth}px`;
+ canvas.style.height = `${displayHeight}px`;
+
+ // Scale the drawing context so everything draws at the correct size
+ ctx.scale(devicePixelRatio, devicePixelRatio);
+
+ const width = displayWidth;
+ const height = displayHeight;
+
+ // Clear canvas
+ ctx.fillStyle = "#2a2a2a";
+ ctx.fillRect(0, 0, width, height);
+
+ if (this.chartData.timestamps.length < 2) {
+ return;
+ }
+
+ // Draw queue size line
+ ctx.strokeStyle = "#00ff00";
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+
+ const maxQueueSize = Math.max(...this.chartData.queueSize, 10);
+
+ this.chartData.queueSize.forEach((size, i) => {
+ const x = (i / (this.chartData.queueSize.length - 1)) * width;
+ const y = height - (size / maxQueueSize) * height;
+
+ if (i === 0) {
+ ctx.moveTo(x, y);
+ } else {
+ ctx.lineTo(x, y);
+ }
+ });
+
+ ctx.stroke();
+
+ // Draw labels with proper font scaling
+ ctx.fillStyle = "#ffffff";
+ ctx.font = "10px monospace";
+ ctx.fillText("Queue Size", 5, 15);
+ ctx.fillText(`Max: ${maxQueueSize}`, 5, height - 5);
+ }
+
+ /**
+ * Analyze current queue
+ */
+ analyzeQueue() {
+ const analysis = mspQueueMonitor.analyzeQueue();
+ const container = document.getElementById("queue-analysis");
+
+ if (!container) {
+ return;
+ }
+
+ container.innerHTML = `
+ Total Items: ${analysis.totalItems}
+ Age Distribution:
+
+ Fresh (<1s): ${analysis.ageDistribution.fresh}
+ Recent (1-5s): ${analysis.ageDistribution.recent}
+ Stale (5-10s): ${analysis.ageDistribution.stale}
+ Ancient (>10s): ${analysis.ageDistribution.ancient}
+
+ By Code:
+
+ ${Object.entries(analysis.byCode)
+ .map(([code, count]) => `Code ${this.escapeHtml(String(code))}: ${count}`)
+ .join("
")}
+
+ ${
+ analysis.potentialIssues.length > 0
+ ? `
+ Issues:
+
+ ${analysis.potentialIssues.map((p) => this.escapeHtml(String(p))).join("
")}
+
+ `
+ : ""
+}
+ `;
+ }
+
+ /**
+ * Display test results
+ */
+ displayTestResults(results) {
+ const container = document.getElementById("test-results");
+ if (!container) {
+ return;
+ }
+
+ const summary = results.summary || {};
+
+ container.innerHTML = `
+ Test Summary:
+
+ Passed: ${summary.passed || 0}
+ Failed: ${summary.failed || 0}
+ Success Rate: ${Math.round((summary.successRate || 0) * 100)}%
+ Grade: ${summary.overallGrade || "N/A"}
+
+ Details (click for more info):
+
+ ${(results.detailedResults || [])
+ .map(
+ (test, index) => `
+
+
+ ${this.escapeHtml(test.name)}
+
+ ${this.escapeHtml(test.status)}
+
+ `,
+ )
+ .join("")}
+
+ `;
+
+ // Store test results for detailed view
+ this.lastTestResults = results;
+ }
+
+ /**
+ * Show detailed test information
+ */
+ showTestDetails(testIndex) {
+ if (!this?.lastTestResults?.detailedResults) {
+ return;
+ }
+
+ const test = this.lastTestResults.detailedResults[testIndex];
+ if (!test) {
+ return;
+ }
+
+ // Pause updates while showing details
+ this.pauseUpdates(5000);
+
+ const detailsHtml = `
+
+
π ${this.escapeHtml(test.name)} Details
+
Status: ${this.escapeHtml(test.status)}
+ ${test.duration ? `
Duration: ${Math.round(test.duration)}ms
` : ""}
+ ${test.error ? `
Error: ${this.escapeHtml(test.error)}
` : ""}
+ ${
+ test.result
+ ? `
+
Results:
+
${this.escapeHtml(JSON.stringify(test.result, null, 2))}
+ `
+ : ""
+}
+ ${
+ test.metrics
+ ? `
+
Metrics:
+
+ Queue Size: ${test.metrics.currentQueueSize}/${test.metrics.maxQueueSize}
+ Total Requests: ${test.metrics.totalRequests}
+ Success Rate: ${Math.round((test.metrics.successRate || 0) * 100)}%
+ Avg Response: ${Math.round(test.metrics.avgResponseTime || 0)}ms
+
+ `
+ : ""
+}
+
+
+ `;
+
+ // Add details after the test results
+ const testContainer = document.getElementById("test-results");
+ const existingDetails = testContainer.querySelector(".test-details");
+ if (existingDetails) {
+ existingDetails.remove();
+ }
+
+ const detailsDiv = document.createElement("div");
+ detailsDiv.className = "test-details";
+ detailsDiv.innerHTML = detailsHtml;
+ testContainer.appendChild(detailsDiv);
+ }
+
+ /**
+ * Export comprehensive report
+ */
+ exportReport() {
+ const report = mspQueueMonitor.generateReport();
+ const blob = new Blob([JSON.stringify(report, null, 2)], {
+ type: "application/json",
+ });
+
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `msp-report-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }
+}
+
+// Create and export dashboard instance
+export const mspDebugDashboard = new MSPDebugDashboard();
+
+// Add global shortcut and console commands
+window.MSPDebug = {
+ dashboard: mspDebugDashboard,
+ monitor: mspQueueMonitor,
+ stressTest: mspStressTest,
+
+ // Convenience methods
+ show: () => mspDebugDashboard.show(),
+ hide: () => mspDebugDashboard.hide(),
+ startMonitoring: () => mspQueueMonitor.startMonitoring(),
+ stopMonitoring: () => mspQueueMonitor.stopMonitoring(),
+ runTests: () => mspStressTest.runStressTestSuite(),
+ runFullSuite: () => mspStressTest.runStressTestSuite(),
+ analyze: () => mspQueueMonitor.analyzeQueue(),
+ report: () => mspQueueMonitor.generateReport(),
+ showTestDetails: (index) => mspDebugDashboard.showTestDetails(index),
+
+ // Individual test methods
+ runTest: (testName) => mspStressTest.runSpecificTest(testName),
+ quickHealthCheck: () => window.MSPTestRunner?.quickHealthCheck?.(),
+ stressScenario: (scenario) => window.MSPTestRunner?.stressScenario?.(scenario),
+ getStatus: () => mspQueueMonitor.getStatus(),
+
+ // Alert testing methods
+ triggerTestAlerts: () => mspQueueMonitor.triggerTestAlerts(),
+ setTestThresholds: () => mspQueueMonitor.setTestThresholds(),
+ setNormalThresholds: () => mspQueueMonitor.setNormalThresholds(),
+
+ // Quick test method
+ testAlerts: () => {
+ console.log("π§ͺ Running alert test...");
+ mspDebugDashboard.show();
+ mspQueueMonitor.startMonitoring(500);
+ return mspQueueMonitor.triggerTestAlerts();
+ },
+};
+
+console.log("π§ MSP Debug Tools loaded! Use Ctrl+Shift+M to toggle dashboard or MSPDebug.show()");
diff --git a/src/js/msp/msp_debug_tools.js b/src/js/msp/msp_debug_tools.js
new file mode 100644
index 00000000..fb602ee5
--- /dev/null
+++ b/src/js/msp/msp_debug_tools.js
@@ -0,0 +1,52 @@
+/**
+ * MSP Debug Tools - Integration file to load all debugging and testing tools
+ * Include this file to get comprehensive MSP monitoring and testing capabilities
+ */
+
+// Import all debug tools
+import "./msp_queue_monitor.js";
+import "./msp_stress_test.js";
+import "./msp_debug_dashboard.js";
+import "./msp_test_runner.js";
+
+console.log(`
+π§ MSP Debug Tools Loaded Successfully!
+
+Quick Start:
+ β’ Press Ctrl+Shift+M to toggle the visual dashboard
+ β’ Use MSPTestRunner.help() to see all available commands
+ β’ Use MSPTestRunner.quickHealthCheck() for a quick test
+
+Example Usage:
+ MSPTestRunner.startQuickMonitor(); // Start monitoring
+ MSPTestRunner.runTest('queue-flooding'); // Run specific test
+ MSPTestRunner.showDashboard(); // Show visual dashboard
+ MSPTestRunner.runFullSuite(); // Run all stress tests
+
+The tools will help you:
+ β Monitor MSP queue health in real-time
+ β Detect memory leaks and performance issues
+ β Stress test the MSP implementation
+ β Analyze queue contents and response times
+ β Export detailed diagnostic reports
+
+Happy debugging! π
+`);
+
+// Auto-start basic monitoring in development
+if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
+ console.log("π Development environment detected - auto-starting basic monitoring");
+
+ // Import the monitor and start it with minimal logging
+ import("./msp_queue_monitor.js").then(({ mspQueueMonitor }) => {
+ mspQueueMonitor.addListener((status) => {
+ // Only log alerts and significant events
+ const alerts = Object.values(status.alerts).filter((a) => a);
+ if (alerts.length > 0) {
+ console.warn("π¨ MSP Alert detected - check dashboard for details");
+ }
+ });
+
+ mspQueueMonitor.startMonitoring(2000); // Monitor every 2 seconds
+ });
+}
diff --git a/src/js/msp/msp_queue_monitor.js b/src/js/msp/msp_queue_monitor.js
new file mode 100644
index 00000000..85a5578e
--- /dev/null
+++ b/src/js/msp/msp_queue_monitor.js
@@ -0,0 +1,692 @@
+/**
+ * MSP Queue Monitor - Real-time monitoring of MSP message queue
+ * Provides insights into queue health, performance metrics, and potential issues
+ */
+
+export class MSPQueueMonitor {
+ constructor(mspInstance) {
+ this.msp = mspInstance;
+ this.isMonitoring = false;
+ this.metrics = {
+ totalRequests: 0,
+ completedRequests: 0,
+ failedRequests: 0,
+ timeouts: 0,
+ duplicates: 0,
+ avgResponseTime: 0,
+ maxResponseTime: 0,
+ queuePeakSize: 0,
+ requestsByCode: new Map(),
+ responseTimes: [],
+ errorsByType: new Map(),
+ };
+
+ this.alerts = {
+ queueFull: false,
+ highTimeout: false,
+ slowResponses: false,
+ memoryLeak: false,
+ };
+
+ this.thresholds = {
+ maxQueueSize: Math.floor((this.msp.MAX_QUEUE_SIZE || 100) * 0.8), // Alert when queue > 80% of MAX_QUEUE_SIZE
+ maxAvgResponseTime: 2000, // Alert when avg response > 2s
+ maxTimeoutRate: 0.1, // Alert when timeout rate > 10%
+ memoryLeakThreshold: 100, // Alert when callbacks grow beyond expected
+ };
+
+ this.monitoringInterval = null;
+ this.listeners = [];
+
+ // Hook into MSP methods to collect metrics
+ this._hookMSPMethods();
+ }
+
+ /**
+ * Hook into MSP methods to collect real-time metrics
+ */
+ _hookMSPMethods() {
+ // Check if MSP instance is already instrumented to prevent double-patching
+ if (this.msp._mspQueueMonitorInstrumented) {
+ console.warn("MSP instance is already instrumented by MSPQueueMonitor");
+ return;
+ }
+
+ // Store original methods
+ this.originalSendMessage = this.msp.send_message.bind(this.msp);
+ this.originalDispatchMessage = this.msp._dispatch_message.bind(this.msp);
+ this.originalRemoveRequest = this.msp._removeRequestFromCallbacks?.bind(this.msp);
+
+ // Override send_message to track requests
+ this.msp.send_message = (...args) => {
+ this._trackRequestStart(args[0], args[1]);
+ return this.originalSendMessage(...args);
+ };
+
+ // Override _dispatch_message to track responses
+ this.msp._dispatch_message = (...args) => {
+ this._trackResponse();
+ return this.originalDispatchMessage(...args);
+ };
+
+ // Override _removeRequestFromCallbacks to track completions
+ if (this.originalRemoveRequest) {
+ this.msp._removeRequestFromCallbacks = (requestObj) => {
+ this._trackRequestCompletion(requestObj);
+ return this.originalRemoveRequest(requestObj);
+ };
+ }
+
+ // Mark MSP instance as instrumented
+ this.msp._mspQueueMonitorInstrumented = true;
+ }
+
+ /**
+ * Track when a request starts
+ */
+ _trackRequestStart(code, data) {
+ this.metrics.totalRequests++;
+
+ // Track requests by code
+ const count = this.metrics.requestsByCode.get(code) || 0;
+ this.metrics.requestsByCode.set(code, count + 1);
+
+ // Check for queue size peaks
+ const currentQueueSize = this.msp.callbacks?.length ?? 0;
+ if (currentQueueSize > this.metrics.queuePeakSize) {
+ this.metrics.queuePeakSize = currentQueueSize;
+ }
+
+ this._checkAlerts();
+ }
+
+ /**
+ * Track when a response is received
+ */
+ _trackResponse() {
+ // This will be called for both successful and failed responses
+ // More detailed tracking happens in _trackRequestCompletion
+ }
+
+ /**
+ * Track when a request is completed (success or failure)
+ */
+ _trackRequestCompletion(requestObj) {
+ if (!requestObj) {
+ return;
+ }
+
+ const responseTime = performance.now() - requestObj.start;
+ this.metrics.responseTimes.push(responseTime);
+
+ // Keep only last 100 response times for rolling average
+ if (this.metrics.responseTimes.length > 100) {
+ this.metrics.responseTimes.shift();
+ }
+
+ // Update max response time
+ if (responseTime > this.metrics.maxResponseTime) {
+ this.metrics.maxResponseTime = responseTime;
+ }
+
+ // Calculate average response time
+ this.metrics.avgResponseTime =
+ this.metrics.responseTimes.reduce((a, b) => a + b, 0) / this.metrics.responseTimes.length;
+
+ // Track completion type
+ if (requestObj.attempts > 1) {
+ this.metrics.timeouts += requestObj.attempts - 1;
+ }
+
+ if (requestObj.success === false) {
+ this.metrics.failedRequests++;
+
+ // Track error types
+ const errorType = requestObj.errorType || "unknown";
+ const errorCount = this.metrics.errorsByType.get(errorType) || 0;
+ this.metrics.errorsByType.set(errorType, errorCount + 1);
+ } else {
+ this.metrics.completedRequests++;
+ }
+
+ this._checkAlerts();
+ }
+
+ /**
+ * Check for alert conditions
+ */
+ _checkAlerts() {
+ const queueSize = this.msp.callbacks?.length ?? 0;
+
+ // Queue full alert
+ const wasQueueFull = this.alerts.queueFull;
+ this.alerts.queueFull = queueSize > this.thresholds.maxQueueSize;
+
+ // High timeout rate alert
+ const timeoutRate = this.metrics.totalRequests > 0 ? this.metrics.timeouts / this.metrics.totalRequests : 0;
+ const wasHighTimeout = this.alerts.highTimeout;
+ this.alerts.highTimeout = timeoutRate > this.thresholds.maxTimeoutRate;
+
+ // Slow responses alert
+ const wasSlowResponses = this.alerts.slowResponses;
+ this.alerts.slowResponses = this.metrics.avgResponseTime > this.thresholds.maxAvgResponseTime;
+
+ // Memory leak detection (callbacks not being cleaned up)
+ const wasMemoryLeak = this.alerts.memoryLeak;
+ this.alerts.memoryLeak = queueSize > this.thresholds.memoryLeakThreshold;
+
+ // Debug logging for alert changes (only when alerts become active)
+ if (this.alerts.queueFull !== wasQueueFull && this.alerts.queueFull) {
+ console.warn(`π¨ Queue Full Alert: size ${queueSize}/${this.thresholds.maxQueueSize}`);
+ }
+ if (this.alerts.highTimeout !== wasHighTimeout && this.alerts.highTimeout) {
+ console.warn(`β±οΈ High Timeout Alert: rate ${(timeoutRate * 100).toFixed(1)}%`);
+ }
+ if (this.alerts.slowResponses !== wasSlowResponses && this.alerts.slowResponses) {
+ console.warn(`π Slow Response Alert: avg ${this.metrics.avgResponseTime}ms`);
+ }
+ if (this.alerts.memoryLeak !== wasMemoryLeak && this.alerts.memoryLeak) {
+ console.warn(`πΎ Memory Leak Alert: callbacks ${queueSize}`);
+ }
+
+ // Notify listeners of alerts
+ this._notifyListeners();
+ }
+
+ /**
+ * Start monitoring
+ */
+ startMonitoring(intervalMs = 1000) {
+ if (this.isMonitoring) {
+ return;
+ }
+
+ this.isMonitoring = true;
+ this.monitoringInterval = setInterval(() => {
+ this._collectMetrics();
+ this._notifyListeners();
+ }, intervalMs);
+
+ console.log("MSP Queue Monitor started");
+ }
+
+ /**
+ * Stop monitoring
+ */
+ stopMonitoring() {
+ if (!this.isMonitoring) {
+ return;
+ }
+
+ this.isMonitoring = false;
+ if (this.monitoringInterval) {
+ clearInterval(this.monitoringInterval);
+ this.monitoringInterval = null;
+ }
+
+ console.log("MSP Queue Monitor stopped");
+ }
+
+ /**
+ * Collect current metrics snapshot
+ */
+ _collectMetrics() {
+ // Update current queue size
+ this.currentQueueSize = this.msp.callbacks.length;
+
+ // Calculate success rate
+ this.metrics.successRate =
+ this.metrics.totalRequests > 0 ? this.metrics.completedRequests / this.metrics.totalRequests : 0;
+
+ // Calculate timeout rate
+ this.metrics.timeoutRate =
+ this.metrics.totalRequests > 0 ? this.metrics.timeouts / this.metrics.totalRequests : 0;
+ }
+
+ /**
+ * Get current status report
+ */
+ getStatus() {
+ return {
+ isMonitoring: this.isMonitoring,
+ currentQueueSize: this.msp.callbacks.length,
+ maxQueueSize: this.msp.MAX_QUEUE_SIZE || 100,
+ metrics: { ...this.metrics },
+ alerts: { ...this.alerts },
+ queueContents: this.msp.callbacks.map((req) => ({
+ code: req.code,
+ attempts: req.attempts || 0,
+ age: performance.now() - req.start,
+ hasTimer: !!req.timer,
+ })),
+ };
+ }
+
+ /**
+ * Get detailed queue analysis
+ */
+ analyzeQueue() {
+ const callbacks = this.msp.callbacks;
+ const now = performance.now();
+
+ const analysis = {
+ totalItems: callbacks.length,
+ byCode: {},
+ ageDistribution: {
+ fresh: 0, // < 1s
+ recent: 0, // 1-5s
+ stale: 0, // 5-10s
+ ancient: 0, // > 10s
+ },
+ retryDistribution: {
+ firstAttempt: 0,
+ retrying: 0,
+ multipleRetries: 0,
+ },
+ potentialIssues: [],
+ };
+
+ callbacks.forEach((req) => {
+ // Group by code
+ if (!analysis.byCode[req.code]) {
+ analysis.byCode[req.code] = 0;
+ }
+ analysis.byCode[req.code]++;
+
+ // Age analysis
+ const age = now - req.start;
+ if (age < 1000) {
+ analysis.ageDistribution.fresh++;
+ } else if (age < 5000) {
+ analysis.ageDistribution.recent++;
+ } else if (age < 10000) {
+ analysis.ageDistribution.stale++;
+ } else {
+ analysis.ageDistribution.ancient++;
+ }
+
+ // Retry analysis
+ const attempts = req.attempts || 0;
+ if (attempts === 0) {
+ analysis.retryDistribution.firstAttempt++;
+ } else if (attempts === 1) {
+ analysis.retryDistribution.retrying++;
+ } else {
+ analysis.retryDistribution.multipleRetries++;
+ }
+
+ // Identify potential issues
+ if (age > 10000) {
+ analysis.potentialIssues.push(`Ancient request: code ${req.code}, age ${Math.round(age / 1000)}s`);
+ }
+ if (attempts > 5) {
+ analysis.potentialIssues.push(`High retry count: code ${req.code}, attempts ${attempts}`);
+ }
+ if (!req.timer) {
+ analysis.potentialIssues.push(`Missing timer: code ${req.code}`);
+ }
+ });
+
+ return analysis;
+ }
+
+ /**
+ * Add a listener for monitoring events
+ */
+ addListener(callback) {
+ this.listeners.push(callback);
+ }
+
+ /**
+ * Remove a listener
+ */
+ removeListener(callback) {
+ const index = this.listeners.indexOf(callback);
+ if (index > -1) {
+ this.listeners.splice(index, 1);
+ }
+ }
+
+ /**
+ * Notify all listeners
+ */
+ _notifyListeners() {
+ const status = this.getStatus();
+ this.listeners.forEach((listener) => {
+ try {
+ listener(status);
+ } catch (error) {
+ console.error("Error in MSP monitor listener:", error);
+ }
+ });
+ }
+
+ /**
+ * Reset metrics only (keep alerts intact)
+ */
+ resetMetrics() {
+ this.metrics = {
+ totalRequests: 0,
+ completedRequests: 0,
+ failedRequests: 0,
+ timeouts: 0,
+ duplicates: 0,
+ avgResponseTime: 0,
+ maxResponseTime: 0,
+ queuePeakSize: 0,
+ requestsByCode: new Map(),
+ responseTimes: [],
+ errorsByType: new Map(),
+ };
+
+ // Note: Alerts are NOT reset here - they should only be cleared when conditions are no longer true
+ // or when explicitly requested via clearAlerts() method
+ }
+
+ /**
+ * Clear alerts (separate from metrics)
+ */
+ clearAlerts() {
+ console.log("π Clearing all alerts...");
+ this.alerts = {
+ queueFull: false,
+ highTimeout: false,
+ slowResponses: false,
+ memoryLeak: false,
+ };
+ this._notifyListeners();
+ }
+
+ /**
+ * Reset both metrics and alerts (complete reset)
+ */
+ resetAll() {
+ this.resetMetrics();
+ this.clearAlerts();
+ }
+
+ /**
+ * Generate a detailed report
+ */
+ generateReport() {
+ const status = this.getStatus();
+ const analysis = this.analyzeQueue();
+
+ return {
+ timestamp: new Date().toISOString(),
+ summary: {
+ queueHealth: this._assessQueueHealth(),
+ performanceGrade: this._calculatePerformanceGrade(),
+ recommendations: this._generateRecommendations(),
+ },
+ status,
+ analysis,
+ rawMetrics: this.metrics,
+ };
+ }
+
+ /**
+ * Assess overall queue health
+ */
+ _assessQueueHealth() {
+ const alerts = Object.values(this.alerts);
+ const activeAlerts = alerts.filter((alert) => alert).length;
+
+ if (activeAlerts === 0) {
+ return "HEALTHY";
+ }
+ if (activeAlerts <= 2) {
+ return "WARNING";
+ }
+ return "CRITICAL";
+ }
+
+ /**
+ * Calculate performance grade
+ */
+ _calculatePerformanceGrade() {
+ let score = 100;
+
+ // Deduct for high timeout rate
+ if (this.metrics.timeoutRate > 0.1) {
+ score -= 30;
+ } else if (this.metrics.timeoutRate > 0.05) {
+ score -= 15;
+ }
+
+ // Deduct for slow responses
+ if (this.metrics.avgResponseTime > 2000) {
+ score -= 25;
+ } else if (this.metrics.avgResponseTime > 1000) {
+ score -= 10;
+ }
+
+ // Deduct for queue size issues
+ const currentQueueSize = this.currentQueueSize || (this.msp.callbacks?.length ?? 0);
+ const queueRatio = currentQueueSize / (this.msp.MAX_QUEUE_SIZE || 100);
+ if (queueRatio > 0.8) {
+ score -= 20;
+ } else if (queueRatio > 0.6) {
+ score -= 10;
+ }
+
+ // Deduct for failed requests
+ const failureRate =
+ this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
+ if (failureRate > 0.05) {
+ score -= 15;
+ }
+
+ if (score >= 90) {
+ return "A";
+ }
+ if (score >= 80) {
+ return "B";
+ }
+ if (score >= 70) {
+ return "C";
+ }
+ if (score >= 60) {
+ return "D";
+ }
+ return "F";
+ }
+
+ /**
+ * Generate recommendations
+ */
+ _generateRecommendations() {
+ const recommendations = [];
+
+ if (this.alerts.queueFull) {
+ recommendations.push(
+ "Queue is near capacity. Consider implementing request prioritization or increasing queue size.",
+ );
+ }
+
+ if (this.alerts.highTimeout) {
+ recommendations.push(
+ "High timeout rate detected. Check serial connection stability or increase timeout values.",
+ );
+ }
+
+ if (this.alerts.slowResponses) {
+ recommendations.push(
+ "Slow response times detected. Investigate flight controller performance or reduce request frequency.",
+ );
+ }
+
+ if (this.alerts.memoryLeak) {
+ recommendations.push(
+ "Potential memory leak detected. Check that all requests are being properly cleaned up.",
+ );
+ }
+
+ if (this.metrics.maxResponseTime > 5000) {
+ recommendations.push(
+ "Some requests are taking very long to complete. Consider implementing request timeouts.",
+ );
+ }
+
+ return recommendations;
+ }
+
+ /**
+ * Test the alert system by manually triggering alerts
+ */
+ triggerTestAlerts() {
+ console.log("π§ͺ Triggering test alerts...");
+
+ // Store original alerts
+ const originalAlerts = { ...this.alerts };
+
+ // Trigger all alerts
+ this.alerts.queueFull = true;
+ this.alerts.highTimeout = true;
+ this.alerts.slowResponses = true;
+ this.alerts.memoryLeak = true;
+
+ console.log("π¨ Test alerts triggered:", this.alerts);
+
+ // Notify listeners immediately
+ this._notifyListeners();
+
+ // Reset after 10 seconds
+ setTimeout(() => {
+ this.alerts = originalAlerts;
+ console.log("β
Test alerts reset");
+ this._notifyListeners();
+ }, 10000);
+
+ return this.alerts;
+ }
+
+ /**
+ * Lower alert thresholds for testing
+ */
+ setTestThresholds() {
+ console.log("π― Setting test thresholds for easier alert triggering...");
+ this.thresholds = {
+ maxQueueSize: 1, // Alert when queue > 1
+ maxAvgResponseTime: 100, // Alert when avg response > 100ms
+ maxTimeoutRate: 0.01, // Alert when timeout rate > 1%
+ memoryLeakThreshold: 5, // Alert when callbacks > 5
+ };
+ console.log("New thresholds:", this.thresholds);
+ }
+
+ /**
+ * Reset to normal thresholds
+ */
+ setNormalThresholds() {
+ console.log("π§ Resetting to normal thresholds...");
+ this.thresholds = {
+ maxQueueSize: Math.floor((this.msp.MAX_QUEUE_SIZE || 100) * 0.8), // Alert when queue > 80% of MAX_QUEUE_SIZE
+ maxAvgResponseTime: 2000, // Alert when avg response > 2s
+ maxTimeoutRate: 0.1, // Alert when timeout rate > 10%
+ memoryLeakThreshold: 100, // Alert when callbacks grow beyond expected
+ };
+ console.log("Normal thresholds restored:", this.thresholds);
+ }
+
+ /**
+ * Cleanup and restore original MSP methods
+ */
+ destroy() {
+ this.stopMonitoring();
+
+ // Restore original methods
+ if (this.originalSendMessage) {
+ this.msp.send_message = this.originalSendMessage;
+ }
+ if (this.originalDispatchMessage) {
+ this.msp._dispatch_message = this.originalDispatchMessage;
+ }
+ if (this.originalRemoveRequest) {
+ this.msp._removeRequestFromCallbacks = this.originalRemoveRequest;
+ }
+
+ // Clear instrumentation flag
+ this.msp._mspQueueMonitorInstrumented = undefined;
+
+ this.listeners = [];
+
+ // Clear the singleton instance to allow creating a fresh monitor later
+ _mspQueueMonitorInstance = null;
+ }
+}
+
+// Lazy initialization to avoid errors when window.MSP is not yet available
+let _mspQueueMonitorInstance = null;
+
+export const mspQueueMonitor = {
+ get instance() {
+ if (!_mspQueueMonitorInstance) {
+ if (typeof window === "undefined" || !window.MSP) {
+ throw new Error(
+ "MSP Queue Monitor: window.MSP is not available. Make sure MSP is loaded before using the monitor.",
+ );
+ }
+ _mspQueueMonitorInstance = new MSPQueueMonitor(window.MSP);
+ }
+ return _mspQueueMonitorInstance;
+ },
+
+ // Proxy all methods to the lazy-initialized instance
+ startMonitoring(...args) {
+ return this.instance.startMonitoring(...args);
+ },
+ stopMonitoring(...args) {
+ return this.instance.stopMonitoring(...args);
+ },
+ getStatus(...args) {
+ return this.instance.getStatus(...args);
+ },
+ analyzeQueue(...args) {
+ return this.instance.analyzeQueue(...args);
+ },
+ addListener(...args) {
+ return this.instance.addListener(...args);
+ },
+ removeListener(...args) {
+ return this.instance.removeListener(...args);
+ },
+ resetMetrics(...args) {
+ return this.instance.resetMetrics(...args);
+ },
+ clearAlerts(...args) {
+ return this.instance.clearAlerts(...args);
+ },
+ resetAll(...args) {
+ return this.instance.resetAll(...args);
+ },
+ generateReport(...args) {
+ return this.instance.generateReport(...args);
+ },
+ triggerTestAlerts(...args) {
+ return this.instance.triggerTestAlerts(...args);
+ },
+ setTestThresholds(...args) {
+ return this.instance.setTestThresholds(...args);
+ },
+ setNormalThresholds(...args) {
+ return this.instance.setNormalThresholds(...args);
+ },
+ destroy(...args) {
+ return this.instance.destroy(...args);
+ },
+
+ // Getters for properties
+ get isMonitoring() {
+ return this.instance.isMonitoring;
+ },
+ get metrics() {
+ return this.instance.metrics;
+ },
+ get alerts() {
+ return this.instance.alerts;
+ },
+ get thresholds() {
+ return this.instance.thresholds;
+ },
+};
diff --git a/src/js/msp/msp_stress_test.js b/src/js/msp/msp_stress_test.js
new file mode 100644
index 00000000..679a7dc6
--- /dev/null
+++ b/src/js/msp/msp_stress_test.js
@@ -0,0 +1,756 @@
+/**
+ * MSP Stress Test Framework
+ * Comprehensive testing tool for MSP queue management, timeout handling, and performance
+ */
+
+import { mspQueueMonitor } from "./msp_queue_monitor.js";
+
+export class MSPStressTest {
+ constructor(mspInstance) {
+ this.msp = mspInstance;
+ this.monitor = mspQueueMonitor; // Reuse singleton to avoid duplicate method patching
+ this.isRunning = false;
+ this.testResults = [];
+ this.currentTest = null;
+
+ // Common MSP codes for testing
+ this.testCodes = {
+ MSP_IDENT: 100,
+ MSP_STATUS: 101,
+ MSP_RAW_IMU: 102,
+ MSP_SERVO: 103,
+ MSP_MOTOR: 104,
+ MSP_RC: 105,
+ MSP_RAW_GPS: 106,
+ MSP_COMP_GPS: 107,
+ MSP_ATTITUDE: 108,
+ MSP_ALTITUDE: 109,
+ MSP_ANALOG: 110,
+ MSP_RC_TUNING: 111,
+ MSP_PID: 112,
+ MSP_PIDNAMES: 116,
+ MSP_BOXNAMES: 117,
+ MSP_MISC: 114,
+ MSP_MOTOR_PINS: 115,
+ };
+ }
+
+ /**
+ * Run a comprehensive stress test suite
+ */
+ async runStressTestSuite() {
+ console.log("π Starting MSP Stress Test Suite");
+ this.isRunning = true;
+ this.monitor.startMonitoring(100); // High frequency monitoring during tests
+
+ const tests = [
+ { name: "Queue Flooding", test: () => this.testQueueFlooding() },
+ { name: "Rapid Fire Requests", test: () => this.testRapidFireRequests() },
+ { name: "Duplicate Request Handling", test: () => this.testDuplicateRequests() },
+ { name: "Timeout Recovery", test: () => this.testTimeoutRecovery() },
+ { name: "Memory Leak Detection", test: () => this.testMemoryLeaks() },
+ { name: "Concurrent Mixed Requests", test: () => this.testConcurrentMixedRequests() },
+ { name: "Queue Overflow Handling", test: () => this.testQueueOverflow() },
+ { name: "Connection Disruption", test: () => this.testConnectionDisruption() },
+ { name: "Performance Under Load", test: () => this.testPerformanceUnderLoad() },
+ ];
+
+ const results = [];
+
+ try {
+ for (const testDef of tests) {
+ try {
+ console.log(`\nπ Running: ${testDef.name}`);
+ this.currentTest = testDef.name;
+ this.monitor.resetAll(); // Reset both metrics and alerts for clean test start
+
+ const startTime = performance.now();
+ const result = await testDef.test();
+ const duration = performance.now() - startTime;
+
+ const testResult = {
+ name: testDef.name,
+ status: "PASSED",
+ duration,
+ result,
+ metrics: this.monitor.getStatus(),
+ timestamp: new Date().toISOString(),
+ };
+
+ results.push(testResult);
+ console.log(`β
${testDef.name} completed in ${Math.round(duration)}ms`);
+
+ // Wait between tests to let queue settle
+ await this.wait(1000);
+ } catch (error) {
+ console.error(`β ${testDef.name} failed:`, error);
+ results.push({
+ name: testDef.name,
+ status: "FAILED",
+ error: error.message,
+ timestamp: new Date().toISOString(),
+ });
+ }
+ }
+
+ this.testResults = results;
+ const report = this.generateTestReport(results);
+ console.log("\nπ Stress Test Suite Complete");
+ console.log(report.summary);
+
+ return report;
+ } finally {
+ // Ensure cleanup always happens regardless of errors
+ this.monitor.stopMonitoring();
+ this.isRunning = false;
+ this.currentTest = null;
+ }
+ }
+
+ /**
+ * Test 1: Queue Flooding - Send many requests quickly to test queue limits
+ */
+ async testQueueFlooding() {
+ const requestCount = 110; // More than default MAX_QUEUE_SIZE
+ const promises = [];
+
+ console.log(` Flooding queue with ${requestCount} requests...`);
+
+ for (let i = 0; i < requestCount; i++) {
+ const code = Object.values(this.testCodes)[i % Object.keys(this.testCodes).length];
+ const promise = this.msp.promise(code, null).catch((err) => ({ error: err.message }));
+ promises.push(promise);
+ }
+
+ const results = await Promise.allSettled(promises);
+ const successful = results.filter((r) => r.status === "fulfilled" && !(r.value && r.value.error)).length;
+ const failed = results.length - successful;
+
+ return {
+ requestsSent: requestCount,
+ successful,
+ failed,
+ successRate: successful / requestCount,
+ peakQueueSize: (this.monitor.getStatus().metrics || {}).queuePeakSize ?? 0,
+ };
+ }
+
+ /**
+ * Test 2: Rapid Fire Requests - Send requests in rapid succession
+ */
+ async testRapidFireRequests() {
+ const requestCount = 20;
+ const interval = 10; // 10ms between request initiation
+
+ console.log(` Sending ${requestCount} requests with ${interval}ms intervals...`);
+
+ const promises = [];
+ const requestStartTimes = [];
+ const startTime = performance.now();
+
+ // Create all requests concurrently with timed intervals
+ for (let i = 0; i < requestCount; i++) {
+ const code = this.testCodes.MSP_STATUS;
+ const requestStart = performance.now();
+ requestStartTimes.push(requestStart);
+
+ // Create promise without awaiting to allow concurrency
+ const promise = this.msp
+ .promise(code, null)
+ .then(() => ({
+ success: true,
+ responseTime: performance.now() - requestStart,
+ index: i,
+ }))
+ .catch((error) => ({
+ success: false,
+ error: error.message,
+ responseTime: performance.now() - requestStart,
+ index: i,
+ }));
+
+ promises.push(promise);
+
+ // Wait interval before starting next request (except for last)
+ if (i < requestCount - 1) {
+ await this.wait(interval);
+ }
+ }
+
+ // Wait for all requests to complete
+ const results = await Promise.allSettled(promises);
+ const totalTime = performance.now() - startTime;
+
+ // Extract results from settled promises
+ const processedResults = results.map((settled) => {
+ if (settled.status === "fulfilled") {
+ return settled.value;
+ } else {
+ return {
+ success: false,
+ error: settled.reason?.message || "Unknown error",
+ responseTime: 0,
+ index: -1,
+ };
+ }
+ });
+
+ const successful = processedResults.filter((r) => r.success).length;
+ const responseTimes = processedResults.map((r) => r.responseTime).filter((t) => t > 0);
+ const avgResponseTime =
+ responseTimes.length > 0 ? responseTimes.reduce((sum, r) => sum + r, 0) / responseTimes.length : 0;
+
+ return {
+ requestCount,
+ successful,
+ failed: requestCount - successful,
+ totalTime,
+ avgResponseTime,
+ throughput: requestCount / (totalTime / 1000), // requests per second
+ concurrentRequests: true,
+ maxConcurrentRequests: requestCount,
+ };
+ }
+
+ /**
+ * Test 3: Duplicate Request Handling
+ */
+ async testDuplicateRequests() {
+ const code = this.testCodes.MSP_IDENT;
+ const data = new Uint8Array([1, 2, 3]); // Same data for all requests
+ const duplicateCount = 5;
+
+ console.log(` Sending ${duplicateCount} duplicate requests...`);
+
+ const promises = [];
+ for (let i = 0; i < duplicateCount; i++) {
+ promises.push(this.msp.promise(code, data).catch((err) => ({ error: err.message })));
+ }
+
+ const results = await Promise.allSettled(promises);
+ const successful = results.filter((r) => r.status === "fulfilled" && !(r.value && r.value.error)).length;
+ const duplicateErrors = results.filter(
+ (r) => r.status === "rejected" || (r.value && r.value.error && r.value.error.includes("duplicate")),
+ ).length;
+
+ return {
+ duplicatesSent: duplicateCount,
+ successful,
+ duplicateRejections: duplicateErrors,
+ queueSizeAfter: this.msp.callbacks.length,
+ };
+ }
+
+ /**
+ * Test 4: Timeout Recovery
+ */
+ async testTimeoutRecovery() {
+ console.log(" Testing timeout recovery...");
+
+ // Save original timeout
+ const originalTimeout = this.msp.TIMEOUT;
+ this.msp.TIMEOUT = 100; // Very short timeout for testing
+
+ try {
+ const code = this.testCodes.MSP_STATUS;
+ const startTime = performance.now();
+
+ try {
+ await this.msp.promise(code, null);
+ return { error: "Expected timeout but request succeeded" };
+ } catch (error) {
+ const timeoutTime = performance.now() - startTime;
+
+ // Test that new requests work after timeout
+ this.msp.TIMEOUT = originalTimeout;
+ await this.wait(200);
+
+ const recoveryStart = performance.now();
+ await this.msp.promise(this.testCodes.MSP_IDENT, null);
+ const recoveryTime = performance.now() - recoveryStart;
+
+ return {
+ timeoutOccurred: true,
+ timeoutDuration: timeoutTime,
+ recoveryTime,
+ queueCleanedUp: this.msp.callbacks.length === 0,
+ };
+ }
+ } finally {
+ this.msp.TIMEOUT = originalTimeout;
+ }
+ }
+
+ /**
+ * Test 5: Memory Leak Detection
+ */
+ async testMemoryLeaks() {
+ console.log(" Testing for memory leaks...");
+
+ const initialCallbackCount = this.msp.callbacks.length;
+ const requestCount = 10;
+
+ // Send requests and let them complete
+ const promises = [];
+ for (let i = 0; i < requestCount; i++) {
+ promises.push(this.msp.promise(this.testCodes.MSP_STATUS, null).catch(() => {}));
+ }
+
+ await Promise.allSettled(promises);
+ await this.wait(100); // Let cleanup complete
+
+ const finalCallbackCount = this.msp.callbacks.length;
+ const leaked = finalCallbackCount - initialCallbackCount;
+
+ return {
+ initialCallbacks: initialCallbackCount,
+ finalCallbacks: finalCallbackCount,
+ leaked,
+ memoryLeakDetected: leaked > 0,
+ requestsProcessed: requestCount,
+ };
+ }
+
+ /**
+ * Test 6: Concurrent Mixed Requests
+ */
+ async testConcurrentMixedRequests() {
+ console.log(" Testing concurrent mixed requests...");
+
+ const promises = [];
+ const codes = Object.values(this.testCodes);
+
+ // Mix of different request types
+ for (let i = 0; i < 15; i++) {
+ const code = codes[i % codes.length];
+ const data = i % 3 === 0 ? new Uint8Array([i]) : null;
+
+ promises.push(this.msp.promise(code, data).catch((err) => ({ error: err.message })));
+ }
+
+ const startTime = performance.now();
+ const results = await Promise.allSettled(promises);
+ const totalTime = performance.now() - startTime;
+
+ const successful = results.filter((r) => r.status === "fulfilled" && !(r.value && r.value.error)).length;
+
+ return {
+ totalRequests: promises.length,
+ successful,
+ failed: promises.length - successful,
+ totalTime,
+ concurrentProcessing: true,
+ };
+ }
+
+ /**
+ * Test 7: Queue Overflow Handling
+ */
+ async testQueueOverflow() {
+ console.log(" Testing queue overflow handling...");
+
+ const maxQueue = this.msp.MAX_QUEUE_SIZE || 100;
+ const overflowCount = maxQueue + 10;
+
+ const promises = [];
+ for (let i = 0; i < overflowCount; i++) {
+ promises.push(this.msp.promise(this.testCodes.MSP_STATUS, null).catch((err) => ({ error: err.message })));
+ }
+
+ const results = await Promise.allSettled(promises);
+ const rejected = results.filter((r) => r.status === "rejected" || r.value?.error).length;
+
+ return {
+ attemptedRequests: overflowCount,
+ maxQueueSize: maxQueue,
+ rejectedDueToOverflow: rejected,
+ overflowHandled: rejected > 0,
+ finalQueueSize: this.msp.callbacks.length,
+ };
+ }
+
+ /**
+ * Test 8: Connection Disruption Simulation
+ */
+ async testConnectionDisruption() {
+ console.log(" Simulating connection disruption...");
+
+ // This test would need to work with the actual serial implementation
+ // For now, we'll simulate by temporarily breaking the connection
+
+ const originalConnected = this.msp.serial?.connected;
+
+ try {
+ // Simulate disconnection
+ if (this.msp.serial) {
+ this.msp.serial.connected = false;
+ }
+
+ // Try to send requests while "disconnected"
+ const promises = [];
+ for (let i = 0; i < 5; i++) {
+ promises.push(
+ this.msp.promise(this.testCodes.MSP_STATUS, null).catch((err) => ({ error: err.message })),
+ );
+ }
+
+ const disconnectedResults = await Promise.allSettled(promises);
+ const failedWhileDisconnected = disconnectedResults.filter(
+ (r) => r.status === "rejected" || r.value?.error,
+ ).length;
+
+ // Restore connection
+ if (this.msp.serial) {
+ this.msp.serial.connected = originalConnected;
+ }
+
+ // Test recovery
+ await this.wait(100);
+ const recoveryResult = await this.msp
+ .promise(this.testCodes.MSP_IDENT, null)
+ .catch((err) => ({ error: err.message }));
+
+ return {
+ failedWhileDisconnected,
+ recoverySuccessful: !recoveryResult.error,
+ connectionHandled: failedWhileDisconnected > 0,
+ };
+ } finally {
+ // Ensure connection is restored
+ if (this.msp.serial) {
+ this.msp.serial.connected = originalConnected;
+ }
+ }
+ }
+
+ /**
+ * Test 9: Performance Under Load
+ */
+ async testPerformanceUnderLoad() {
+ console.log(" Testing performance under sustained load...");
+
+ const duration = 5000; // 5 seconds
+ const requestInterval = 20; // Request every 20ms for higher concurrency
+ const batchSize = 10; // Process batches of 10 concurrent requests
+ const batchInterval = 200; // Process batches every 200ms
+
+ const startTime = performance.now();
+ const results = [];
+ let requestCount = 0;
+ let pendingPromises = [];
+ let lastBatchTime = startTime;
+
+ while (performance.now() - startTime < duration) {
+ const requestStart = performance.now();
+ requestCount++;
+
+ // Create promise without awaiting to allow concurrency
+ const promise = this.msp
+ .promise(this.testCodes.MSP_STATUS, null)
+ .then(() => ({
+ success: true,
+ responseTime: performance.now() - requestStart,
+ }))
+ .catch((error) => ({
+ success: false,
+ responseTime: performance.now() - requestStart,
+ error: error.message,
+ }));
+
+ pendingPromises.push(promise);
+
+ // Process batch when we hit batch size or time interval
+ const now = performance.now();
+ if (pendingPromises.length >= batchSize || now - lastBatchTime >= batchInterval) {
+ const batchResults = await Promise.allSettled(pendingPromises);
+
+ // Extract results from settled promises
+ batchResults.forEach((settled) => {
+ if (settled.status === "fulfilled") {
+ results.push(settled.value);
+ } else {
+ results.push({
+ success: false,
+ responseTime: 0,
+ error: settled.reason?.message || "Unknown error",
+ });
+ }
+ });
+
+ pendingPromises = [];
+ lastBatchTime = now;
+ }
+
+ await this.wait(requestInterval);
+ }
+
+ // Process any remaining pending promises
+ if (pendingPromises.length > 0) {
+ const finalBatchResults = await Promise.allSettled(pendingPromises);
+ finalBatchResults.forEach((settled) => {
+ if (settled.status === "fulfilled") {
+ results.push(settled.value);
+ } else {
+ results.push({
+ success: false,
+ responseTime: 0,
+ error: settled.reason?.message || "Unknown error",
+ });
+ }
+ });
+ }
+
+ const successful = results.filter((r) => r.success).length;
+ const responseTimes = results.map((r) => r.responseTime).filter((t) => t > 0);
+ const avgResponseTime =
+ responseTimes.length > 0 ? responseTimes.reduce((sum, r) => sum + r, 0) / responseTimes.length : 0;
+ const maxResponseTime = responseTimes.length > 0 ? Math.max(...responseTimes) : 0;
+
+ return {
+ duration,
+ requestCount,
+ successful,
+ failed: requestCount - successful,
+ successRate: successful / requestCount,
+ avgResponseTime,
+ maxResponseTime,
+ throughput: requestCount / (duration / 1000),
+ concurrentRequests: true,
+ batchSize,
+ maxConcurrentRequests: batchSize,
+ };
+ }
+
+ /**
+ * Generate comprehensive test report
+ */
+ generateTestReport(results) {
+ const totalTests = results.length;
+ const passedTests = results.filter((r) => r.status === "PASSED").length;
+ const failedTests = totalTests - passedTests;
+
+ const summary = {
+ totalTests,
+ passed: passedTests,
+ failed: failedTests,
+ successRate: passedTests / totalTests,
+ overallGrade: this._calculateOverallGrade(results),
+ };
+
+ const recommendations = this._generateTestRecommendations(results);
+
+ return {
+ timestamp: new Date().toISOString(),
+ summary,
+ recommendations,
+ detailedResults: results,
+ monitorReport: this.monitor.generateReport(),
+ };
+ }
+
+ /**
+ * Calculate overall test grade
+ */
+ _calculateOverallGrade(results) {
+ const passRate = results.filter((r) => r.status === "PASSED").length / results.length;
+
+ if (passRate >= 0.95) {
+ return "A+";
+ }
+ if (passRate >= 0.9) {
+ return "A";
+ }
+ if (passRate >= 0.85) {
+ return "B+";
+ }
+ if (passRate >= 0.8) {
+ return "B";
+ }
+ if (passRate >= 0.75) {
+ return "C+";
+ }
+ if (passRate >= 0.7) {
+ return "C";
+ }
+ if (passRate >= 0.6) {
+ return "D";
+ }
+ return "F";
+ }
+
+ /**
+ * Generate recommendations based on test results
+ */
+ _generateTestRecommendations(results) {
+ const recommendations = [];
+
+ // Check for specific test failures
+ const failedTests = results.filter((r) => r.status === "FAILED");
+ if (failedTests.length > 0) {
+ recommendations.push(
+ `${failedTests.length} tests failed. Review implementation for: ${failedTests.map((t) => t.name).join(", ")}`,
+ );
+ }
+
+ // Check performance issues
+ const perfTest = results.find((r) => r.name === "Performance Under Load");
+ if (perfTest?.result?.avgResponseTime > 1000) {
+ recommendations.push("Average response time is high. Consider optimizing MSP request handling.");
+ }
+
+ // Check memory leaks
+ const memTest = results.find((r) => r.name === "Memory Leak Detection");
+ if (memTest?.result?.memoryLeakDetected) {
+ recommendations.push("Memory leak detected. Ensure all callbacks are properly cleaned up.");
+ }
+
+ // Check queue overflow handling
+ const overflowTest = results.find((r) => r.name === "Queue Overflow Handling");
+ if (!overflowTest?.result?.overflowHandled) {
+ recommendations.push("Queue overflow not properly handled. Implement proper queue management.");
+ }
+
+ return recommendations;
+ }
+
+ /**
+ * Utility: Wait for specified milliseconds
+ */
+ wait(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ /**
+ * Run a specific test by name
+ */
+ async runSpecificTest(testName) {
+ const testMethods = {
+ "queue-flooding": () => this.testQueueFlooding(),
+ "rapid-fire": () => this.testRapidFireRequests(),
+ duplicates: () => this.testDuplicateRequests(),
+ "timeout-recovery": () => this.testTimeoutRecovery(),
+ "memory-leaks": () => this.testMemoryLeaks(),
+ "concurrent-mixed": () => this.testConcurrentMixedRequests(),
+ "queue-overflow": () => this.testQueueOverflow(),
+ "connection-disruption": () => this.testConnectionDisruption(),
+ "performance-load": () => this.testPerformanceUnderLoad(),
+ };
+
+ const testMethod = testMethods[testName];
+ if (!testMethod) {
+ throw new Error(`Unknown test: ${testName}`);
+ }
+
+ console.log(`π§ͺ Running specific test: ${testName}`);
+ this.monitor.startMonitoring(100);
+ this.monitor.resetAll(); // Reset both metrics and alerts for clean test start
+
+ try {
+ const result = await testMethod();
+ return {
+ name: testName,
+ status: "PASSED",
+ result,
+ metrics: this.monitor.getStatus(),
+ };
+ } catch (error) {
+ return {
+ name: testName,
+ status: "FAILED",
+ error: error.message,
+ };
+ } finally {
+ this.monitor.stopMonitoring();
+ }
+ }
+
+ /**
+ * Cleanup
+ */
+ destroy() {
+ // Only stop monitoring, don't destroy the shared singleton
+ if (this.monitor.isMonitoring) {
+ this.monitor.stopMonitoring();
+ }
+ }
+}
+
+// Lazy initialization to avoid errors when window.MSP is not yet available
+let _mspStressTestInstance = null;
+
+export const mspStressTest = {
+ get instance() {
+ if (!_mspStressTestInstance) {
+ if (typeof window === "undefined" || !window.MSP) {
+ throw new Error(
+ "MSP Stress Test: window.MSP is not available. Make sure MSP is loaded before using the stress test.",
+ );
+ }
+ _mspStressTestInstance = new MSPStressTest(window.MSP);
+ }
+ return _mspStressTestInstance;
+ },
+
+ // Proxy all methods to the lazy-initialized instance
+ runStressTestSuite(...args) {
+ return this.instance.runStressTestSuite(...args);
+ },
+ runSpecificTest(...args) {
+ return this.instance.runSpecificTest(...args);
+ },
+ generateTestReport(...args) {
+ return this.instance.generateTestReport(...args);
+ },
+ wait(...args) {
+ return this.instance.wait(...args);
+ },
+ destroy(...args) {
+ return this.instance.destroy(...args);
+ },
+
+ // Test methods
+ testQueueFlooding(...args) {
+ return this.instance.testQueueFlooding(...args);
+ },
+ testRapidFireRequests(...args) {
+ return this.instance.testRapidFireRequests(...args);
+ },
+ testDuplicateRequests(...args) {
+ return this.instance.testDuplicateRequests(...args);
+ },
+ testTimeoutRecovery(...args) {
+ return this.instance.testTimeoutRecovery(...args);
+ },
+ testMemoryLeaks(...args) {
+ return this.instance.testMemoryLeaks(...args);
+ },
+ testConcurrentMixedRequests(...args) {
+ return this.instance.testConcurrentMixedRequests(...args);
+ },
+ testQueueOverflow(...args) {
+ return this.instance.testQueueOverflow(...args);
+ },
+ testConnectionDisruption(...args) {
+ return this.instance.testConnectionDisruption(...args);
+ },
+ testPerformanceUnderLoad(...args) {
+ return this.instance.testPerformanceUnderLoad(...args);
+ },
+
+ // Getters for properties
+ get monitor() {
+ return this.instance.monitor;
+ },
+ get isRunning() {
+ return this.instance.isRunning;
+ },
+ get testResults() {
+ return this.instance.testResults;
+ },
+ get currentTest() {
+ return this.instance.currentTest;
+ },
+ get testCodes() {
+ return this.instance.testCodes;
+ },
+};
diff --git a/src/js/msp/msp_test_runner.js b/src/js/msp/msp_test_runner.js
new file mode 100644
index 00000000..f54563af
--- /dev/null
+++ b/src/js/msp/msp_test_runner.js
@@ -0,0 +1,437 @@
+/**
+ * MSP Test Runner - Simple script to run tests and monitoring
+ * Usage examples from browser console:
+ *
+ * // Quick start monitoring
+ * MSPTestRunner.startQuickMonitor();
+ *
+ * // Run specific test
+ * MSPTestRunner.runTest('queue-flooding');
+ *
+ * // Run full stress test suite
+ * MSPTestRunner.runFullSuite();
+ *
+ * // Get current status
+ * MSPTestRunner.getStatus();
+ */
+
+import { mspQueueMonitor } from "./msp_queue_monitor.js";
+import { mspStressTest } from "./msp_stress_test.js";
+import { mspDebugDashboard } from "./msp_debug_dashboard.js";
+
+export const MSPTestRunner = {
+ // Store the listener function so it can be removed later
+ _quickMonitorListener: null,
+
+ /**
+ * Start quick monitoring with console output
+ */
+ startQuickMonitor() {
+ console.log("π Starting MSP Quick Monitor...");
+
+ // Remove any existing listener first
+ if (this._quickMonitorListener) {
+ mspQueueMonitor.removeListener(this._quickMonitorListener);
+ }
+
+ // Define the listener function so it can be referenced for removal
+ this._quickMonitorListener = (status) => {
+ if (status.alerts && Object.values(status.alerts).some((alert) => alert)) {
+ console.warn("π¨ MSP Alert:", status.alerts);
+ }
+
+ // Log every 10 seconds if monitoring
+ if (Date.now() % 10000 < 500) {
+ console.log(
+ `π MSP Status: Queue=${status.currentQueueSize}/${status.maxQueueSize}, Requests=${status.metrics.totalRequests}, AvgTime=${Math.round(status.metrics.avgResponseTime)}ms`,
+ );
+ }
+ };
+
+ mspQueueMonitor.addListener(this._quickMonitorListener);
+ mspQueueMonitor.startMonitoring(1000);
+ console.log("β
Quick monitor started. Use MSPTestRunner.stopMonitor() to stop.");
+
+ return {
+ stop: () => this.stopMonitor(),
+ status: () => this.getStatus(),
+ analyze: () => this.analyzeQueue(),
+ };
+ },
+
+ /**
+ * Stop monitoring
+ */
+ stopMonitor() {
+ mspQueueMonitor.stopMonitoring();
+
+ // Remove the listener to prevent duplicate logs
+ if (this._quickMonitorListener) {
+ mspQueueMonitor.removeListener(this._quickMonitorListener);
+ this._quickMonitorListener = null;
+ }
+
+ console.log("βΉοΈ MSP Monitor stopped");
+ },
+
+ /**
+ * Run a specific stress test
+ */
+ async runTest(testName) {
+ console.log(`π§ͺ Running MSP test: ${testName}`);
+
+ try {
+ const result = await mspStressTest.runSpecificTest(testName);
+
+ if (result.status === "PASSED") {
+ console.log(`β
Test ${testName} PASSED`);
+ console.table(result.result);
+ } else {
+ console.error(`β Test ${testName} FAILED:`, result.error);
+ }
+
+ return result;
+ } catch (error) {
+ console.error(`π₯ Test ${testName} crashed:`, error);
+ return { status: "ERROR", error: error.message };
+ }
+ },
+
+ /**
+ * Run the full stress test suite
+ */
+ async runFullSuite() {
+ console.log("π Running FULL MSP Stress Test Suite...");
+ console.log("This may take several minutes and will stress the MSP system.");
+
+ const startTime = Date.now();
+
+ try {
+ const results = await mspStressTest.runStressTestSuite();
+ const duration = Date.now() - startTime;
+
+ console.log(`\nπ Test Suite Complete (${Math.round(duration / 1000)}s)`);
+ console.log(`β
Passed: ${results.summary.passed}`);
+ console.log(`β Failed: ${results.summary.failed}`);
+ console.log(`π Success Rate: ${Math.round(results.summary.successRate * 100)}%`);
+ console.log(`π― Overall Grade: ${results.summary.overallGrade}`);
+
+ if (results.recommendations && results.recommendations.length > 0) {
+ console.log("\nπ‘ Recommendations:");
+ results.recommendations.forEach((rec) => console.log(` β’ ${rec}`));
+ }
+
+ // Show detailed results table
+ console.log("\nπ Detailed Results:");
+ console.table(
+ results.detailedResults.map((test) => ({
+ Test: test.name,
+ Status: test.status,
+ Duration: test.duration ? `${Math.round(test.duration)}ms` : "N/A",
+ })),
+ );
+
+ return results;
+ } catch (error) {
+ console.error("π₯ Test Suite Failed:", error);
+ return { error: error.message };
+ }
+ },
+
+ /**
+ * Get current MSP status
+ */
+ getStatus() {
+ const status = mspQueueMonitor.getStatus();
+
+ console.log("π Current MSP Status:");
+ console.log(` Queue: ${status.currentQueueSize}/${status.maxQueueSize}`);
+ console.log(` Total Requests: ${status.metrics.totalRequests}`);
+ console.log(` Success Rate: ${Math.round((status.metrics.successRate || 0) * 100)}%`);
+ console.log(` Avg Response Time: ${Math.round(status.metrics.avgResponseTime || 0)}ms`);
+ console.log(` Active Alerts: ${Object.values(status.alerts).filter((a) => a).length}`);
+
+ if (status.queueContents.length > 0) {
+ console.log("\nπ Queue Contents:");
+ console.table(status.queueContents);
+ }
+
+ return status;
+ },
+
+ /**
+ * Analyze current queue
+ */
+ analyzeQueue() {
+ const analysis = mspQueueMonitor.analyzeQueue();
+
+ console.log("π Queue Analysis:");
+ console.log(` Total Items: ${analysis.totalItems}`);
+ console.log(" Age Distribution:", analysis.ageDistribution);
+ console.log(" By Code:", analysis.byCode);
+
+ if (analysis.potentialIssues.length > 0) {
+ console.log("β οΈ Potential Issues:");
+ analysis.potentialIssues.forEach((issue) => console.log(` β’ ${issue}`));
+ }
+
+ return analysis;
+ },
+
+ /**
+ * Generate and download comprehensive report
+ */
+ generateReport() {
+ const report = mspQueueMonitor.generateReport();
+
+ console.log("π Generating MSP Report...");
+ console.log(" Queue Health:", report.summary.queueHealth);
+ console.log(" Performance Grade:", report.summary.performanceGrade);
+
+ // Create downloadable report
+ const blob = new Blob([JSON.stringify(report, null, 2)], {
+ type: "application/json",
+ });
+
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `msp-report-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ console.log("β
Report downloaded");
+ return report;
+ },
+
+ /**
+ * Show the visual dashboard
+ */
+ showDashboard() {
+ mspDebugDashboard.show();
+ console.log("π₯οΈ Debug dashboard opened. Press Ctrl+Shift+M to toggle.");
+ },
+
+ /**
+ * Run a quick health check
+ */
+ async quickHealthCheck() {
+ console.log("π₯ Running Quick MSP Health Check...");
+
+ if (!window.MSP) {
+ console.error("MSP not available");
+ return { status: "ERROR", error: "MSP not initialized" };
+ }
+
+ // Start monitoring briefly
+ mspQueueMonitor.startMonitoring(100);
+
+ // Send a few test requests
+ const testPromises = [
+ window.MSP.promise(100, null), // MSP_IDENT
+ window.MSP.promise(101, null), // MSP_STATUS
+ window.MSP.promise(108, null), // MSP_ATTITUDE
+ ];
+
+ try {
+ const startTime = Date.now();
+ await Promise.all(testPromises);
+ const responseTime = Date.now() - startTime;
+
+ // Get status after test
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ const status = mspQueueMonitor.getStatus();
+
+ mspQueueMonitor.stopMonitoring();
+
+ const health = {
+ status: "HEALTHY",
+ responseTime,
+ queueClearedAfterTest: status.currentQueueSize === 0,
+ successRate: status.metrics.successRate || 0,
+ };
+
+ if (responseTime > 2000) {
+ health.status = "SLOW";
+ health.warning = "Response times are slow";
+ }
+
+ if (!health.queueClearedAfterTest) {
+ health.status = "WARNING";
+ health.warning = "Queue not properly cleared after requests";
+ }
+
+ if (health.successRate < 1) {
+ health.status = "FAILING";
+ health.warning = "Some requests are failing";
+ }
+
+ console.log(`π₯ Health Check Result: ${health.status}`);
+ console.log(` Response Time: ${responseTime}ms`);
+ console.log(` Queue Cleared: ${health.queueClearedAfterTest ? "β
" : "β"}`);
+ console.log(` Success Rate: ${Math.round(health.successRate * 100)}%`);
+
+ if (health.warning) {
+ console.warn(`β οΈ ${health.warning}`);
+ }
+
+ return health;
+ } catch (error) {
+ mspQueueMonitor.stopMonitoring();
+ console.error("π₯ Health check failed:", error);
+ return { status: "ERROR", error: error.message };
+ }
+ },
+
+ /**
+ * Stress test a specific scenario
+ */
+ async stressScenario(scenario) {
+ const scenarios = {
+ "high-frequency": async () => {
+ console.log("π₯ High Frequency Scenario: Sending requests every 10ms for 5 seconds");
+ const promises = [];
+ const startTime = Date.now();
+
+ while (Date.now() - startTime < 5000) {
+ promises.push(
+ window.MSP.promise(101, null).catch((err) => {
+ console.error("MSP request failed in sustained-load scenario:", err);
+ return { error: err.message || "Unknown error" };
+ }),
+ );
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+
+ const results = await Promise.allSettled(promises);
+ return {
+ totalRequests: promises.length,
+ successful: results.filter((r) => r.status === "fulfilled").length,
+ duration: Date.now() - startTime,
+ };
+ },
+
+ "queue-overflow": async () => {
+ console.log("π₯ Queue Overflow Scenario: Flooding queue beyond capacity");
+ const promises = [];
+
+ // Send more requests than queue can handle
+ for (let i = 0; i < 100; i++) {
+ promises.push(window.MSP.promise(101, null).catch((err) => ({ error: err.message })));
+ }
+
+ const results = await Promise.allSettled(promises);
+ const successful = results.filter((r) => r.status === "fulfilled" && !r.value.error).length;
+
+ return {
+ requestsSent: 100,
+ successful,
+ rejected: 100 - successful,
+ };
+ },
+
+ "mixed-load": async () => {
+ console.log("π Mixed Load Scenario: Various request types and sizes");
+ const codes = [100, 101, 102, 104, 108, 110, 111, 112];
+ const promises = [];
+
+ for (let i = 0; i < 30; i++) {
+ const code = codes[i % codes.length];
+ const data = i % 4 === 0 ? new Uint8Array([i, i + 1, i + 2]) : null;
+ promises.push(
+ window.MSP.promise(code, data).catch((err) => {
+ console.error(`MSP request failed in mixed-load scenario (code: ${code}):`, err);
+ return { error: err.message || "Unknown error" };
+ }),
+ );
+ }
+
+ const startTime = Date.now();
+ const results = await Promise.allSettled(promises);
+ const duration = Date.now() - startTime;
+
+ return {
+ totalRequests: 30,
+ successful: results.filter((r) => r.status === "fulfilled").length,
+ duration,
+ avgResponseTime: duration / 30,
+ };
+ },
+ };
+
+ const scenarioFn = scenarios[scenario];
+ if (!scenarioFn) {
+ console.error(`β Unknown scenario: ${scenario}`);
+ console.log("Available scenarios:", Object.keys(scenarios));
+ return;
+ }
+
+ mspQueueMonitor.startMonitoring(100);
+
+ try {
+ const result = await scenarioFn();
+ const status = mspQueueMonitor.getStatus();
+
+ console.log("π Scenario Results:");
+ console.table(result);
+ console.log("π Final MSP Status:");
+ console.table({
+ "Queue Size": status.currentQueueSize,
+ "Total Requests": status.metrics.totalRequests,
+ "Success Rate": `${Math.round((status.metrics.successRate || 0) * 100)}%`,
+ "Avg Response": `${Math.round(status.metrics.avgResponseTime || 0)}ms`,
+ });
+
+ return { scenario: result, mspStatus: status };
+ } catch (error) {
+ console.error("π₯ Scenario failed:", error);
+ return { error: error.message };
+ } finally {
+ mspQueueMonitor.stopMonitoring();
+ }
+ },
+
+ /**
+ * List available commands
+ */
+ help() {
+ console.log(`
+π§ MSP Test Runner Commands:
+
+Basic Monitoring:
+ MSPTestRunner.startQuickMonitor() - Start monitoring with console output
+ MSPTestRunner.stopMonitor() - Stop monitoring
+ MSPTestRunner.getStatus() - Get current status
+ MSPTestRunner.analyzeQueue() - Analyze current queue
+
+Testing:
+ MSPTestRunner.runTest('test-name') - Run specific test
+ MSPTestRunner.runFullSuite() - Run full stress test suite
+ MSPTestRunner.quickHealthCheck() - Quick health check
+
+Stress Scenarios:
+ MSPTestRunner.stressScenario('high-frequency') - High frequency requests
+ MSPTestRunner.stressScenario('queue-overflow') - Queue overflow test
+ MSPTestRunner.stressScenario('mixed-load') - Mixed request types
+
+Visual Tools:
+ MSPTestRunner.showDashboard() - Show visual dashboard
+ MSPTestRunner.generateReport() - Generate and download report
+
+Available Test Names:
+ 'queue-flooding', 'rapid-fire', 'duplicates', 'timeout-recovery',
+ 'memory-leaks', 'concurrent-mixed', 'queue-overflow',
+ 'connection-disruption', 'performance-load'
+
+Keyboard Shortcuts:
+ Ctrl+Shift+M - Toggle debug dashboard
+ `);
+ },
+};
+
+// Make globally available
+window.MSPTestRunner = MSPTestRunner;
+
+console.log("π§ MSP Test Runner loaded! Type MSPTestRunner.help() for commands.");
diff --git a/src/js/utils/checkBrowserCompatibility.js b/src/js/utils/checkBrowserCompatibility.js
index a1a34411..da3e0fdc 100644
--- a/src/js/utils/checkBrowserCompatibility.js
+++ b/src/js/utils/checkBrowserCompatibility.js
@@ -68,7 +68,11 @@ export function checkBrowserCompatibility() {
const isNative = Capacitor.isNativePlatform();
- const compatible = isNative || (isChromium && (isWebSerial || isWebBluetooth || isWebUSB));
+ // Check if running in a test environment
+ const isTestEnvironment =
+ typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined);
+
+ const compatible = isTestEnvironment || isNative || (isChromium && (isWebSerial || isWebBluetooth || isWebUSB));
console.log("User Agent: ", navigator.userAgentData);
console.log("Native: ", isNative);
diff --git a/test/js/msp/MSPHelper.test.js b/test/js/msp/MSPHelper.test.js
index ce439a47..01579bb4 100644
--- a/test/js/msp/MSPHelper.test.js
+++ b/test/js/msp/MSPHelper.test.js
@@ -3,7 +3,7 @@ import MspHelper from "../../../src/js/msp/MSPHelper";
import MSPCodes from "../../../src/js/msp/MSPCodes";
import "../../../src/js/injected_methods";
import FC from "../../../src/js/fc";
-import { API_VERSION_1_46 } from "../../../src/js/data_storage";
+import { API_VERSION_1_47 } from "../../../src/js/data_storage";
describe("MspHelper", () => {
const mspHelper = new MspHelper();
@@ -79,7 +79,7 @@ describe("MspHelper", () => {
expect(FC.MOTOR_DATA.slice(motorCount, 8)).toContain(undefined);
});
it("handles MSP_BOARD_INFO correctly for API version", () => {
- FC.CONFIG.apiVersion = API_VERSION_1_46;
+ FC.CONFIG.apiVersion = API_VERSION_1_47;
let infoBuffer = [];
const boardIdentifier = appendStringToArray(infoBuffer, generateRandomString(4)); // set board-identifier