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 * as THREE from "three";
|
||||||
import NotificationManager from "./utils/notifications.js";
|
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") {
|
if (typeof String.prototype.replaceAll === "undefined") {
|
||||||
String.prototype.replaceAll = function (match, replace) {
|
String.prototype.replaceAll = function (match, replace) {
|
||||||
return this.replace(new RegExp(match, "g"), () => replace);
|
return this.replace(new RegExp(match, "g"), () => replace);
|
||||||
|
@ -118,7 +128,8 @@ function startProcess() {
|
||||||
console.log(`Libraries: jQuery - ${$.fn.jquery}, three.js - ${THREE.REVISION}`);
|
console.log(`Libraries: jQuery - ${$.fn.jquery}, three.js - ${THREE.REVISION}`);
|
||||||
|
|
||||||
// Check if this is the first visit
|
// Check if this is the first visit
|
||||||
if (getConfig("firstRun").firstRun === undefined) {
|
const firstRunCfg = getConfig("firstRun") ?? {};
|
||||||
|
if (firstRunCfg.firstRun === undefined) {
|
||||||
setConfig({ firstRun: true });
|
setConfig({ firstRun: true });
|
||||||
import("./tabs/static_tab.js").then(({ staticTab }) => {
|
import("./tabs/static_tab.js").then(({ staticTab }) => {
|
||||||
staticTab.initialize("options", () => {
|
staticTab.initialize("options", () => {
|
||||||
|
|
|
@ -51,16 +51,13 @@ const MSP = {
|
||||||
message_buffer: null,
|
message_buffer: null,
|
||||||
message_buffer_uint8_view: null,
|
message_buffer_uint8_view: null,
|
||||||
message_checksum: 0,
|
message_checksum: 0,
|
||||||
messageIsJumboFrame: false,
|
|
||||||
crcError: false,
|
crcError: false,
|
||||||
|
|
||||||
callbacks: [],
|
callbacks: [],
|
||||||
packet_error: 0,
|
packet_error: 0,
|
||||||
unsupported: 0,
|
unsupported: 0,
|
||||||
|
|
||||||
MIN_TIMEOUT: 200,
|
TIMEOUT: 1000,
|
||||||
MAX_TIMEOUT: 2000,
|
|
||||||
timeout: 200,
|
|
||||||
|
|
||||||
last_received_timestamp: null,
|
last_received_timestamp: null,
|
||||||
listeners: [],
|
listeners: [],
|
||||||
|
@ -374,28 +371,19 @@ const MSP = {
|
||||||
serial.send(bufferOut);
|
serial.send(bufferOut);
|
||||||
},
|
},
|
||||||
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
|
send_message(code, data, callback_sent, callback_msp, doCallbackOnError) {
|
||||||
const connected = serial.connected;
|
if (code === undefined || !serial.connected || CONFIGURATOR.virtualMode) {
|
||||||
|
|
||||||
if (code === undefined || !connected || CONFIGURATOR.virtualMode) {
|
|
||||||
if (callback_msp) {
|
if (callback_msp) {
|
||||||
callback_msp();
|
callback_msp();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestExists = false;
|
const requestExists = this.callbacks.some((instance) => instance.code === code);
|
||||||
for (const instance of this.callbacks) {
|
|
||||||
if (instance.code === code) {
|
|
||||||
requestExists = true;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
|
const bufferOut = code <= 254 ? this.encode_message_v1(code, data) : this.encode_message_v2(code, data);
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
code: code,
|
code,
|
||||||
requestBuffer: bufferOut,
|
requestBuffer: bufferOut,
|
||||||
callback: callback_msp,
|
callback: callback_msp,
|
||||||
callbackOnError: doCallbackOnError,
|
callbackOnError: doCallbackOnError,
|
||||||
|
@ -412,31 +400,31 @@ const MSP = {
|
||||||
serial.send(bufferOut, (_sendInfo) => {
|
serial.send(bufferOut, (_sendInfo) => {
|
||||||
obj.stop = performance.now();
|
obj.stop = performance.now();
|
||||||
const executionTime = Math.round(obj.stop - obj.start);
|
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);
|
this.callbacks.push(obj);
|
||||||
|
|
||||||
// always send messages with data payload (even when there is a message already in the queue)
|
// always send messages with data payload (even when there is a message already in the queue)
|
||||||
if (data || !requestExists) {
|
if (data || !requestExists) {
|
||||||
if (this.timeout > this.MIN_TIMEOUT) {
|
|
||||||
this.timeout--;
|
|
||||||
}
|
|
||||||
|
|
||||||
serial.send(bufferOut, (sendInfo) => {
|
serial.send(bufferOut, (sendInfo) => {
|
||||||
if (sendInfo.bytesSent === bufferOut.byteLength) {
|
if (sendInfo.bytesSent === bufferOut.byteLength && callback_sent) {
|
||||||
if (callback_sent) {
|
callback_sent();
|
||||||
callback_sent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* resolves: {command: code, data: data, length: message_length}
|
* 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 isNative = Capacitor.isNativePlatform();
|
||||||
|
|
||||||
const compatible = isNative || (isChromium && (isWebSerial || isWebBluetooth || isWebUSB));
|
// Check if running in a test environment
|
||||||
|
const isTestEnvironment =
|
||||||
|
typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined);
|
||||||
|
|
||||||
|
const compatible = isTestEnvironment || isNative || (isChromium && (isWebSerial || isWebBluetooth || isWebUSB));
|
||||||
|
|
||||||
console.log("User Agent: ", navigator.userAgentData);
|
console.log("User Agent: ", navigator.userAgentData);
|
||||||
console.log("Native: ", isNative);
|
console.log("Native: ", isNative);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import MspHelper from "../../../src/js/msp/MSPHelper";
|
||||||
import MSPCodes from "../../../src/js/msp/MSPCodes";
|
import MSPCodes from "../../../src/js/msp/MSPCodes";
|
||||||
import "../../../src/js/injected_methods";
|
import "../../../src/js/injected_methods";
|
||||||
import FC from "../../../src/js/fc";
|
import FC from "../../../src/js/fc";
|
||||||
import { API_VERSION_1_46 } from "../../../src/js/data_storage";
|
import { API_VERSION_1_47 } from "../../../src/js/data_storage";
|
||||||
|
|
||||||
describe("MspHelper", () => {
|
describe("MspHelper", () => {
|
||||||
const mspHelper = new MspHelper();
|
const mspHelper = new MspHelper();
|
||||||
|
@ -79,7 +79,7 @@ describe("MspHelper", () => {
|
||||||
expect(FC.MOTOR_DATA.slice(motorCount, 8)).toContain(undefined);
|
expect(FC.MOTOR_DATA.slice(motorCount, 8)).toContain(undefined);
|
||||||
});
|
});
|
||||||
it("handles MSP_BOARD_INFO correctly for API version", () => {
|
it("handles MSP_BOARD_INFO correctly for API version", () => {
|
||||||
FC.CONFIG.apiVersion = API_VERSION_1_46;
|
FC.CONFIG.apiVersion = API_VERSION_1_47;
|
||||||
let infoBuffer = [];
|
let infoBuffer = [];
|
||||||
|
|
||||||
const boardIdentifier = appendStringToArray(infoBuffer, generateRandomString(4)); // set board-identifier
|
const boardIdentifier = appendStringToArray(infoBuffer, generateRandomString(4)); // set board-identifier
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue