XNSim/XNSimHtml/components/model-monitor.js

725 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class ModelMonitor extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.monitorId = `model_monitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.monitorStatus = {
isMonitoring: false,
lastError: null
};
this.modelInfo = null;
this.statusCheckInterval = null;
this.chart = null;
this.chartInitialized = false;
this.domainId = '10'; // 默认值
this.isActive = false;
}
connectedCallback() {
this.render();
this.isActive = true; // 设置初始状态为激活
// 等待组件完全加载
setTimeout(() => {
this.initializeComponent();
}, 100);
}
async initializeComponent() {
try {
// 确保图表被正确初始化
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
// 初始化图表
this.initChart();
// 检查当前监控状态
const statusResponse = await fetch('/api/model-monitor/status');
const statusData = await statusResponse.json();
this.monitorStatus = statusData;
// 如果已经在监控中,立即开始获取数据
if (this.monitorStatus.isMonitoring) {
// 先获取一次数据
await this.checkMonitorStatus();
// 然后开始定时检查
this.startStatusCheck();
}
this.chartInitialized = true;
} catch (error) {
console.error('初始化组件失败:', error);
this.monitorStatus.lastError = error.message;
this.updateUI();
}
}
// 修改 reactivate 方法
reactivate() {
this.isActive = true;
// 如果已经在监控中,重新开始状态检查
if (this.monitorStatus.isMonitoring) {
this.startStatusCheck();
}
}
// 修改 startStatusCheck 方法
startStatusCheck() {
if (!this.isActive) return;
// 如果已经有定时器在运行,先停止它
this.stopStatusCheck();
let consecutiveErrors = 0;
const maxConsecutiveErrors = 3;
const checkStatus = async () => {
if (!this.isActive) return;
try {
await this.checkMonitorStatus();
consecutiveErrors = 0; // 重置错误计数
} catch (error) {
consecutiveErrors++;
console.error(`状态检查失败 (${consecutiveErrors}/${maxConsecutiveErrors}):`, error);
if (consecutiveErrors >= maxConsecutiveErrors) {
console.error('连续错误次数过多,停止状态检查');
this.stopStatusCheck();
this.monitorStatus.lastError = '监控服务异常,请重新启动监控';
this.updateUI();
return;
}
}
};
// 立即执行一次
checkStatus();
// 设置定时器,每秒执行一次
this.statusCheckInterval = setInterval(checkStatus, 1000);
}
// 修改 stopStatusCheck 方法
stopStatusCheck() {
if (this.statusCheckInterval) {
clearInterval(this.statusCheckInterval);
this.statusCheckInterval = null;
}
}
// 修改 startMonitoring 方法
async startMonitoring() {
const savedSelection = localStorage.getItem('xnsim-selection');
const selection = savedSelection ? JSON.parse(savedSelection) : {};
const confID = selection.configurationId;
try {
// 获取构型参数
const configResponse = await fetch(`/api/configurations/${confID}`);
if (!configResponse.ok) {
throw new Error('获取构型参数失败');
}
const configData = await configResponse.json();
// 从构型参数中提取域ID
const domainId = configData.DomainID;
if (!domainId) {
throw new Error('构型参数中未找到有效的域ID');
}
this.domainId = domainId;
// 首先检查DDS监控状态
const ddsStatusResponse = await fetch('/api/dds-monitor/status');
const ddsStatusData = await ddsStatusResponse.json();
// 如果DDS监控未初始化先初始化DDS监控
if (!ddsStatusData.isInitialized) {
const ddsInitResponse = await fetch('/api/dds-monitor/initialize', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
domainId,
monitorId: this.monitorId
})
});
if (!ddsInitResponse.ok) {
const errorData = await ddsInitResponse.json();
console.error('DDS监控初始化失败:', errorData.error);
return;
}
}
// 启动模型监控
const response = await fetch('/api/model-monitor/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domainId })
});
const data = await response.json();
if (response.ok) {
this.monitorStatus = data.status;
this.updateUI();
this.startStatusCheck(); // 直接启动状态检查,不需要检查 isActive
} else {
console.error('启动监控失败:', data.error);
}
} catch (error) {
console.error('启动监控失败:', error);
}
}
disconnectedCallback() {
this.isActive = false;
this.stopStatusCheck();
// 注销监控器
fetch('/api/dds-monitor/unregister', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ monitorId: this.monitorId })
}).catch(error => {
console.error('注销监控器失败:', error);
});
// 不要在这里销毁图表实例,让它在重新激活时重新创建
}
async checkMonitorStatus() {
try {
// 获取监控状态
const statusResponse = await fetch('/api/model-monitor/status');
const statusData = await statusResponse.json();
this.monitorStatus = statusData;
if (this.monitorStatus.isMonitoring) {
try {
// 获取模型信息
const modelResponse = await fetch('/api/model-monitor/model-info');
if (!modelResponse.ok) {
throw new Error(`模型信息获取失败: ${modelResponse.status}`);
}
const modelData = await modelResponse.json();
if (!modelResponse.ok) {
console.error('获取模型信息失败:', modelData.error || modelData.message);
this.monitorStatus.lastError = modelData.error || modelData.message;
// 保持原有数据不变
} else {
// 只有在数据发生变化时才更新
if (JSON.stringify(this.modelInfo) !== JSON.stringify(modelData.data)) {
this.modelInfo = modelData.data;
this.monitorStatus.lastError = null;
// 确保图表存在
if (!this.chart) {
this.initChart();
}
// 更新UI
this.updateUI();
}
}
} catch (error) {
console.warn('获取模型信息失败:', error);
// 保持原有数据不变
}
} else {
// 只有在状态发生变化时才更新
if (this.modelInfo !== null) {
this.modelInfo = null;
this.monitorStatus.lastError = null;
this.updateUI();
}
}
} catch (error) {
console.error('获取监控状态失败:', error);
this.monitorStatus.lastError = error.message;
this.updateUI();
}
}
getStatusDisplay(status) {
switch (status) {
case 0:
return { text: '未运行', color: '#999999' };
case 1:
return { text: '运行中', color: '#4CAF50' };
case 2:
return { text: '暂停中', color: '#FF9800' };
case 3:
return { text: '错误', color: '#f44336' };
default:
return { text: '未知状态', color: '#f44336' };
}
}
updateUI() {
const startButton = this.shadowRoot.querySelector('.start-button');
const stopButton = this.shadowRoot.querySelector('.stop-button');
const statusDisplay = this.shadowRoot.querySelector('.status-display');
const modelTableBody = this.shadowRoot.querySelector('#model-table-body');
if (this.monitorStatus.isMonitoring) {
startButton.disabled = true;
stopButton.disabled = false;
} else {
startButton.disabled = false;
stopButton.disabled = true;
}
// 更新状态显示
statusDisplay.textContent = `监控状态: ${this.monitorStatus.isMonitoring ? '运行中' : '已停止'}`;
if (this.monitorStatus.lastError) {
statusDisplay.textContent += ` (错误: ${this.monitorStatus.lastError})`;
}
// 更新模型表格
if (this.monitorStatus.isMonitoring && this.modelInfo && Array.isArray(this.modelInfo)) {
modelTableBody.innerHTML = this.modelInfo.map(model => {
const statusInfo = this.getStatusDisplay(model.status);
return `
<tr>
<td>${model.name || '未知'}</td>
<td>${model.id || '未知'}</td>
<td style="color: ${statusInfo.color}">${statusInfo.text}</td>
<td>${model.threadId || '未知'}</td>
<td>${model.node}</td>
<td>${model.priority || '未知'}</td>
<td>${model.runCount || '0'}</td>
<td>${(model.setFrequency || 0).toFixed(2)}</td>
<td>${(model.avgFrequency || 0).toFixed(2)}</td>
<td>${(model.maxFrequency || 0).toFixed(2)}</td>
<td>${(model.minFrequency || 0).toFixed(2)}</td>
<td>${(model.setPeriod || 0).toFixed(2)}</td>
<td>${(model.avgPeriod || 0).toFixed(2)}</td>
<td>${(model.maxPeriod || 0).toFixed(2)}</td>
<td>${(model.minPeriod || 0).toFixed(2)}</td>
</tr>
`;
}).join('');
// 更新图表数据
this.updateChartData();
} else {
modelTableBody.innerHTML = '<tr><td colspan="14" style="text-align: center;">暂无模型信息</td></tr>';
// 清空图表数据
if (this.chart) {
this.chart.data.labels = [];
this.chart.data.datasets = [];
this.chart.update();
}
}
}
async stopMonitoring() {
try {
const response = await fetch('/api/model-monitor/stop', {
method: 'POST'
});
const data = await response.json();
if (response.ok) {
this.monitorStatus = data.status;
this.updateUI();
} else {
console.error('停止监控失败:', data.error);
}
} catch (error) {
console.error('停止监控失败:', error);
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.monitor-container {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
box-sizing: border-box;
}
.toolbar-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
}
.content-container {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
}
.panel-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
}
.toolbar {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
}
.toolbar-left {
display: flex;
gap: 12px;
align-items: center;
}
.input-group {
display: flex;
align-items: center;
gap: 8px;
}
.input-label {
font-size: 14px;
color: #333;
white-space: nowrap;
}
.control-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.control-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.start-button {
background-color: #4CAF50;
color: white;
}
.start-button:hover:not(:disabled) {
background-color: #45a049;
}
.stop-button {
background-color: #f44336;
color: white;
}
.stop-button:hover:not(:disabled) {
background-color: #da190b;
}
.status-display {
padding: 8px 12px;
background-color: #f5f5f5;
border-radius: 4px;
font-size: 14px;
color: #666;
}
h3 {
margin: 0 0 12px 0;
color: #333;
font-size: 16px;
}
.model-table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
}
.model-table th,
.model-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.model-table th {
background-color: #f8f9fa;
font-weight: 500;
color: #333;
}
.model-table tr:hover {
background-color: #f8f9fa;
}
.chart-container {
position: relative;
width: 100%;
height: 300px;
margin: 0;
padding: 0;
}
#model-chart {
width: 100% !important;
height: 100% !important;
display: block;
}
.chart-controls {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
</style>
<div class="monitor-container">
<div class="toolbar-section">
<div class="toolbar">
<div class="toolbar-left">
<button class="control-button start-button" onclick="this.getRootNode().host.startMonitoring()">开始监控</button>
<button class="control-button stop-button" onclick="this.getRootNode().host.stopMonitoring()">停止监控</button>
</div>
<div class="status-display">监控状态: 未启动</div>
</div>
</div>
<div class="content-container">
<div class="panel-section">
<h3>模型信息</h3>
<table class="model-table">
<thead>
<tr>
<th>模型名称</th>
<th>模型ID</th>
<th>状态</th>
<th>线程ID</th>
<th>节点</th>
<th>优先级</th>
<th>运行次数</th>
<th>设定频率(Hz)</th>
<th>平均频率(Hz)</th>
<th>最大频率(Hz)</th>
<th>最小频率(Hz)</th>
<th>设定周期(ms)</th>
<th>平均周期(ms)</th>
<th>最大周期(ms)</th>
<th>最小周期(ms)</th>
</tr>
</thead>
<tbody id="model-table-body"></tbody>
</table>
</div>
<div class="panel-section">
<h3>模型监控</h3>
<div class="chart-container">
<canvas id="model-chart"></canvas>
</div>
<div class="chart-controls">
<span>显示类型:</span>
<label class="switch">
<input type="checkbox" id="display-type-switch">
<span class="slider"></span>
</label>
<span id="display-type-label">频率</span>
</div>
</div>
</div>
</div>
`;
}
// 修改 initChart 方法
initChart() {
const chartElement = this.shadowRoot.querySelector('#model-chart');
if (!chartElement) {
console.error('找不到图表元素');
return;
}
// 确保 Chart.js 已加载
if (typeof Chart === 'undefined') {
console.error('Chart.js 未加载');
return;
}
try {
// 创建图表实例
const ctx = chartElement.getContext('2d');
if (!ctx) {
console.error('无法获取 canvas 上下文');
return;
}
// 销毁已存在的图表实例
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
// 创建新的图表实例
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: []
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
},
animation: false,
plugins: {
legend: {
display: true
},
tooltip: {
enabled: false
}
}
}
});
// 监听显示类型切换
const switchInput = this.shadowRoot.querySelector('#display-type-switch');
const displayTypeLabel = this.shadowRoot.querySelector('#display-type-label');
if (switchInput && displayTypeLabel) {
switchInput.addEventListener('change', (e) => {
const isFrequency = e.target.checked;
displayTypeLabel.textContent = isFrequency ? '频率' : '周期';
this.updateChartDisplayType(isFrequency);
});
}
} catch (error) {
console.error('初始化图表失败:', error);
this.chart = null;
}
}
updateChartData() {
if (!this.chart || !this.modelInfo || !Array.isArray(this.modelInfo)) {
return;
}
const isFrequency = this.shadowRoot.querySelector('#display-type-switch').checked;
const currentTime = new Date().toLocaleTimeString();
// 确保图表数据集存在
if (!this.chart.data.datasets || this.chart.data.datasets.length === 0) {
this.updateChartDisplayType(isFrequency);
}
// 更新数据
const newData = {
labels: [...this.chart.data.labels, currentTime].slice(-30),
datasets: this.modelInfo.map((model, index) => {
const dataset = this.chart.data.datasets[index] || {
label: model.name,
data: [],
borderColor: this.getRandomColor(),
fill: false
};
const value = isFrequency ? (model.currentFrequency || 0) : (model.currentPeriod || 0);
dataset.data = [...dataset.data, value].slice(-30);
return dataset;
})
};
// 使用新数据更新图表
this.chart.data = newData;
this.chart.update('none'); // 使用 'none' 模式更新,避免触发动画和事件
}
updateChartDisplayType(isFrequency) {
if (!this.modelInfo) return;
const datasets = this.modelInfo.map(model => ({
label: model.name,
data: [],
borderColor: this.getRandomColor(),
fill: false
}));
this.chart.data.datasets = datasets;
this.chart.update('none'); // 使用 'none' 模式更新
}
getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
}
customElements.define('model-monitor', ModelMonitor);