mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-26 01:35:28 +03:00
Add MSP debugging tools
This commit is contained in:
parent
1a30f95176
commit
07ca99ad0d
11 changed files with 3051 additions and 92 deletions
|
@ -19,6 +19,19 @@ import { updateTabList } from "./utils/updateTabList.js";
|
|||
import * as THREE from "three";
|
||||
import NotificationManager from "./utils/notifications.js";
|
||||
|
||||
// Load MSP debug tools in development environment
|
||||
if (import.meta.env.DEV || window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
|
||||
import("./msp/msp_debug_tools.js")
|
||||
.then(() => {
|
||||
console.log("🔧 MSP Debug Tools loaded for development environment");
|
||||
console.log("• Press Ctrl+Shift+M to toggle debug dashboard");
|
||||
console.log("• Use MSPTestRunner.help() for all commands");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("Failed to load MSP debug tools:", err);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof String.prototype.replaceAll === "undefined") {
|
||||
String.prototype.replaceAll = function (match, replace) {
|
||||
return this.replace(new RegExp(match, "g"), () => replace);
|
||||
|
|
118
src/js/msp.js
118
src/js/msp.js
|
@ -1,3 +1,4 @@
|
|||
import GUI from "./gui.js";
|
||||
import CONFIGURATOR from "./data_storage.js";
|
||||
import { serial } from "./serial.js";
|
||||
|
||||
|
@ -50,8 +51,6 @@ const MSP = {
|
|||
message_buffer: null,
|
||||
message_buffer_uint8_view: null,
|
||||
message_checksum: 0,
|
||||
messageIsJumboFrame: false,
|
||||
crcError: false,
|
||||
|
||||
callbacks: [],
|
||||
packet_error: 0,
|
||||
|
@ -66,9 +65,6 @@ const MSP = {
|
|||
cli_output: [],
|
||||
cli_callback: null,
|
||||
|
||||
// Simplified retry configuration
|
||||
MAX_RETRIES: 10,
|
||||
MAX_QUEUE_SIZE: 50,
|
||||
TIMEOUT: 1000,
|
||||
|
||||
read(readInfo) {
|
||||
|
@ -373,21 +369,6 @@ const MSP = {
|
|||
|
||||
serial.send(bufferOut);
|
||||
},
|
||||
// Helper function to create a unique key for request identification
|
||||
_createRequestKey(code, data) {
|
||||
if (!data || data.length === 0) {
|
||||
return `${code}:empty`;
|
||||
}
|
||||
|
||||
// Create a simple hash of the data
|
||||
let hash = 0;
|
||||
for (const byte of data) {
|
||||
hash = ((hash << 5) - hash + byte) & 0xffffffff;
|
||||
}
|
||||
|
||||
return `${code}:${hash}`;
|
||||
},
|
||||
|
||||
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
|
||||
if (code === undefined || !serial.connected || CONFIGURATOR.virtualMode) {
|
||||
if (callback_msp) {
|
||||
|
@ -397,29 +378,47 @@ const MSP = {
|
|||
}
|
||||
|
||||
// Create unique key combining code and data
|
||||
const requestKey = this._createRequestKey(code, data);
|
||||
const isDuplicateRequest = this.callbacks.some((instance) => instance.requestKey === requestKey);
|
||||
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 requestObj = {
|
||||
const obj = {
|
||||
code,
|
||||
requestKey,
|
||||
requestBuffer: bufferOut,
|
||||
callback: callback_msp,
|
||||
callbackOnError: doCallbackOnError,
|
||||
start: performance.now(),
|
||||
attempts: 0,
|
||||
};
|
||||
|
||||
// Track only the first outstanding request for a given key
|
||||
if (!isDuplicateRequest) {
|
||||
this._setupTimeout(requestObj, bufferOut);
|
||||
this.callbacks.push(requestObj);
|
||||
if (!requestExists) {
|
||||
obj.timer = setTimeout(() => {
|
||||
console.warn(
|
||||
`MSP: data request timed-out: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${
|
||||
this.timeout
|
||||
} QUEUE: ${this.callbacks.length} (${this.callbacks.map((e) => e.code)})`,
|
||||
);
|
||||
serial.send(bufferOut, (_sendInfo) => {
|
||||
obj.stop = performance.now();
|
||||
const executionTime = Math.round(obj.stop - obj.start);
|
||||
// We should probably give up connection if the request takes too long ?
|
||||
if (executionTime > 5000) {
|
||||
console.warn(
|
||||
`MSP: data request took too long: ${code} ID: ${serial.connectionId} TAB: ${GUI.active_tab} TIMEOUT: ${
|
||||
this.timeout
|
||||
} EXECUTION TIME: ${executionTime}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
// Send message if it has data or is a new request
|
||||
if (data || !isDuplicateRequest) {
|
||||
clearTimeout(obj.timer); // prevent leaks
|
||||
});
|
||||
}, 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) {
|
||||
serial.send(bufferOut, (sendInfo) => {
|
||||
if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) {
|
||||
callback_sent();
|
||||
|
@ -429,65 +428,6 @@ const MSP = {
|
|||
|
||||
return true;
|
||||
},
|
||||
|
||||
_setupTimeout(requestObj, bufferOut) {
|
||||
requestObj.timer = setTimeout(() => {
|
||||
this._handleTimeout(requestObj, bufferOut);
|
||||
}, this.TIMEOUT);
|
||||
},
|
||||
|
||||
_handleTimeout(requestObj, bufferOut) {
|
||||
// Increment retry attempts
|
||||
requestObj.attempts++;
|
||||
|
||||
console.warn(
|
||||
`MSP: data request timed-out: ${requestObj.code} ` +
|
||||
`QUEUE: ${this.callbacks.length}/${this.MAX_QUEUE_SIZE} ` +
|
||||
`(${this.callbacks.map((e) => e.code)}) ` +
|
||||
`ATTEMPTS: ${requestObj.attempts}/${this.MAX_RETRIES}`,
|
||||
);
|
||||
|
||||
// Check if max retries exceeded OR queue is too large
|
||||
if (requestObj.attempts >= this.MAX_RETRIES || this.callbacks.length > this.MAX_QUEUE_SIZE) {
|
||||
const reason =
|
||||
requestObj.attempts >= this.MAX_RETRIES ? `max retries (${this.MAX_RETRIES})` : `queue overflow`;
|
||||
|
||||
console.error(`MSP: Request ${requestObj.code} exceeded ${reason}, giving up`);
|
||||
this._removeRequestFromCallbacks(requestObj);
|
||||
|
||||
if (requestObj.callbackOnError && requestObj.callback) {
|
||||
requestObj.callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
serial.send(bufferOut, (sendInfo) => {
|
||||
if (sendInfo.bytesSent === bufferOut.byteLength) {
|
||||
requestObj.timer = setTimeout(() => {
|
||||
this._handleTimeout(requestObj, bufferOut);
|
||||
}, this.TIMEOUT);
|
||||
} else {
|
||||
console.error(`MSP: Failed to send retry for request ${requestObj.code}`);
|
||||
this._removeRequestFromCallbacks(requestObj);
|
||||
if (requestObj.callbackOnError && requestObj.callback) {
|
||||
requestObj.callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_removeRequestFromCallbacks(requestObj) {
|
||||
// Clear the timer if it exists
|
||||
if (requestObj.timer) {
|
||||
clearTimeout(requestObj.timer);
|
||||
}
|
||||
|
||||
const index = this.callbacks.indexOf(requestObj);
|
||||
if (index > -1) {
|
||||
this.callbacks.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* resolves: {command: code, data: data, length: message_length}
|
||||
*/
|
||||
|
|
279
src/js/msp/MSP_DEBUG_README.md
Normal file
279
src/js/msp/MSP_DEBUG_README.md
Normal file
|
@ -0,0 +1,279 @@
|
|||
# MSP Debug Tools
|
||||
|
||||
Comprehensive monitoring and stress testing tools for the MSP (MultiWii Serial Protocol) implementation in Betaflight Configurator.
|
||||
|
||||
## Features
|
||||
|
||||
🔍 **Real-time Queue Monitoring**
|
||||
- Track queue size, response times, and success rates
|
||||
- Detect memory leaks and performance bottlenecks
|
||||
- Alert system for potential issues
|
||||
|
||||
🧪 **Comprehensive Stress Testing**
|
||||
- Queue flooding tests
|
||||
- Timeout recovery validation
|
||||
- Memory leak detection
|
||||
- Performance under load testing
|
||||
|
||||
📊 **Visual Dashboard**
|
||||
- Real-time metrics display with smart updates
|
||||
- Live charts and graphs
|
||||
- Queue analysis tools
|
||||
- Test result visualization
|
||||
- **Interactive-friendly updates**: Dashboard pauses updates during user interactions
|
||||
- **Clickable test results**: Click on any test result for detailed information
|
||||
- Visual pause indicators when updates are suspended
|
||||
|
||||
⚡ **Easy-to-use API**
|
||||
- Console commands for quick testing
|
||||
- Programmable test scenarios
|
||||
- Detailed reporting and export
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Load the Debug Tools
|
||||
|
||||
Include the debug tools in your page:
|
||||
|
||||
```javascript
|
||||
import './src/js/msp_debug_tools.js';
|
||||
```
|
||||
|
||||
Or in development, load via console:
|
||||
```javascript
|
||||
import('./src/js/msp_debug_tools.js');
|
||||
```
|
||||
|
||||
### 2. Basic Usage
|
||||
|
||||
**Start monitoring:**
|
||||
```javascript
|
||||
MSPTestRunner.startQuickMonitor();
|
||||
```
|
||||
|
||||
**Show visual dashboard:**
|
||||
```javascript
|
||||
MSPTestRunner.showDashboard();
|
||||
// Or press Ctrl+Shift+M
|
||||
```
|
||||
|
||||
**Quick health check:**
|
||||
```javascript
|
||||
MSPTestRunner.quickHealthCheck();
|
||||
```
|
||||
|
||||
**Run stress tests:**
|
||||
```javascript
|
||||
// Run specific test
|
||||
MSPTestRunner.runTest('queue-flooding');
|
||||
|
||||
// Run full test suite
|
||||
MSPTestRunner.runFullSuite();
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Monitoring Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `MSPTestRunner.startQuickMonitor()` | Start monitoring with console output |
|
||||
| `MSPTestRunner.stopMonitor()` | Stop monitoring |
|
||||
| `MSPTestRunner.getStatus()` | Get current MSP status |
|
||||
| `MSPTestRunner.analyzeQueue()` | Analyze current queue contents |
|
||||
|
||||
### Testing Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `MSPTestRunner.runTest('test-name')` | Run specific stress test |
|
||||
| `MSPTestRunner.runFullSuite()` | Run complete test suite |
|
||||
| `MSPTestRunner.quickHealthCheck()` | Quick health validation |
|
||||
|
||||
### Stress Scenarios
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `MSPTestRunner.stressScenario('high-frequency')` | High frequency request test |
|
||||
| `MSPTestRunner.stressScenario('queue-overflow')` | Queue overflow handling test |
|
||||
| `MSPTestRunner.stressScenario('mixed-load')` | Mixed request types test |
|
||||
|
||||
### Visual Tools
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `MSPTestRunner.showDashboard()` | Show visual debug dashboard |
|
||||
| `MSPTestRunner.generateReport()` | Generate and download report |
|
||||
|
||||
## Dashboard Interactions
|
||||
|
||||
The visual dashboard includes smart interaction handling to ensure a smooth user experience:
|
||||
|
||||
### Automatic Update Pausing
|
||||
|
||||
- **Mouse hover**: Updates pause for 3 seconds when hovering over interactive elements
|
||||
- **Click events**: Updates pause for 5 seconds when clicking buttons or test results
|
||||
- **Focus events**: Updates pause for 10 seconds when focusing on input elements
|
||||
- **Visual indicator**: Orange "Updates Paused" indicator appears when updates are suspended
|
||||
|
||||
### Clickable Test Results
|
||||
|
||||
- Click on any test result item to see detailed information including:
|
||||
- Full error messages
|
||||
- Performance metrics
|
||||
- JSON response data
|
||||
- Test duration and status
|
||||
- Details remain stable and clickable while displayed
|
||||
- Use the "Close Details" button to dismiss and resume normal updates
|
||||
|
||||
### Interactive Elements
|
||||
|
||||
- All buttons remain stable during interactions
|
||||
- Queue analysis results are preserved during examination
|
||||
- Export functionality works without interference from updates
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
- **Ctrl+Shift+M**: Toggle dashboard visibility
|
||||
- Use console commands for programmatic control
|
||||
|
||||
## Available Tests
|
||||
|
||||
1. **Queue Flooding** - Tests queue limits with many simultaneous requests
|
||||
2. **Rapid Fire Requests** - Tests high-frequency request handling
|
||||
3. **Duplicate Request Handling** - Validates duplicate request management
|
||||
4. **Timeout Recovery** - Tests timeout and retry mechanisms
|
||||
5. **Memory Leak Detection** - Checks for proper cleanup of completed requests
|
||||
6. **Concurrent Mixed Requests** - Tests various request types simultaneously
|
||||
7. **Queue Overflow Handling** - Tests behavior when queue reaches capacity
|
||||
8. **Connection Disruption** - Simulates connection issues
|
||||
9. **Performance Under Load** - Tests sustained load performance
|
||||
|
||||
## Monitoring Metrics
|
||||
|
||||
The tools track various metrics:
|
||||
|
||||
- **Queue Size**: Current number of pending requests
|
||||
- **Response Times**: Average, minimum, and maximum response times
|
||||
- **Success Rate**: Percentage of successful requests
|
||||
- **Timeout Rate**: Percentage of requests that timeout
|
||||
- **Request Distribution**: Breakdown by MSP command codes
|
||||
- **Error Tracking**: Categorized error types and frequencies
|
||||
|
||||
## Alert System
|
||||
|
||||
The monitoring system provides alerts for:
|
||||
|
||||
- 🚨 **Queue Full**: Queue approaching capacity
|
||||
- ⏱️ **High Timeout Rate**: Excessive request timeouts
|
||||
- 🐌 **Slow Responses**: Average response time too high
|
||||
- 💾 **Memory Leak**: Callbacks not being cleaned up properly
|
||||
|
||||
## Dashboard Features
|
||||
|
||||
The visual dashboard provides:
|
||||
|
||||
- **Real-time Status**: Current queue state and metrics
|
||||
- **Live Charts**: Queue size and response time trends
|
||||
- **Queue Analysis**: Detailed breakdown of pending requests
|
||||
- **Alert Display**: Active alerts and warnings
|
||||
- **Test Integration**: Run tests directly from the UI
|
||||
- **Export Tools**: Generate and download reports
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
- `Ctrl+Shift+M` - Toggle debug dashboard
|
||||
- Dashboard is draggable and resizable
|
||||
|
||||
## Example Usage Scenarios
|
||||
|
||||
### Development Testing
|
||||
```javascript
|
||||
// Start monitoring during development
|
||||
MSPTestRunner.startQuickMonitor();
|
||||
|
||||
// Run quick health check after changes
|
||||
MSPTestRunner.quickHealthCheck();
|
||||
|
||||
// Test specific functionality
|
||||
MSPTestRunner.runTest('timeout-recovery');
|
||||
```
|
||||
|
||||
### Performance Analysis
|
||||
```javascript
|
||||
// Show dashboard for visual monitoring
|
||||
MSPTestRunner.showDashboard();
|
||||
|
||||
// Run performance stress test
|
||||
MSPTestRunner.stressScenario('high-frequency');
|
||||
|
||||
// Generate detailed report
|
||||
MSPTestRunner.generateReport();
|
||||
```
|
||||
|
||||
### Issue Debugging
|
||||
```javascript
|
||||
// Analyze current queue state
|
||||
MSPTestRunner.analyzeQueue();
|
||||
|
||||
// Check for memory leaks
|
||||
MSPTestRunner.runTest('memory-leaks');
|
||||
|
||||
// Run full diagnostic
|
||||
MSPTestRunner.runFullSuite();
|
||||
```
|
||||
|
||||
## Integration with Existing Code
|
||||
|
||||
The debug tools are designed to be non-intrusive:
|
||||
|
||||
- They hook into existing MSP methods without modifying core functionality
|
||||
- Monitoring can be enabled/disabled at runtime
|
||||
- No performance impact when not actively monitoring
|
||||
- Original MSP behavior is preserved
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/js/
|
||||
├── msp_queue_monitor.js # Core monitoring functionality
|
||||
├── msp_stress_test.js # Stress testing framework
|
||||
├── msp_debug_dashboard.js # Visual dashboard UI
|
||||
├── msp_test_runner.js # Console command interface
|
||||
└── msp_debug_tools.js # Integration and auto-loading
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Modern browser with ES6 module support
|
||||
- Access to the global `MSP` object
|
||||
- Console access for command-line interface
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Tools not loading:**
|
||||
- Ensure MSP object is available globally
|
||||
- Check browser console for import errors
|
||||
|
||||
**Tests failing unexpectedly:**
|
||||
- Verify serial connection is active
|
||||
- Check that flight controller is responding
|
||||
- Review console for specific error messages
|
||||
|
||||
**Dashboard not appearing:**
|
||||
- Try `MSPTestRunner.showDashboard()` from console
|
||||
- Check for CSS conflicts
|
||||
- Verify no popup blockers are interfering
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new tests or monitoring features:
|
||||
|
||||
1. Add test methods to `MSPStressTest` class
|
||||
2. Update monitor metrics in `MSPQueueMonitor`
|
||||
3. Extend dashboard UI as needed
|
||||
4. Update this documentation
|
||||
|
||||
## License
|
||||
|
||||
Same as Betaflight Configurator project.
|
1031
src/js/msp/msp_debug_dashboard.js
Normal file
1031
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
|
||||
});
|
||||
}
|
549
src/js/msp/msp_queue_monitor.js
Normal file
549
src/js/msp/msp_queue_monitor.js
Normal file
|
@ -0,0 +1,549 @@
|
|||
/**
|
||||
* MSP Queue Monitor - Real-time monitoring of MSP message queue
|
||||
* Provides insights into queue health, performance metrics, and potential issues
|
||||
*/
|
||||
|
||||
export class MSPQueueMonitor {
|
||||
constructor(mspInstance) {
|
||||
this.msp = mspInstance;
|
||||
this.isMonitoring = false;
|
||||
this.metrics = {
|
||||
totalRequests: 0,
|
||||
completedRequests: 0,
|
||||
failedRequests: 0,
|
||||
timeouts: 0,
|
||||
duplicates: 0,
|
||||
avgResponseTime: 0,
|
||||
maxResponseTime: 0,
|
||||
queuePeakSize: 0,
|
||||
requestsByCode: new Map(),
|
||||
responseTimes: [],
|
||||
errorsByType: new Map(),
|
||||
};
|
||||
|
||||
this.alerts = {
|
||||
queueFull: false,
|
||||
highTimeout: false,
|
||||
slowResponses: false,
|
||||
memoryLeak: false,
|
||||
};
|
||||
|
||||
this.thresholds = {
|
||||
maxQueueSize: 40, // Alert when queue > 80% of MAX_QUEUE_SIZE
|
||||
maxAvgResponseTime: 2000, // Alert when avg response > 2s
|
||||
maxTimeoutRate: 0.1, // Alert when timeout rate > 10%
|
||||
memoryLeakThreshold: 100, // Alert when callbacks grow beyond expected
|
||||
};
|
||||
|
||||
this.monitoringInterval = null;
|
||||
this.listeners = [];
|
||||
|
||||
// Hook into MSP methods to collect metrics
|
||||
this._hookMSPMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into MSP methods to collect real-time metrics
|
||||
*/
|
||||
_hookMSPMethods() {
|
||||
// Store original methods
|
||||
this.originalSendMessage = this.msp.send_message.bind(this.msp);
|
||||
this.originalDispatchMessage = this.msp._dispatch_message.bind(this.msp);
|
||||
this.originalRemoveRequest = this.msp._removeRequestFromCallbacks?.bind(this.msp);
|
||||
|
||||
// Override send_message to track requests
|
||||
this.msp.send_message = (...args) => {
|
||||
this._trackRequestStart(args[0], args[1]);
|
||||
return this.originalSendMessage(...args);
|
||||
};
|
||||
|
||||
// Override _dispatch_message to track responses
|
||||
this.msp._dispatch_message = (...args) => {
|
||||
this._trackResponse();
|
||||
return this.originalDispatchMessage(...args);
|
||||
};
|
||||
|
||||
// Override _removeRequestFromCallbacks to track completions
|
||||
if (this.originalRemoveRequest) {
|
||||
this.msp._removeRequestFromCallbacks = (requestObj) => {
|
||||
this._trackRequestCompletion(requestObj);
|
||||
return this.originalRemoveRequest(requestObj);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track when a request starts
|
||||
*/
|
||||
_trackRequestStart(code, data) {
|
||||
this.metrics.totalRequests++;
|
||||
|
||||
// Track requests by code
|
||||
const count = this.metrics.requestsByCode.get(code) || 0;
|
||||
this.metrics.requestsByCode.set(code, count + 1);
|
||||
|
||||
// Check for queue size peaks
|
||||
const currentQueueSize = this.msp.callbacks.length;
|
||||
if (currentQueueSize > this.metrics.queuePeakSize) {
|
||||
this.metrics.queuePeakSize = currentQueueSize;
|
||||
}
|
||||
|
||||
this._checkAlerts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Track when a response is received
|
||||
*/
|
||||
_trackResponse() {
|
||||
// This will be called for both successful and failed responses
|
||||
// More detailed tracking happens in _trackRequestCompletion
|
||||
}
|
||||
|
||||
/**
|
||||
* Track when a request is completed (success or failure)
|
||||
*/
|
||||
_trackRequestCompletion(requestObj) {
|
||||
if (!requestObj) return;
|
||||
|
||||
const responseTime = performance.now() - requestObj.start;
|
||||
this.metrics.responseTimes.push(responseTime);
|
||||
|
||||
// Keep only last 100 response times for rolling average
|
||||
if (this.metrics.responseTimes.length > 100) {
|
||||
this.metrics.responseTimes.shift();
|
||||
}
|
||||
|
||||
// Update max response time
|
||||
if (responseTime > this.metrics.maxResponseTime) {
|
||||
this.metrics.maxResponseTime = responseTime;
|
||||
}
|
||||
|
||||
// Calculate average response time
|
||||
this.metrics.avgResponseTime =
|
||||
this.metrics.responseTimes.reduce((a, b) => a + b, 0) / this.metrics.responseTimes.length;
|
||||
|
||||
// Track completion type
|
||||
if (requestObj.attempts > 1) {
|
||||
this.metrics.timeouts += requestObj.attempts - 1;
|
||||
}
|
||||
|
||||
if (requestObj.success === false) {
|
||||
this.metrics.failedRequests++;
|
||||
|
||||
// Track error types
|
||||
const errorType = requestObj.errorType || "unknown";
|
||||
const errorCount = this.metrics.errorsByType.get(errorType) || 0;
|
||||
this.metrics.errorsByType.set(errorType, errorCount + 1);
|
||||
} else {
|
||||
this.metrics.completedRequests++;
|
||||
}
|
||||
|
||||
this._checkAlerts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for alert conditions
|
||||
*/
|
||||
_checkAlerts() {
|
||||
const queueSize = this.msp.callbacks.length;
|
||||
const maxQueueSize = this.msp.MAX_QUEUE_SIZE || 50;
|
||||
|
||||
// Queue full alert
|
||||
const wasQueueFull = this.alerts.queueFull;
|
||||
this.alerts.queueFull = queueSize > this.thresholds.maxQueueSize;
|
||||
|
||||
// High timeout rate alert
|
||||
const timeoutRate = this.metrics.totalRequests > 0 ? this.metrics.timeouts / this.metrics.totalRequests : 0;
|
||||
const wasHighTimeout = this.alerts.highTimeout;
|
||||
this.alerts.highTimeout = timeoutRate > this.thresholds.maxTimeoutRate;
|
||||
|
||||
// Slow responses alert
|
||||
const wasSlowResponses = this.alerts.slowResponses;
|
||||
this.alerts.slowResponses = this.metrics.avgResponseTime > this.thresholds.maxAvgResponseTime;
|
||||
|
||||
// Memory leak detection (callbacks not being cleaned up)
|
||||
const wasMemoryLeak = this.alerts.memoryLeak;
|
||||
this.alerts.memoryLeak = queueSize > this.thresholds.memoryLeakThreshold;
|
||||
|
||||
// Debug logging for alert changes (only when alerts become active)
|
||||
if (this.alerts.queueFull !== wasQueueFull && this.alerts.queueFull) {
|
||||
console.warn(`🚨 Queue Full Alert: size ${queueSize}/${this.thresholds.maxQueueSize}`);
|
||||
}
|
||||
if (this.alerts.highTimeout !== wasHighTimeout && this.alerts.highTimeout) {
|
||||
console.warn(`⏱️ High Timeout Alert: rate ${(timeoutRate * 100).toFixed(1)}%`);
|
||||
}
|
||||
if (this.alerts.slowResponses !== wasSlowResponses && this.alerts.slowResponses) {
|
||||
console.warn(`🐌 Slow Response Alert: avg ${this.metrics.avgResponseTime}ms`);
|
||||
}
|
||||
if (this.alerts.memoryLeak !== wasMemoryLeak && this.alerts.memoryLeak) {
|
||||
console.warn(`💾 Memory Leak Alert: callbacks ${queueSize}`);
|
||||
}
|
||||
|
||||
// Notify listeners of alerts
|
||||
this._notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring
|
||||
*/
|
||||
startMonitoring(intervalMs = 1000) {
|
||||
if (this.isMonitoring) return;
|
||||
|
||||
this.isMonitoring = true;
|
||||
this.monitoringInterval = setInterval(() => {
|
||||
this._collectMetrics();
|
||||
this._notifyListeners();
|
||||
}, intervalMs);
|
||||
|
||||
console.log("MSP Queue Monitor started");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring
|
||||
*/
|
||||
stopMonitoring() {
|
||||
if (!this.isMonitoring) return;
|
||||
|
||||
this.isMonitoring = false;
|
||||
if (this.monitoringInterval) {
|
||||
clearInterval(this.monitoringInterval);
|
||||
this.monitoringInterval = null;
|
||||
}
|
||||
|
||||
console.log("MSP Queue Monitor stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect current metrics snapshot
|
||||
*/
|
||||
_collectMetrics() {
|
||||
// Update current queue size
|
||||
this.currentQueueSize = this.msp.callbacks.length;
|
||||
|
||||
// Calculate success rate
|
||||
this.metrics.successRate =
|
||||
this.metrics.totalRequests > 0 ? this.metrics.completedRequests / this.metrics.totalRequests : 0;
|
||||
|
||||
// Calculate timeout rate
|
||||
this.metrics.timeoutRate =
|
||||
this.metrics.totalRequests > 0 ? this.metrics.timeouts / this.metrics.totalRequests : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current status report
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
isMonitoring: this.isMonitoring,
|
||||
currentQueueSize: this.msp.callbacks.length,
|
||||
maxQueueSize: this.msp.MAX_QUEUE_SIZE || 50,
|
||||
metrics: { ...this.metrics },
|
||||
alerts: { ...this.alerts },
|
||||
queueContents: this.msp.callbacks.map((req) => ({
|
||||
code: req.code,
|
||||
attempts: req.attempts || 0,
|
||||
age: performance.now() - req.start,
|
||||
hasTimer: !!req.timer,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed queue analysis
|
||||
*/
|
||||
analyzeQueue() {
|
||||
const callbacks = this.msp.callbacks;
|
||||
const now = performance.now();
|
||||
|
||||
const analysis = {
|
||||
totalItems: callbacks.length,
|
||||
byCode: {},
|
||||
ageDistribution: {
|
||||
fresh: 0, // < 1s
|
||||
recent: 0, // 1-5s
|
||||
stale: 0, // 5-10s
|
||||
ancient: 0, // > 10s
|
||||
},
|
||||
retryDistribution: {
|
||||
firstAttempt: 0,
|
||||
retrying: 0,
|
||||
multipleRetries: 0,
|
||||
},
|
||||
potentialIssues: [],
|
||||
};
|
||||
|
||||
callbacks.forEach((req) => {
|
||||
// Group by code
|
||||
if (!analysis.byCode[req.code]) {
|
||||
analysis.byCode[req.code] = 0;
|
||||
}
|
||||
analysis.byCode[req.code]++;
|
||||
|
||||
// Age analysis
|
||||
const age = now - req.start;
|
||||
if (age < 1000) analysis.ageDistribution.fresh++;
|
||||
else if (age < 5000) analysis.ageDistribution.recent++;
|
||||
else if (age < 10000) analysis.ageDistribution.stale++;
|
||||
else analysis.ageDistribution.ancient++;
|
||||
|
||||
// Retry analysis
|
||||
const attempts = req.attempts || 0;
|
||||
if (attempts === 0) analysis.retryDistribution.firstAttempt++;
|
||||
else if (attempts === 1) analysis.retryDistribution.retrying++;
|
||||
else analysis.retryDistribution.multipleRetries++;
|
||||
|
||||
// Identify potential issues
|
||||
if (age > 10000) {
|
||||
analysis.potentialIssues.push(`Ancient request: code ${req.code}, age ${Math.round(age / 1000)}s`);
|
||||
}
|
||||
if (attempts > 5) {
|
||||
analysis.potentialIssues.push(`High retry count: code ${req.code}, attempts ${attempts}`);
|
||||
}
|
||||
if (!req.timer) {
|
||||
analysis.potentialIssues.push(`Missing timer: code ${req.code}`);
|
||||
}
|
||||
});
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener for monitoring events
|
||||
*/
|
||||
addListener(callback) {
|
||||
this.listeners.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener
|
||||
*/
|
||||
removeListener(callback) {
|
||||
const index = this.listeners.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all listeners
|
||||
*/
|
||||
_notifyListeners() {
|
||||
const status = this.getStatus();
|
||||
this.listeners.forEach((listener) => {
|
||||
try {
|
||||
listener(status);
|
||||
} catch (error) {
|
||||
console.error("Error in MSP monitor listener:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset metrics
|
||||
*/
|
||||
resetMetrics() {
|
||||
this.metrics = {
|
||||
totalRequests: 0,
|
||||
completedRequests: 0,
|
||||
failedRequests: 0,
|
||||
timeouts: 0,
|
||||
duplicates: 0,
|
||||
avgResponseTime: 0,
|
||||
maxResponseTime: 0,
|
||||
queuePeakSize: 0,
|
||||
requestsByCode: new Map(),
|
||||
responseTimes: [],
|
||||
errorsByType: new Map(),
|
||||
};
|
||||
|
||||
this.alerts = {
|
||||
queueFull: false,
|
||||
highTimeout: false,
|
||||
slowResponses: false,
|
||||
memoryLeak: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a detailed report
|
||||
*/
|
||||
generateReport() {
|
||||
const status = this.getStatus();
|
||||
const analysis = this.analyzeQueue();
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
queueHealth: this._assessQueueHealth(),
|
||||
performanceGrade: this._calculatePerformanceGrade(),
|
||||
recommendations: this._generateRecommendations(),
|
||||
},
|
||||
status,
|
||||
analysis,
|
||||
rawMetrics: this.metrics,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess overall queue health
|
||||
*/
|
||||
_assessQueueHealth() {
|
||||
const alerts = Object.values(this.alerts);
|
||||
const activeAlerts = alerts.filter((alert) => alert).length;
|
||||
|
||||
if (activeAlerts === 0) return "HEALTHY";
|
||||
if (activeAlerts <= 2) return "WARNING";
|
||||
return "CRITICAL";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate performance grade
|
||||
*/
|
||||
_calculatePerformanceGrade() {
|
||||
let score = 100;
|
||||
|
||||
// Deduct for high timeout rate
|
||||
if (this.metrics.timeoutRate > 0.1) score -= 30;
|
||||
else if (this.metrics.timeoutRate > 0.05) score -= 15;
|
||||
|
||||
// Deduct for slow responses
|
||||
if (this.metrics.avgResponseTime > 2000) score -= 25;
|
||||
else if (this.metrics.avgResponseTime > 1000) score -= 10;
|
||||
|
||||
// Deduct for queue size issues
|
||||
const queueRatio = this.currentQueueSize / (this.msp.MAX_QUEUE_SIZE || 50);
|
||||
if (queueRatio > 0.8) score -= 20;
|
||||
else if (queueRatio > 0.6) score -= 10;
|
||||
|
||||
// Deduct for failed requests
|
||||
const failureRate =
|
||||
this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
|
||||
if (failureRate > 0.05) score -= 15;
|
||||
|
||||
if (score >= 90) return "A";
|
||||
if (score >= 80) return "B";
|
||||
if (score >= 70) return "C";
|
||||
if (score >= 60) return "D";
|
||||
return "F";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate recommendations
|
||||
*/
|
||||
_generateRecommendations() {
|
||||
const recommendations = [];
|
||||
|
||||
if (this.alerts.queueFull) {
|
||||
recommendations.push(
|
||||
"Queue is near capacity. Consider implementing request prioritization or increasing queue size.",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.alerts.highTimeout) {
|
||||
recommendations.push(
|
||||
"High timeout rate detected. Check serial connection stability or increase timeout values.",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.alerts.slowResponses) {
|
||||
recommendations.push(
|
||||
"Slow response times detected. Investigate flight controller performance or reduce request frequency.",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.alerts.memoryLeak) {
|
||||
recommendations.push(
|
||||
"Potential memory leak detected. Check that all requests are being properly cleaned up.",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.metrics.maxResponseTime > 5000) {
|
||||
recommendations.push(
|
||||
"Some requests are taking very long to complete. Consider implementing request timeouts.",
|
||||
);
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the alert system by manually triggering alerts
|
||||
*/
|
||||
triggerTestAlerts() {
|
||||
console.log("🧪 Triggering test alerts...");
|
||||
|
||||
// Store original alerts
|
||||
const originalAlerts = { ...this.alerts };
|
||||
|
||||
// Trigger all alerts
|
||||
this.alerts.queueFull = true;
|
||||
this.alerts.highTimeout = true;
|
||||
this.alerts.slowResponses = true;
|
||||
this.alerts.memoryLeak = true;
|
||||
|
||||
console.log("🚨 Test alerts triggered:", this.alerts);
|
||||
|
||||
// Notify listeners immediately
|
||||
this._notifyListeners();
|
||||
|
||||
// Reset after 10 seconds
|
||||
setTimeout(() => {
|
||||
this.alerts = originalAlerts;
|
||||
console.log("✅ Test alerts reset");
|
||||
this._notifyListeners();
|
||||
}, 10000);
|
||||
|
||||
return this.alerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lower alert thresholds for testing
|
||||
*/
|
||||
setTestThresholds() {
|
||||
console.log("🎯 Setting test thresholds for easier alert triggering...");
|
||||
this.thresholds = {
|
||||
maxQueueSize: 1, // Alert when queue > 1
|
||||
maxAvgResponseTime: 100, // Alert when avg response > 100ms
|
||||
maxTimeoutRate: 0.01, // Alert when timeout rate > 1%
|
||||
memoryLeakThreshold: 5, // Alert when callbacks > 5
|
||||
};
|
||||
console.log("New thresholds:", this.thresholds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to normal thresholds
|
||||
*/
|
||||
setNormalThresholds() {
|
||||
console.log("🔧 Resetting to normal thresholds...");
|
||||
this.thresholds = {
|
||||
maxQueueSize: 40, // Alert when queue > 80% of MAX_QUEUE_SIZE
|
||||
maxAvgResponseTime: 2000, // Alert when avg response > 2s
|
||||
maxTimeoutRate: 0.1, // Alert when timeout rate > 10%
|
||||
memoryLeakThreshold: 100, // Alert when callbacks grow beyond expected
|
||||
};
|
||||
console.log("Normal thresholds restored:", this.thresholds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup and restore original MSP methods
|
||||
*/
|
||||
destroy() {
|
||||
this.stopMonitoring();
|
||||
|
||||
// Restore original methods
|
||||
if (this.originalSendMessage) {
|
||||
this.msp.send_message = this.originalSendMessage;
|
||||
}
|
||||
if (this.originalDispatchMessage) {
|
||||
this.msp._dispatch_message = this.originalDispatchMessage;
|
||||
}
|
||||
if (this.originalRemoveRequest) {
|
||||
this.msp._removeRequestFromCallbacks = this.originalRemoveRequest;
|
||||
}
|
||||
|
||||
this.listeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for easy use
|
||||
export const mspQueueMonitor = new MSPQueueMonitor(window.MSP);
|
580
src/js/msp/msp_stress_test.js
Normal file
580
src/js/msp/msp_stress_test.js
Normal file
|
@ -0,0 +1,580 @@
|
|||
/**
|
||||
* MSP Stress Test Framework
|
||||
* Comprehensive testing tool for MSP queue management, timeout handling, and performance
|
||||
*/
|
||||
|
||||
import { MSPQueueMonitor } from "./msp_queue_monitor.js";
|
||||
|
||||
export class MSPStressTest {
|
||||
constructor(mspInstance) {
|
||||
this.msp = mspInstance;
|
||||
this.monitor = new MSPQueueMonitor(mspInstance);
|
||||
this.isRunning = false;
|
||||
this.testResults = [];
|
||||
this.currentTest = null;
|
||||
|
||||
// Common MSP codes for testing
|
||||
this.testCodes = {
|
||||
MSP_IDENT: 100,
|
||||
MSP_STATUS: 101,
|
||||
MSP_RAW_IMU: 102,
|
||||
MSP_SERVO: 103,
|
||||
MSP_MOTOR: 104,
|
||||
MSP_RC: 105,
|
||||
MSP_RAW_GPS: 106,
|
||||
MSP_COMP_GPS: 107,
|
||||
MSP_ATTITUDE: 108,
|
||||
MSP_ALTITUDE: 109,
|
||||
MSP_ANALOG: 110,
|
||||
MSP_RC_TUNING: 111,
|
||||
MSP_PID: 112,
|
||||
MSP_PIDNAMES: 116,
|
||||
MSP_BOXNAMES: 116,
|
||||
MSP_MISC: 114,
|
||||
MSP_MOTOR_PINS: 115,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a comprehensive stress test suite
|
||||
*/
|
||||
async runStressTestSuite() {
|
||||
console.log("🚀 Starting MSP Stress Test Suite");
|
||||
this.monitor.startMonitoring(100); // High frequency monitoring during tests
|
||||
|
||||
const tests = [
|
||||
{ name: "Queue Flooding", test: () => this.testQueueFlooding() },
|
||||
{ name: "Rapid Fire Requests", test: () => this.testRapidFireRequests() },
|
||||
{ name: "Duplicate Request Handling", test: () => this.testDuplicateRequests() },
|
||||
{ name: "Timeout Recovery", test: () => this.testTimeoutRecovery() },
|
||||
{ name: "Memory Leak Detection", test: () => this.testMemoryLeaks() },
|
||||
{ name: "Concurrent Mixed Requests", test: () => this.testConcurrentMixedRequests() },
|
||||
{ name: "Queue Overflow Handling", test: () => this.testQueueOverflow() },
|
||||
{ name: "Connection Disruption", test: () => this.testConnectionDisruption() },
|
||||
{ name: "Performance Under Load", test: () => this.testPerformanceUnderLoad() },
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const testDef of tests) {
|
||||
try {
|
||||
console.log(`\n📋 Running: ${testDef.name}`);
|
||||
this.currentTest = testDef.name;
|
||||
this.monitor.resetMetrics();
|
||||
|
||||
const startTime = performance.now();
|
||||
const result = await testDef.test();
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
const testResult = {
|
||||
name: testDef.name,
|
||||
status: "PASSED",
|
||||
duration,
|
||||
result,
|
||||
metrics: this.monitor.getStatus(),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
results.push(testResult);
|
||||
console.log(`✅ ${testDef.name} completed in ${Math.round(duration)}ms`);
|
||||
|
||||
// Wait between tests to let queue settle
|
||||
await this.wait(1000);
|
||||
} catch (error) {
|
||||
console.error(`❌ ${testDef.name} failed:`, error);
|
||||
results.push({
|
||||
name: testDef.name,
|
||||
status: "FAILED",
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.monitor.stopMonitoring();
|
||||
this.testResults = results;
|
||||
|
||||
const report = this.generateTestReport(results);
|
||||
console.log("\n📊 Stress Test Suite Complete");
|
||||
console.log(report.summary);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 1: Queue Flooding - Send many requests quickly to test queue limits
|
||||
*/
|
||||
async testQueueFlooding() {
|
||||
const requestCount = 60; // More than default MAX_QUEUE_SIZE
|
||||
const promises = [];
|
||||
|
||||
console.log(` Flooding queue with ${requestCount} requests...`);
|
||||
|
||||
for (let i = 0; i < requestCount; i++) {
|
||||
const code = Object.values(this.testCodes)[i % Object.keys(this.testCodes).length];
|
||||
const promise = this.msp.promise(code, null).catch((err) => ({ error: err.message }));
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const successful = results.filter((r) => r.status === "fulfilled" && !r.value.error).length;
|
||||
const failed = results.length - successful;
|
||||
|
||||
return {
|
||||
requestsSent: requestCount,
|
||||
successful,
|
||||
failed,
|
||||
successRate: successful / requestCount,
|
||||
peakQueueSize: this.monitor.metrics.queuePeakSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 2: Rapid Fire Requests - Send requests in rapid succession
|
||||
*/
|
||||
async testRapidFireRequests() {
|
||||
const requestCount = 20;
|
||||
const interval = 10; // 10ms between requests
|
||||
|
||||
console.log(` Sending ${requestCount} requests with ${interval}ms intervals...`);
|
||||
|
||||
const results = [];
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < requestCount; i++) {
|
||||
const code = this.testCodes.MSP_STATUS;
|
||||
const requestStart = performance.now();
|
||||
|
||||
try {
|
||||
const result = await this.msp.promise(code, null);
|
||||
results.push({
|
||||
success: true,
|
||||
responseTime: performance.now() - requestStart,
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: error.message,
|
||||
responseTime: performance.now() - requestStart,
|
||||
});
|
||||
}
|
||||
|
||||
if (i < requestCount - 1) {
|
||||
await this.wait(interval);
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - startTime;
|
||||
const successful = results.filter((r) => r.success).length;
|
||||
const avgResponseTime = results.reduce((sum, r) => sum + r.responseTime, 0) / results.length;
|
||||
|
||||
return {
|
||||
requestCount,
|
||||
successful,
|
||||
failed: requestCount - successful,
|
||||
totalTime,
|
||||
avgResponseTime,
|
||||
throughput: requestCount / (totalTime / 1000), // requests per second
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 3: Duplicate Request Handling
|
||||
*/
|
||||
async testDuplicateRequests() {
|
||||
const code = this.testCodes.MSP_IDENT;
|
||||
const data = new Uint8Array([1, 2, 3]); // Same data for all requests
|
||||
const duplicateCount = 5;
|
||||
|
||||
console.log(` Sending ${duplicateCount} duplicate requests...`);
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < duplicateCount; i++) {
|
||||
promises.push(this.msp.promise(code, data).catch((err) => ({ error: err.message })));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const successful = results.filter((r) => r.status === "fulfilled" && !r.value.error).length;
|
||||
const duplicateErrors = results.filter(
|
||||
(r) => r.status === "rejected" || (r.value && r.value.error && r.value.error.includes("duplicate")),
|
||||
).length;
|
||||
|
||||
return {
|
||||
duplicatesSent: duplicateCount,
|
||||
successful,
|
||||
duplicateRejections: duplicateErrors,
|
||||
queueSizeAfter: this.msp.callbacks.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 4: Timeout Recovery
|
||||
*/
|
||||
async testTimeoutRecovery() {
|
||||
console.log(" Testing timeout recovery...");
|
||||
|
||||
// Save original timeout
|
||||
const originalTimeout = this.msp.TIMEOUT;
|
||||
this.msp.TIMEOUT = 100; // Very short timeout for testing
|
||||
|
||||
try {
|
||||
const code = this.testCodes.MSP_STATUS;
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
await this.msp.promise(code, null);
|
||||
return { error: "Expected timeout but request succeeded" };
|
||||
} catch (error) {
|
||||
const timeoutTime = performance.now() - startTime;
|
||||
|
||||
// Test that new requests work after timeout
|
||||
this.msp.TIMEOUT = originalTimeout;
|
||||
await this.wait(200);
|
||||
|
||||
const recoveryStart = performance.now();
|
||||
await this.msp.promise(this.testCodes.MSP_IDENT, null);
|
||||
const recoveryTime = performance.now() - recoveryStart;
|
||||
|
||||
return {
|
||||
timeoutOccurred: true,
|
||||
timeoutDuration: timeoutTime,
|
||||
recoveryTime,
|
||||
queueCleanedUp: this.msp.callbacks.length === 0,
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
this.msp.TIMEOUT = originalTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 5: Memory Leak Detection
|
||||
*/
|
||||
async testMemoryLeaks() {
|
||||
console.log(" Testing for memory leaks...");
|
||||
|
||||
const initialCallbackCount = this.msp.callbacks.length;
|
||||
const requestCount = 10;
|
||||
|
||||
// Send requests and let them complete
|
||||
const promises = [];
|
||||
for (let i = 0; i < requestCount; i++) {
|
||||
promises.push(this.msp.promise(this.testCodes.MSP_STATUS, null).catch(() => {}));
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
await this.wait(100); // Let cleanup complete
|
||||
|
||||
const finalCallbackCount = this.msp.callbacks.length;
|
||||
const leaked = finalCallbackCount - initialCallbackCount;
|
||||
|
||||
return {
|
||||
initialCallbacks: initialCallbackCount,
|
||||
finalCallbacks: finalCallbackCount,
|
||||
leaked,
|
||||
memoryLeakDetected: leaked > 0,
|
||||
requestsProcessed: requestCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 6: Concurrent Mixed Requests
|
||||
*/
|
||||
async testConcurrentMixedRequests() {
|
||||
console.log(" Testing concurrent mixed requests...");
|
||||
|
||||
const promises = [];
|
||||
const codes = Object.values(this.testCodes);
|
||||
|
||||
// Mix of different request types
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const code = codes[i % codes.length];
|
||||
const data = i % 3 === 0 ? new Uint8Array([i]) : null;
|
||||
|
||||
promises.push(this.msp.promise(code, data).catch((err) => ({ error: err.message })));
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const results = await Promise.allSettled(promises);
|
||||
const totalTime = performance.now() - startTime;
|
||||
|
||||
const successful = results.filter((r) => r.status === "fulfilled" && !r.value.error).length;
|
||||
|
||||
return {
|
||||
totalRequests: promises.length,
|
||||
successful,
|
||||
failed: promises.length - successful,
|
||||
totalTime,
|
||||
concurrentProcessing: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 7: Queue Overflow Handling
|
||||
*/
|
||||
async testQueueOverflow() {
|
||||
console.log(" Testing queue overflow handling...");
|
||||
|
||||
const maxQueue = this.msp.MAX_QUEUE_SIZE || 50;
|
||||
const overflowCount = maxQueue + 10;
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < overflowCount; i++) {
|
||||
promises.push(this.msp.promise(this.testCodes.MSP_STATUS, null).catch((err) => ({ error: err.message })));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const rejected = results.filter((r) => r.status === "rejected" || (r.value && r.value.error)).length;
|
||||
|
||||
return {
|
||||
attemptedRequests: overflowCount,
|
||||
maxQueueSize: maxQueue,
|
||||
rejectedDueToOverflow: rejected,
|
||||
overflowHandled: rejected > 0,
|
||||
finalQueueSize: this.msp.callbacks.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 8: Connection Disruption Simulation
|
||||
*/
|
||||
async testConnectionDisruption() {
|
||||
console.log(" Simulating connection disruption...");
|
||||
|
||||
// This test would need to work with the actual serial implementation
|
||||
// For now, we'll simulate by temporarily breaking the connection
|
||||
|
||||
const originalConnected = this.msp.serial?.connected;
|
||||
|
||||
try {
|
||||
// Simulate disconnection
|
||||
if (this.msp.serial) {
|
||||
this.msp.serial.connected = false;
|
||||
}
|
||||
|
||||
// Try to send requests while "disconnected"
|
||||
const promises = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
promises.push(
|
||||
this.msp.promise(this.testCodes.MSP_STATUS, null).catch((err) => ({ error: err.message })),
|
||||
);
|
||||
}
|
||||
|
||||
const disconnectedResults = await Promise.allSettled(promises);
|
||||
const failedWhileDisconnected = disconnectedResults.filter(
|
||||
(r) => r.status === "rejected" || (r.value && r.value.error),
|
||||
).length;
|
||||
|
||||
// Restore connection
|
||||
if (this.msp.serial) {
|
||||
this.msp.serial.connected = originalConnected;
|
||||
}
|
||||
|
||||
// Test recovery
|
||||
await this.wait(100);
|
||||
const recoveryResult = await this.msp
|
||||
.promise(this.testCodes.MSP_IDENT, null)
|
||||
.catch((err) => ({ error: err.message }));
|
||||
|
||||
return {
|
||||
failedWhileDisconnected,
|
||||
recoverySuccessful: !recoveryResult.error,
|
||||
connectionHandled: failedWhileDisconnected > 0,
|
||||
};
|
||||
} finally {
|
||||
// Ensure connection is restored
|
||||
if (this.msp.serial) {
|
||||
this.msp.serial.connected = originalConnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 9: Performance Under Load
|
||||
*/
|
||||
async testPerformanceUnderLoad() {
|
||||
console.log(" Testing performance under sustained load...");
|
||||
|
||||
const duration = 5000; // 5 seconds
|
||||
const requestInterval = 50; // Request every 50ms
|
||||
const startTime = performance.now();
|
||||
|
||||
const results = [];
|
||||
let requestCount = 0;
|
||||
|
||||
while (performance.now() - startTime < duration) {
|
||||
const requestStart = performance.now();
|
||||
requestCount++;
|
||||
|
||||
try {
|
||||
await this.msp.promise(this.testCodes.MSP_STATUS, null);
|
||||
results.push({
|
||||
success: true,
|
||||
responseTime: performance.now() - requestStart,
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
responseTime: performance.now() - requestStart,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
await this.wait(requestInterval);
|
||||
}
|
||||
|
||||
const successful = results.filter((r) => r.success).length;
|
||||
const avgResponseTime = results.reduce((sum, r) => sum + r.responseTime, 0) / results.length;
|
||||
const maxResponseTime = Math.max(...results.map((r) => r.responseTime));
|
||||
|
||||
return {
|
||||
duration,
|
||||
requestCount,
|
||||
successful,
|
||||
failed: requestCount - successful,
|
||||
successRate: successful / requestCount,
|
||||
avgResponseTime,
|
||||
maxResponseTime,
|
||||
throughput: requestCount / (duration / 1000),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate comprehensive test report
|
||||
*/
|
||||
generateTestReport(results) {
|
||||
const totalTests = results.length;
|
||||
const passedTests = results.filter((r) => r.status === "PASSED").length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
const summary = {
|
||||
totalTests,
|
||||
passed: passedTests,
|
||||
failed: failedTests,
|
||||
successRate: passedTests / totalTests,
|
||||
overallGrade: this._calculateOverallGrade(results),
|
||||
};
|
||||
|
||||
const recommendations = this._generateTestRecommendations(results);
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
summary,
|
||||
recommendations,
|
||||
detailedResults: results,
|
||||
monitorReport: this.monitor.generateReport(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate overall test grade
|
||||
*/
|
||||
_calculateOverallGrade(results) {
|
||||
const passRate = results.filter((r) => r.status === "PASSED").length / results.length;
|
||||
|
||||
if (passRate >= 0.95) return "A+";
|
||||
if (passRate >= 0.9) return "A";
|
||||
if (passRate >= 0.85) return "B+";
|
||||
if (passRate >= 0.8) return "B";
|
||||
if (passRate >= 0.75) return "C+";
|
||||
if (passRate >= 0.7) return "C";
|
||||
if (passRate >= 0.6) return "D";
|
||||
return "F";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate recommendations based on test results
|
||||
*/
|
||||
_generateTestRecommendations(results) {
|
||||
const recommendations = [];
|
||||
|
||||
// Check for specific test failures
|
||||
const failedTests = results.filter((r) => r.status === "FAILED");
|
||||
if (failedTests.length > 0) {
|
||||
recommendations.push(
|
||||
`${failedTests.length} tests failed. Review implementation for: ${failedTests.map((t) => t.name).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check performance issues
|
||||
const perfTest = results.find((r) => r.name === "Performance Under Load");
|
||||
if (perfTest && perfTest.result && perfTest.result.avgResponseTime > 1000) {
|
||||
recommendations.push("Average response time is high. Consider optimizing MSP request handling.");
|
||||
}
|
||||
|
||||
// Check memory leaks
|
||||
const memTest = results.find((r) => r.name === "Memory Leak Detection");
|
||||
if (memTest && memTest.result && memTest.result.memoryLeakDetected) {
|
||||
recommendations.push("Memory leak detected. Ensure all callbacks are properly cleaned up.");
|
||||
}
|
||||
|
||||
// Check queue overflow handling
|
||||
const overflowTest = results.find((r) => r.name === "Queue Overflow Handling");
|
||||
if (overflowTest && overflowTest.result && !overflowTest.result.overflowHandled) {
|
||||
recommendations.push("Queue overflow not properly handled. Implement proper queue management.");
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: Wait for specified milliseconds
|
||||
*/
|
||||
wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a specific test by name
|
||||
*/
|
||||
async runSpecificTest(testName) {
|
||||
const testMethods = {
|
||||
"queue-flooding": () => this.testQueueFlooding(),
|
||||
"rapid-fire": () => this.testRapidFireRequests(),
|
||||
duplicates: () => this.testDuplicateRequests(),
|
||||
"timeout-recovery": () => this.testTimeoutRecovery(),
|
||||
"memory-leaks": () => this.testMemoryLeaks(),
|
||||
"concurrent-mixed": () => this.testConcurrentMixedRequests(),
|
||||
"queue-overflow": () => this.testQueueOverflow(),
|
||||
"connection-disruption": () => this.testConnectionDisruption(),
|
||||
"performance-load": () => this.testPerformanceUnderLoad(),
|
||||
};
|
||||
|
||||
const testMethod = testMethods[testName];
|
||||
if (!testMethod) {
|
||||
throw new Error(`Unknown test: ${testName}`);
|
||||
}
|
||||
|
||||
console.log(`🧪 Running specific test: ${testName}`);
|
||||
this.monitor.startMonitoring(100);
|
||||
this.monitor.resetMetrics();
|
||||
|
||||
try {
|
||||
const result = await testMethod();
|
||||
return {
|
||||
name: testName,
|
||||
status: "PASSED",
|
||||
result,
|
||||
metrics: this.monitor.getStatus(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: testName,
|
||||
status: "FAILED",
|
||||
error: error.message,
|
||||
};
|
||||
} finally {
|
||||
this.monitor.stopMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
this.monitor.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton for easy use
|
||||
export const mspStressTest = new MSPStressTest(window.MSP);
|
405
src/js/msp/msp_test_runner.js
Normal file
405
src/js/msp/msp_test_runner.js
Normal file
|
@ -0,0 +1,405 @@
|
|||
/**
|
||||
* MSP Test Runner - Simple script to run tests and monitoring
|
||||
* Usage examples from browser console:
|
||||
*
|
||||
* // Quick start monitoring
|
||||
* MSPTestRunner.startQuickMonitor();
|
||||
*
|
||||
* // Run specific test
|
||||
* MSPTestRunner.runTest('queue-flooding');
|
||||
*
|
||||
* // Run full stress test suite
|
||||
* MSPTestRunner.runFullSuite();
|
||||
*
|
||||
* // Get current status
|
||||
* MSPTestRunner.getStatus();
|
||||
*/
|
||||
|
||||
import { mspQueueMonitor } from "./msp_queue_monitor.js";
|
||||
import { mspStressTest } from "./msp_stress_test.js";
|
||||
import { mspDebugDashboard } from "./msp_debug_dashboard.js";
|
||||
|
||||
export const MSPTestRunner = {
|
||||
/**
|
||||
* Start quick monitoring with console output
|
||||
*/
|
||||
startQuickMonitor() {
|
||||
console.log("🚀 Starting MSP Quick Monitor...");
|
||||
|
||||
mspQueueMonitor.addListener((status) => {
|
||||
if (status.alerts && Object.values(status.alerts).some((alert) => alert)) {
|
||||
console.warn("🚨 MSP Alert:", status.alerts);
|
||||
}
|
||||
|
||||
// Log every 10 seconds if monitoring
|
||||
if (Date.now() % 10000 < 500) {
|
||||
console.log(
|
||||
`📊 MSP Status: Queue=${status.currentQueueSize}/${status.maxQueueSize}, Requests=${status.metrics.totalRequests}, AvgTime=${Math.round(status.metrics.avgResponseTime)}ms`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
mspQueueMonitor.startMonitoring(1000);
|
||||
console.log("✅ Quick monitor started. Use MSPTestRunner.stopMonitor() to stop.");
|
||||
|
||||
return {
|
||||
stop: () => this.stopMonitor(),
|
||||
status: () => this.getStatus(),
|
||||
analyze: () => this.analyzeQueue(),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop monitoring
|
||||
*/
|
||||
stopMonitor() {
|
||||
mspQueueMonitor.stopMonitoring();
|
||||
console.log("⏹️ MSP Monitor stopped");
|
||||
},
|
||||
|
||||
/**
|
||||
* Run a specific stress test
|
||||
*/
|
||||
async runTest(testName) {
|
||||
console.log(`🧪 Running MSP test: ${testName}`);
|
||||
|
||||
try {
|
||||
const result = await mspStressTest.runSpecificTest(testName);
|
||||
|
||||
if (result.status === "PASSED") {
|
||||
console.log(`✅ Test ${testName} PASSED`);
|
||||
console.table(result.result);
|
||||
} else {
|
||||
console.error(`❌ Test ${testName} FAILED:`, result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`💥 Test ${testName} crashed:`, error);
|
||||
return { status: "ERROR", error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Run the full stress test suite
|
||||
*/
|
||||
async runFullSuite() {
|
||||
console.log("🚀 Running FULL MSP Stress Test Suite...");
|
||||
console.log("This may take several minutes and will stress the MSP system.");
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const results = await mspStressTest.runStressTestSuite();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(`\n📊 Test Suite Complete (${Math.round(duration / 1000)}s)`);
|
||||
console.log(`✅ Passed: ${results.summary.passed}`);
|
||||
console.log(`❌ Failed: ${results.summary.failed}`);
|
||||
console.log(`📈 Success Rate: ${Math.round(results.summary.successRate * 100)}%`);
|
||||
console.log(`🎯 Overall Grade: ${results.summary.overallGrade}`);
|
||||
|
||||
if (results.recommendations && results.recommendations.length > 0) {
|
||||
console.log("\n💡 Recommendations:");
|
||||
results.recommendations.forEach((rec) => console.log(` • ${rec}`));
|
||||
}
|
||||
|
||||
// Show detailed results table
|
||||
console.log("\n📋 Detailed Results:");
|
||||
console.table(
|
||||
results.detailedResults.map((test) => ({
|
||||
Test: test.name,
|
||||
Status: test.status,
|
||||
Duration: test.duration ? `${Math.round(test.duration)}ms` : "N/A",
|
||||
})),
|
||||
);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error("💥 Test Suite Failed:", error);
|
||||
return { error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current MSP status
|
||||
*/
|
||||
getStatus() {
|
||||
const status = mspQueueMonitor.getStatus();
|
||||
|
||||
console.log("📊 Current MSP Status:");
|
||||
console.log(` Queue: ${status.currentQueueSize}/${status.maxQueueSize}`);
|
||||
console.log(` Total Requests: ${status.metrics.totalRequests}`);
|
||||
console.log(` Success Rate: ${Math.round((status.metrics.successRate || 0) * 100)}%`);
|
||||
console.log(` Avg Response Time: ${Math.round(status.metrics.avgResponseTime || 0)}ms`);
|
||||
console.log(` Active Alerts: ${Object.values(status.alerts).filter((a) => a).length}`);
|
||||
|
||||
if (status.queueContents.length > 0) {
|
||||
console.log("\n📋 Queue Contents:");
|
||||
console.table(status.queueContents);
|
||||
}
|
||||
|
||||
return status;
|
||||
},
|
||||
|
||||
/**
|
||||
* Analyze current queue
|
||||
*/
|
||||
analyzeQueue() {
|
||||
const analysis = mspQueueMonitor.analyzeQueue();
|
||||
|
||||
console.log("🔍 Queue Analysis:");
|
||||
console.log(` Total Items: ${analysis.totalItems}`);
|
||||
console.log(" Age Distribution:", analysis.ageDistribution);
|
||||
console.log(" By Code:", analysis.byCode);
|
||||
|
||||
if (analysis.potentialIssues.length > 0) {
|
||||
console.log("⚠️ Potential Issues:");
|
||||
analysis.potentialIssues.forEach((issue) => console.log(` • ${issue}`));
|
||||
}
|
||||
|
||||
return analysis;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate and download comprehensive report
|
||||
*/
|
||||
generateReport() {
|
||||
const report = mspQueueMonitor.generateReport();
|
||||
|
||||
console.log("📄 Generating MSP Report...");
|
||||
console.log(" Queue Health:", report.summary.queueHealth);
|
||||
console.log(" Performance Grade:", report.summary.performanceGrade);
|
||||
|
||||
// Create downloadable report
|
||||
const blob = new Blob([JSON.stringify(report, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `msp-report-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
console.log("✅ Report downloaded");
|
||||
return report;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the visual dashboard
|
||||
*/
|
||||
showDashboard() {
|
||||
mspDebugDashboard.show();
|
||||
console.log("🖥️ Debug dashboard opened. Press Ctrl+Shift+M to toggle.");
|
||||
},
|
||||
|
||||
/**
|
||||
* Run a quick health check
|
||||
*/
|
||||
async quickHealthCheck() {
|
||||
console.log("🏥 Running Quick MSP Health Check...");
|
||||
|
||||
// Start monitoring briefly
|
||||
mspQueueMonitor.startMonitoring(100);
|
||||
|
||||
// Send a few test requests
|
||||
const testPromises = [
|
||||
window.MSP.promise(100, null), // MSP_IDENT
|
||||
window.MSP.promise(101, null), // MSP_STATUS
|
||||
window.MSP.promise(108, null), // MSP_ATTITUDE
|
||||
];
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
await Promise.all(testPromises);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Get status after test
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
const status = mspQueueMonitor.getStatus();
|
||||
|
||||
mspQueueMonitor.stopMonitoring();
|
||||
|
||||
const health = {
|
||||
status: "HEALTHY",
|
||||
responseTime,
|
||||
queueClearedAfterTest: status.currentQueueSize === 0,
|
||||
successRate: status.metrics.successRate || 0,
|
||||
};
|
||||
|
||||
if (responseTime > 2000) {
|
||||
health.status = "SLOW";
|
||||
health.warning = "Response times are slow";
|
||||
}
|
||||
|
||||
if (!health.queueClearedAfterTest) {
|
||||
health.status = "WARNING";
|
||||
health.warning = "Queue not properly cleared after requests";
|
||||
}
|
||||
|
||||
if (health.successRate < 1) {
|
||||
health.status = "FAILING";
|
||||
health.warning = "Some requests are failing";
|
||||
}
|
||||
|
||||
console.log(`🏥 Health Check Result: ${health.status}`);
|
||||
console.log(` Response Time: ${responseTime}ms`);
|
||||
console.log(` Queue Cleared: ${health.queueClearedAfterTest ? "✅" : "❌"}`);
|
||||
console.log(` Success Rate: ${Math.round(health.successRate * 100)}%`);
|
||||
|
||||
if (health.warning) {
|
||||
console.warn(`⚠️ ${health.warning}`);
|
||||
}
|
||||
|
||||
return health;
|
||||
} catch (error) {
|
||||
mspQueueMonitor.stopMonitoring();
|
||||
console.error("💥 Health check failed:", error);
|
||||
return { status: "ERROR", error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stress test a specific scenario
|
||||
*/
|
||||
async stressScenario(scenario) {
|
||||
const scenarios = {
|
||||
"high-frequency": async () => {
|
||||
console.log("🔥 High Frequency Scenario: Sending requests every 10ms for 5 seconds");
|
||||
const promises = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < 5000) {
|
||||
promises.push(window.MSP.promise(101, null).catch(() => {}));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
return {
|
||||
totalRequests: promises.length,
|
||||
successful: results.filter((r) => r.status === "fulfilled").length,
|
||||
duration: Date.now() - startTime,
|
||||
};
|
||||
},
|
||||
|
||||
"queue-overflow": async () => {
|
||||
console.log("💥 Queue Overflow Scenario: Flooding queue beyond capacity");
|
||||
const promises = [];
|
||||
|
||||
// Send more requests than queue can handle
|
||||
for (let i = 0; i < 100; i++) {
|
||||
promises.push(window.MSP.promise(101, null).catch((err) => ({ error: err.message })));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const successful = results.filter((r) => r.status === "fulfilled" && !r.value.error).length;
|
||||
|
||||
return {
|
||||
requestsSent: 100,
|
||||
successful,
|
||||
rejected: 100 - successful,
|
||||
};
|
||||
},
|
||||
|
||||
"mixed-load": async () => {
|
||||
console.log("🎭 Mixed Load Scenario: Various request types and sizes");
|
||||
const codes = [100, 101, 102, 104, 108, 110, 111, 112];
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const code = codes[i % codes.length];
|
||||
const data = i % 4 === 0 ? new Uint8Array([i, i + 1, i + 2]) : null;
|
||||
promises.push(window.MSP.promise(code, data).catch(() => {}));
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const results = await Promise.allSettled(promises);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
totalRequests: 30,
|
||||
successful: results.filter((r) => r.status === "fulfilled").length,
|
||||
duration,
|
||||
avgResponseTime: duration / 30,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const scenarioFn = scenarios[scenario];
|
||||
if (!scenarioFn) {
|
||||
console.error(`❌ Unknown scenario: ${scenario}`);
|
||||
console.log("Available scenarios:", Object.keys(scenarios));
|
||||
return;
|
||||
}
|
||||
|
||||
mspQueueMonitor.startMonitoring(100);
|
||||
|
||||
try {
|
||||
const result = await scenarioFn();
|
||||
const status = mspQueueMonitor.getStatus();
|
||||
|
||||
console.log("📊 Scenario Results:");
|
||||
console.table(result);
|
||||
console.log("📈 Final MSP Status:");
|
||||
console.table({
|
||||
"Queue Size": status.currentQueueSize,
|
||||
"Total Requests": status.metrics.totalRequests,
|
||||
"Success Rate": `${Math.round((status.metrics.successRate || 0) * 100)}%`,
|
||||
"Avg Response": `${Math.round(status.metrics.avgResponseTime || 0)}ms`,
|
||||
});
|
||||
|
||||
return { scenario: result, mspStatus: status };
|
||||
} catch (error) {
|
||||
console.error("💥 Scenario failed:", error);
|
||||
return { error: error.message };
|
||||
} finally {
|
||||
mspQueueMonitor.stopMonitoring();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List available commands
|
||||
*/
|
||||
help() {
|
||||
console.log(`
|
||||
🔧 MSP Test Runner Commands:
|
||||
|
||||
Basic Monitoring:
|
||||
MSPTestRunner.startQuickMonitor() - Start monitoring with console output
|
||||
MSPTestRunner.stopMonitor() - Stop monitoring
|
||||
MSPTestRunner.getStatus() - Get current status
|
||||
MSPTestRunner.analyzeQueue() - Analyze current queue
|
||||
|
||||
Testing:
|
||||
MSPTestRunner.runTest('test-name') - Run specific test
|
||||
MSPTestRunner.runFullSuite() - Run full stress test suite
|
||||
MSPTestRunner.quickHealthCheck() - Quick health check
|
||||
|
||||
Stress Scenarios:
|
||||
MSPTestRunner.stressScenario('high-frequency') - High frequency requests
|
||||
MSPTestRunner.stressScenario('queue-overflow') - Queue overflow test
|
||||
MSPTestRunner.stressScenario('mixed-load') - Mixed request types
|
||||
|
||||
Visual Tools:
|
||||
MSPTestRunner.showDashboard() - Show visual dashboard
|
||||
MSPTestRunner.generateReport() - Generate and download report
|
||||
|
||||
Available Test Names:
|
||||
'queue-flooding', 'rapid-fire', 'duplicates', 'timeout-recovery',
|
||||
'memory-leaks', 'concurrent-mixed', 'queue-overflow',
|
||||
'connection-disruption', 'performance-load'
|
||||
|
||||
Keyboard Shortcuts:
|
||||
Ctrl+Shift+M - Toggle debug dashboard
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
// Make globally available
|
||||
window.MSPTestRunner = MSPTestRunner;
|
||||
|
||||
console.log("🔧 MSP Test Runner loaded! Type MSPTestRunner.help() for commands.");
|
|
@ -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);
|
||||
|
|
106
src/test_alerts.js
Normal file
106
src/test_alerts.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* Alert System Test Script
|
||||
* Run this in the browser console to test the MSP debug alert system
|
||||
*/
|
||||
|
||||
console.log("🧪 Starting MSP Alert System Test...");
|
||||
|
||||
// Test function to check alert system
|
||||
async function testAlertSystem() {
|
||||
console.log("1. Checking if MSPDebug is available...");
|
||||
if (typeof window.MSPDebug === "undefined") {
|
||||
console.error("❌ MSPDebug not found! Debug tools may not be loaded.");
|
||||
return;
|
||||
}
|
||||
console.log("✅ MSPDebug is available");
|
||||
|
||||
console.log("2. Checking dashboard...");
|
||||
const dashboard = window.MSPDebug.dashboard;
|
||||
if (!dashboard) {
|
||||
console.error("❌ Dashboard not found!");
|
||||
return;
|
||||
}
|
||||
console.log("✅ Dashboard is available");
|
||||
|
||||
console.log("3. Checking monitor...");
|
||||
const monitor = window.MSPDebug.monitor;
|
||||
if (!monitor) {
|
||||
console.error("❌ Monitor not found!");
|
||||
return;
|
||||
}
|
||||
console.log("✅ Monitor is available");
|
||||
|
||||
console.log("4. Showing dashboard...");
|
||||
dashboard.show();
|
||||
|
||||
console.log("5. Starting monitoring...");
|
||||
monitor.startMonitoring(500);
|
||||
|
||||
console.log("6. Checking alert container exists...");
|
||||
const alertContainer = document.getElementById("alerts-container");
|
||||
if (!alertContainer) {
|
||||
console.error("❌ Alert container not found in DOM!");
|
||||
return;
|
||||
}
|
||||
console.log("✅ Alert container found:", alertContainer);
|
||||
|
||||
console.log("7. Getting current status...");
|
||||
const status = monitor.getStatus();
|
||||
console.log("Current status:", status);
|
||||
console.log("Current alerts:", status.alerts);
|
||||
|
||||
console.log("8. Testing alert display directly...");
|
||||
dashboard.updateAlerts({
|
||||
queueFull: true,
|
||||
highTimeout: false,
|
||||
slowResponses: true,
|
||||
memoryLeak: false,
|
||||
});
|
||||
|
||||
console.log("9. Checking alert container content after manual update...");
|
||||
console.log("Alert container HTML:", alertContainer.innerHTML);
|
||||
|
||||
console.log("10. Triggering test alerts...");
|
||||
const testAlerts = monitor.triggerTestAlerts();
|
||||
console.log("Test alerts triggered:", testAlerts);
|
||||
|
||||
console.log("11. Waiting 2 seconds and checking again...");
|
||||
setTimeout(() => {
|
||||
const newStatus = monitor.getStatus();
|
||||
console.log("Status after test alerts:", newStatus);
|
||||
console.log("Alerts after test:", newStatus.alerts);
|
||||
console.log("Alert container HTML after test:", alertContainer.innerHTML);
|
||||
|
||||
// Test the complete update flow
|
||||
console.log("12. Testing complete update flow...");
|
||||
dashboard.updateDisplay(newStatus);
|
||||
console.log("Alert container HTML after updateDisplay:", alertContainer.innerHTML);
|
||||
|
||||
console.log("🏁 Alert system test complete!");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testAlertSystem();
|
||||
|
||||
// Also provide manual test functions
|
||||
window.testAlertSystem = testAlertSystem;
|
||||
window.checkAlerts = () => {
|
||||
const container = document.getElementById("alerts-container");
|
||||
console.log("Alert container:", container);
|
||||
console.log("Alert container HTML:", container?.innerHTML);
|
||||
const status = window.MSPDebug?.monitor?.getStatus();
|
||||
console.log("Current alerts:", status?.alerts);
|
||||
};
|
||||
|
||||
window.manualTestAlert = () => {
|
||||
console.log("Testing alert display manually...");
|
||||
const dashboard = window.MSPDebug.dashboard;
|
||||
dashboard.updateAlerts({
|
||||
queueFull: true,
|
||||
highTimeout: true,
|
||||
slowResponses: false,
|
||||
memoryLeak: true,
|
||||
});
|
||||
console.log("Manual test alerts set");
|
||||
};
|
|
@ -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