mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-23 16:25:22 +03:00
Merge cdbdda0fac
into be45ddf05e
This commit is contained in:
commit
20b8015e86
10 changed files with 3522 additions and 31 deletions
|
@ -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", () => {
|
||||
|
|
|
@ -51,16 +51,13 @@ const MSP = {
|
|||
message_buffer: null,
|
||||
message_buffer_uint8_view: null,
|
||||
message_checksum: 0,
|
||||
messageIsJumboFrame: false,
|
||||
crcError: false,
|
||||
|
||||
callbacks: [],
|
||||
packet_error: 0,
|
||||
unsupported: 0,
|
||||
|
||||
MIN_TIMEOUT: 200,
|
||||
MAX_TIMEOUT: 2000,
|
||||
timeout: 200,
|
||||
TIMEOUT: 1000,
|
||||
|
||||
last_received_timestamp: null,
|
||||
listeners: [],
|
||||
|
@ -374,28 +371,19 @@ const MSP = {
|
|||
serial.send(bufferOut);
|
||||
},
|
||||
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
|
||||
const connected = serial.connected;
|
||||
|
||||
if (code === undefined || !connected || CONFIGURATOR.virtualMode) {
|
||||
if (code === undefined || !serial.connected || CONFIGURATOR.virtualMode) {
|
||||
if (callback_msp) {
|
||||
callback_msp();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let requestExists = false;
|
||||
for (const instance of this.callbacks) {
|
||||
if (instance.code === code) {
|
||||
requestExists = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
const requestExists = this.callbacks.some((instance) => instance.code === code);
|
||||
|
||||
const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
|
||||
|
||||
const obj = {
|
||||
code: code,
|
||||
code,
|
||||
requestBuffer: bufferOut,
|
||||
callback: callback_msp,
|
||||
callbackOnError: doCallbackOnError,
|
||||
|
@ -412,31 +400,31 @@ const MSP = {
|
|||
serial.send(bufferOut, (_sendInfo) => {
|
||||
obj.stop = performance.now();
|
||||
const executionTime = Math.round(obj.stop - obj.start);
|
||||
this.timeout = Math.max(this.MIN_TIMEOUT, Math.min(executionTime, this.MAX_TIMEOUT));
|
||||
// We should probably give up connection if the request takes too long ?
|
||||
if (executionTime > 5000) {
|
||||
console.warn(
|
||||
`MSP: data request took too long: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} EXECUTION TIME: ${executionTime}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
clearTimeout(obj.timer); // prevent leaks
|
||||
});
|
||||
}, this.timeout);
|
||||
}, this.TIMEOUT);
|
||||
}
|
||||
|
||||
this.callbacks.push(obj);
|
||||
|
||||
// always send messages with data payload (even when there is a message already in the queue)
|
||||
if (data || !requestExists) {
|
||||
if (this.timeout > this.MIN_TIMEOUT) {
|
||||
this.timeout--;
|
||||
}
|
||||
|
||||
serial.send(bufferOut, (sendInfo) => {
|
||||
if (sendInfo.bytesSent === bufferOut.byteLength) {
|
||||
if (callback_sent) {
|
||||
callback_sent();
|
||||
}
|
||||
if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) {
|
||||
callback_sent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* resolves: {command: code, data: data, length: message_length}
|
||||
*/
|
||||
|
|
359
src/js/msp/MSP_DEBUG_README.md
Normal file
359
src/js/msp/MSP_DEBUG_README.md
Normal 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.
|
1192
src/js/msp/msp_debug_dashboard.js
Normal file
1192
src/js/msp/msp_debug_dashboard.js
Normal file
File diff suppressed because it is too large
Load diff
52
src/js/msp/msp_debug_tools.js
Normal file
52
src/js/msp/msp_debug_tools.js
Normal 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
|
||||
});
|
||||
}
|
692
src/js/msp/msp_queue_monitor.js
Normal file
692
src/js/msp/msp_queue_monitor.js
Normal 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;
|
||||
},
|
||||
};
|
756
src/js/msp/msp_stress_test.js
Normal file
756
src/js/msp/msp_stress_test.js
Normal 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;
|
||||
},
|
||||
};
|
437
src/js/msp/msp_test_runner.js
Normal file
437
src/js/msp/msp_test_runner.js
Normal 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.");
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue