1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-26 01:35:28 +03:00

Add MSP debugging tools

This commit is contained in:
Mark Haslinghuis 2025-06-12 18:14:55 +02:00
parent 1a30f95176
commit 07ca99ad0d
11 changed files with 3051 additions and 92 deletions

View file

@ -19,6 +19,19 @@ import { updateTabList } from "./utils/updateTabList.js";
import * as THREE from "three"; import * as THREE from "three";
import NotificationManager from "./utils/notifications.js"; import NotificationManager from "./utils/notifications.js";
// Load MSP debug tools in development environment
if (import.meta.env.DEV || window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
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") { if (typeof String.prototype.replaceAll === "undefined") {
String.prototype.replaceAll = function (match, replace) { String.prototype.replaceAll = function (match, replace) {
return this.replace(new RegExp(match, "g"), () => replace); return this.replace(new RegExp(match, "g"), () => replace);

View file

@ -1,3 +1,4 @@
import GUI from "./gui.js";
import CONFIGURATOR from "./data_storage.js"; import CONFIGURATOR from "./data_storage.js";
import { serial } from "./serial.js"; import { serial } from "./serial.js";
@ -50,8 +51,6 @@ const MSP = {
message_buffer: null, message_buffer: null,
message_buffer_uint8_view: null, message_buffer_uint8_view: null,
message_checksum: 0, message_checksum: 0,
messageIsJumboFrame: false,
crcError: false,
callbacks: [], callbacks: [],
packet_error: 0, packet_error: 0,
@ -66,9 +65,6 @@ const MSP = {
cli_output: [], cli_output: [],
cli_callback: null, cli_callback: null,
// Simplified retry configuration
MAX_RETRIES: 10,
MAX_QUEUE_SIZE: 50,
TIMEOUT: 1000, TIMEOUT: 1000,
read(readInfo) { read(readInfo) {
@ -373,21 +369,6 @@ const MSP = {
serial.send(bufferOut); serial.send(bufferOut);
}, },
// Helper function to create a unique key for request identification
_createRequestKey(code, data) {
if (!data || data.length === 0) {
return `${code}:empty`;
}
// Create a simple hash of the data
let hash = 0;
for (const byte of data) {
hash = ((hash << 5) - hash + byte) & 0xffffffff;
}
return `${code}:${hash}`;
},
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) { send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
if (code === undefined || !serial.connected || CONFIGURATOR.virtualMode) { if (code === undefined || !serial.connected || CONFIGURATOR.virtualMode) {
if (callback_msp) { if (callback_msp) {
@ -397,29 +378,47 @@ const MSP = {
} }
// Create unique key combining code and data // Create unique key combining code and data
const requestKey = this._createRequestKey(code, data); const requestExists = this.callbacks.some((instance) => instance.code === code);
const isDuplicateRequest = this.callbacks.some((instance) => instance.requestKey === requestKey);
const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data); const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
const requestObj = { const obj = {
code, code,
requestKey,
requestBuffer: bufferOut, requestBuffer: bufferOut,
callback: callback_msp, callback: callback_msp,
callbackOnError: doCallbackOnError, callbackOnError: doCallbackOnError,
start: performance.now(), start: performance.now(),
attempts: 0,
}; };
// Track only the first outstanding request for a given key // Track only the first outstanding request for a given key
if (!isDuplicateRequest) { if (!requestExists) {
this._setupTimeout(requestObj, bufferOut); obj.timer = setTimeout(() => {
this.callbacks.push(requestObj); console.warn(
`MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${
this.timeout
} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
);
serial.send(bufferOut, (_sendInfo) => {
obj.stop = performance.now();
const executionTime = Math.round(obj.stop - obj.start);
// 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} TIMEOUT: ${
this.timeout
} EXECUTION TIME: ${executionTime}ms`,
);
}
clearTimeout(obj.timer); // prevent leaks
});
}, this.TIMEOUT);
} }
// Send message if it has data or is a new request this.callbacks.push(obj);
if (data || !isDuplicateRequest) {
// always send messages with data payload (even when there is a message already in the queue)
if (data || !requestExists) {
serial.send(bufferOut, (sendInfo) => { serial.send(bufferOut, (sendInfo) => {
if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) { if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) {
callback_sent(); callback_sent();
@ -429,65 +428,6 @@ const MSP = {
return true; return true;
}, },
_setupTimeout(requestObj, bufferOut) {
requestObj.timer = setTimeout(() => {
this._handleTimeout(requestObj, bufferOut);
}, this.TIMEOUT);
},
_handleTimeout(requestObj, bufferOut) {
// Increment retry attempts
requestObj.attempts++;
console.warn(
`MSP: data request timed-out: ${requestObj.code} ` +
`QUEUE: ${this.callbacks.length}/${this.MAX_QUEUE_SIZE} ` +
`(${this.callbacks.map((e) => e.code)}) ` +
`ATTEMPTS: ${requestObj.attempts}/${this.MAX_RETRIES}`,
);
// Check if max retries exceeded OR queue is too large
if (requestObj.attempts >= this.MAX_RETRIES || this.callbacks.length > this.MAX_QUEUE_SIZE) {
const reason =
requestObj.attempts >= this.MAX_RETRIES ? `max retries (${this.MAX_RETRIES})` : `queue overflow`;
console.error(`MSP: Request ${requestObj.code} exceeded ${reason}, giving up`);
this._removeRequestFromCallbacks(requestObj);
if (requestObj.callbackOnError && requestObj.callback) {
requestObj.callback();
}
return;
}
serial.send(bufferOut, (sendInfo) => {
if (sendInfo.bytesSent === bufferOut.byteLength) {
requestObj.timer = setTimeout(() => {
this._handleTimeout(requestObj, bufferOut);
}, this.TIMEOUT);
} else {
console.error(`MSP: Failed to send retry for request ${requestObj.code}`);
this._removeRequestFromCallbacks(requestObj);
if (requestObj.callbackOnError && requestObj.callback) {
requestObj.callback();
}
}
});
},
_removeRequestFromCallbacks(requestObj) {
// Clear the timer if it exists
if (requestObj.timer) {
clearTimeout(requestObj.timer);
}
const index = this.callbacks.indexOf(requestObj);
if (index > -1) {
this.callbacks.splice(index, 1);
}
},
/** /**
* resolves: {command: code, data: data, length: message_length} * resolves: {command: code, data: data, length: message_length}
*/ */

View file

@ -0,0 +1,279 @@
# 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
MSPTestRunner.startQuickMonitor();
```
**Show visual dashboard:**
```javascript
MSPTestRunner.showDashboard();
// Or press Ctrl+Shift+M
```
**Quick health check:**
```javascript
MSPTestRunner.quickHealthCheck();
```
**Run stress tests:**
```javascript
// Run specific test
MSPTestRunner.runTest('queue-flooding');
// Run full test suite
MSPTestRunner.runFullSuite();
```
## Available Commands
### Monitoring Commands
| Command | Description |
|---------|-------------|
| `MSPTestRunner.startQuickMonitor()` | Start monitoring with console output |
| `MSPTestRunner.stopMonitor()` | Stop monitoring |
| `MSPTestRunner.getStatus()` | Get current MSP status |
| `MSPTestRunner.analyzeQueue()` | Analyze current queue contents |
### Testing Commands
| Command | Description |
|---------|-------------|
| `MSPTestRunner.runTest('test-name')` | Run specific stress test |
| `MSPTestRunner.runFullSuite()` | Run complete test suite |
| `MSPTestRunner.quickHealthCheck()` | Quick health validation |
### Stress Scenarios
| Command | Description |
|---------|-------------|
| `MSPTestRunner.stressScenario('high-frequency')` | High frequency request test |
| `MSPTestRunner.stressScenario('queue-overflow')` | Queue overflow handling test |
| `MSPTestRunner.stressScenario('mixed-load')` | Mixed request types test |
### Visual Tools
| Command | Description |
|---------|-------------|
| `MSPTestRunner.showDashboard()` | Show visual debug dashboard |
| `MSPTestRunner.generateReport()` | 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
1. **Queue Flooding** - Tests queue limits with many simultaneous requests
2. **Rapid Fire Requests** - Tests high-frequency request handling
3. **Duplicate Request Handling** - Validates duplicate request management
4. **Timeout Recovery** - Tests timeout and retry mechanisms
5. **Memory Leak Detection** - Checks for proper cleanup of completed requests
6. **Concurrent Mixed Requests** - Tests various request types simultaneously
7. **Queue Overflow Handling** - Tests behavior when queue reaches capacity
8. **Connection Disruption** - Simulates connection issues
9. **Performance Under Load** - Tests sustained load performance
## 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
MSPTestRunner.startQuickMonitor();
// Run quick health check after changes
MSPTestRunner.quickHealthCheck();
// Test specific functionality
MSPTestRunner.runTest('timeout-recovery');
```
### Performance Analysis
```javascript
// Show dashboard for visual monitoring
MSPTestRunner.showDashboard();
// Run performance stress test
MSPTestRunner.stressScenario('high-frequency');
// Generate detailed report
MSPTestRunner.generateReport();
```
### Issue Debugging
```javascript
// Analyze current queue state
MSPTestRunner.analyzeQueue();
// Check for memory leaks
MSPTestRunner.runTest('memory-leaks');
// Run full diagnostic
MSPTestRunner.runFullSuite();
```
## 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
## 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.

File diff suppressed because it is too large Load diff

View file

@ -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
});
}

View file

@ -0,0 +1,549 @@
/**
* 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: 40, // 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() {
// 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);
};
}
}
/**
* 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;
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;
const maxQueueSize = this.msp.MAX_QUEUE_SIZE || 50;
// 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 || 50,
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
*/
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(),
};
this.alerts = {
queueFull: false,
highTimeout: false,
slowResponses: false,
memoryLeak: false,
};
}
/**
* 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 queueRatio = this.currentQueueSize / (this.msp.MAX_QUEUE_SIZE || 50);
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: 40, // 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;
}
this.listeners = [];
}
}
// Export singleton instance for easy use
export const mspQueueMonitor = new MSPQueueMonitor(window.MSP);

View file

@ -0,0 +1,580 @@
/**
* 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 = new MSPQueueMonitor(mspInstance);
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: 116,
MSP_MISC: 114,
MSP_MOTOR_PINS: 115,
};
}
/**
* Run a comprehensive stress test suite
*/
async runStressTestSuite() {
console.log("🚀 Starting MSP Stress Test Suite");
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 = [];
for (const testDef of tests) {
try {
console.log(`\n📋 Running: ${testDef.name}`);
this.currentTest = testDef.name;
this.monitor.resetMetrics();
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.monitor.stopMonitoring();
this.testResults = results;
const report = this.generateTestReport(results);
console.log("\n📊 Stress Test Suite Complete");
console.log(report.summary);
return report;
}
/**
* Test 1: Queue Flooding - Send many requests quickly to test queue limits
*/
async testQueueFlooding() {
const requestCount = 60; // 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.error).length;
const failed = results.length - successful;
return {
requestsSent: requestCount,
successful,
failed,
successRate: successful / requestCount,
peakQueueSize: this.monitor.metrics.queuePeakSize,
};
}
/**
* Test 2: Rapid Fire Requests - Send requests in rapid succession
*/
async testRapidFireRequests() {
const requestCount = 20;
const interval = 10; // 10ms between requests
console.log(` Sending ${requestCount} requests with ${interval}ms intervals...`);
const results = [];
const startTime = performance.now();
for (let i = 0; i < requestCount; i++) {
const code = this.testCodes.MSP_STATUS;
const requestStart = performance.now();
try {
const result = await this.msp.promise(code, null);
results.push({
success: true,
responseTime: performance.now() - requestStart,
});
} catch (error) {
results.push({
success: false,
error: error.message,
responseTime: performance.now() - requestStart,
});
}
if (i < requestCount - 1) {
await this.wait(interval);
}
}
const totalTime = performance.now() - startTime;
const successful = results.filter((r) => r.success).length;
const avgResponseTime = results.reduce((sum, r) => sum + r.responseTime, 0) / results.length;
return {
requestCount,
successful,
failed: requestCount - successful,
totalTime,
avgResponseTime,
throughput: requestCount / (totalTime / 1000), // requests per second
};
}
/**
* 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.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.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 || 50;
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 && 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 && 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 = 50; // Request every 50ms
const startTime = performance.now();
const results = [];
let requestCount = 0;
while (performance.now() - startTime < duration) {
const requestStart = performance.now();
requestCount++;
try {
await this.msp.promise(this.testCodes.MSP_STATUS, null);
results.push({
success: true,
responseTime: performance.now() - requestStart,
});
} catch (error) {
results.push({
success: false,
responseTime: performance.now() - requestStart,
error: error.message,
});
}
await this.wait(requestInterval);
}
const successful = results.filter((r) => r.success).length;
const avgResponseTime = results.reduce((sum, r) => sum + r.responseTime, 0) / results.length;
const maxResponseTime = Math.max(...results.map((r) => r.responseTime));
return {
duration,
requestCount,
successful,
failed: requestCount - successful,
successRate: successful / requestCount,
avgResponseTime,
maxResponseTime,
throughput: requestCount / (duration / 1000),
};
}
/**
* 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 && perfTest.result && 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 && memTest.result && 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 && overflowTest.result && !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.resetMetrics();
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() {
this.monitor.destroy();
}
}
// Export singleton for easy use
export const mspStressTest = new MSPStressTest(window.MSP);

View file

@ -0,0 +1,405 @@
/**
* 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 = {
/**
* Start quick monitoring with console output
*/
startQuickMonitor() {
console.log("🚀 Starting MSP Quick Monitor...");
mspQueueMonitor.addListener((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.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();
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...");
// 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(() => {}));
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(() => {}));
}
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.");

View file

@ -68,7 +68,11 @@ export function checkBrowserCompatibility() {
const isNative = Capacitor.isNativePlatform(); 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("User Agent: ", navigator.userAgentData);
console.log("Native: ", isNative); console.log("Native: ", isNative);

106
src/test_alerts.js Normal file
View file

@ -0,0 +1,106 @@
/**
* Alert System Test Script
* Run this in the browser console to test the MSP debug alert system
*/
console.log("🧪 Starting MSP Alert System Test...");
// Test function to check alert system
async function testAlertSystem() {
console.log("1. Checking if MSPDebug is available...");
if (typeof window.MSPDebug === "undefined") {
console.error("❌ MSPDebug not found! Debug tools may not be loaded.");
return;
}
console.log("✅ MSPDebug is available");
console.log("2. Checking dashboard...");
const dashboard = window.MSPDebug.dashboard;
if (!dashboard) {
console.error("❌ Dashboard not found!");
return;
}
console.log("✅ Dashboard is available");
console.log("3. Checking monitor...");
const monitor = window.MSPDebug.monitor;
if (!monitor) {
console.error("❌ Monitor not found!");
return;
}
console.log("✅ Monitor is available");
console.log("4. Showing dashboard...");
dashboard.show();
console.log("5. Starting monitoring...");
monitor.startMonitoring(500);
console.log("6. Checking alert container exists...");
const alertContainer = document.getElementById("alerts-container");
if (!alertContainer) {
console.error("❌ Alert container not found in DOM!");
return;
}
console.log("✅ Alert container found:", alertContainer);
console.log("7. Getting current status...");
const status = monitor.getStatus();
console.log("Current status:", status);
console.log("Current alerts:", status.alerts);
console.log("8. Testing alert display directly...");
dashboard.updateAlerts({
queueFull: true,
highTimeout: false,
slowResponses: true,
memoryLeak: false,
});
console.log("9. Checking alert container content after manual update...");
console.log("Alert container HTML:", alertContainer.innerHTML);
console.log("10. Triggering test alerts...");
const testAlerts = monitor.triggerTestAlerts();
console.log("Test alerts triggered:", testAlerts);
console.log("11. Waiting 2 seconds and checking again...");
setTimeout(() => {
const newStatus = monitor.getStatus();
console.log("Status after test alerts:", newStatus);
console.log("Alerts after test:", newStatus.alerts);
console.log("Alert container HTML after test:", alertContainer.innerHTML);
// Test the complete update flow
console.log("12. Testing complete update flow...");
dashboard.updateDisplay(newStatus);
console.log("Alert container HTML after updateDisplay:", alertContainer.innerHTML);
console.log("🏁 Alert system test complete!");
}, 2000);
}
// Run the test
testAlertSystem();
// Also provide manual test functions
window.testAlertSystem = testAlertSystem;
window.checkAlerts = () => {
const container = document.getElementById("alerts-container");
console.log("Alert container:", container);
console.log("Alert container HTML:", container?.innerHTML);
const status = window.MSPDebug?.monitor?.getStatus();
console.log("Current alerts:", status?.alerts);
};
window.manualTestAlert = () => {
console.log("Testing alert display manually...");
const dashboard = window.MSPDebug.dashboard;
dashboard.updateAlerts({
queueFull: true,
highTimeout: true,
slowResponses: false,
memoryLeak: true,
});
console.log("Manual test alerts set");
};

View file

@ -3,7 +3,7 @@ import MspHelper from "../../../src/js/msp/MSPHelper";
import MSPCodes from "../../../src/js/msp/MSPCodes"; import MSPCodes from "../../../src/js/msp/MSPCodes";
import "../../../src/js/injected_methods"; import "../../../src/js/injected_methods";
import FC from "../../../src/js/fc"; 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", () => { describe("MspHelper", () => {
const mspHelper = new MspHelper(); const mspHelper = new MspHelper();
@ -79,7 +79,7 @@ describe("MspHelper", () => {
expect(FC.MOTOR_DATA.slice(motorCount, 8)).toContain(undefined); expect(FC.MOTOR_DATA.slice(motorCount, 8)).toContain(undefined);
}); });
it("handles MSP_BOARD_INFO correctly for API version", () => { 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 = []; let infoBuffer = [];
const boardIdentifier = appendStringToArray(infoBuffer, generateRandomString(4)); // set board-identifier const boardIdentifier = appendStringToArray(infoBuffer, generateRandomString(4)); // set board-identifier