1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-23 16:25:22 +03:00
This commit is contained in:
Mark Haslinghuis 2025-07-15 18:47:14 +02:00 committed by GitHub
commit 20b8015e86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 3522 additions and 31 deletions

View file

@ -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", () => {

View file

@ -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) {
if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) {
callback_sent();
}
}
});
}
return true;
},
/**
* resolves: {command: code, data: data, length: message_length}
*/

View file

@ -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.

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,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;
},
};

View file

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

View file

@ -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.");

View file

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

View file

@ -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