XNSim/XNSimHtml/components/simulation-monitor.js

807 lines
28 KiB
JavaScript
Raw 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 SimulationMonitor extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.monitorStatus = {
isMonitoring: false,
lastError: null
};
this.systemInfo = null;
this.threadInfo = null;
this.statusCheckInterval = null;
this.chart = null;
this.chartInitialized = false;
}
connectedCallback() {
this.render();
// 等待组件完全加载
setTimeout(() => {
this.initializeComponent();
}, 100);
}
initializeComponent() {
if (this.chartInitialized) return;
try {
this.initChart();
this.startStatusCheck();
this.chartInitialized = true;
} catch (error) {
console.error('初始化组件失败:', error);
}
}
disconnectedCallback() {
this.stopStatusCheck();
}
startStatusCheck() {
this.checkMonitorStatus();
this.statusCheckInterval = setInterval(() => {
this.checkMonitorStatus();
}, 1000);
}
stopStatusCheck() {
if (this.statusCheckInterval) {
clearInterval(this.statusCheckInterval);
this.statusCheckInterval = null;
}
}
async checkMonitorStatus() {
try {
// 获取监控状态
const statusResponse = await fetch('/api/system-monitor/status');
const statusData = await statusResponse.json();
this.monitorStatus = statusData;
if (this.monitorStatus.isMonitoring) {
// 获取系统信息
const systemResponse = await fetch('/api/system-monitor/system-info');
const systemData = await systemResponse.json();
this.systemInfo = systemData.data;
// 获取线程信息
const threadResponse = await fetch('/api/system-monitor/thread-info');
const threadData = await threadResponse.json();
this.threadInfo = threadData.data;
}
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' };
}
}
getCoreStatusDisplay(status) {
switch (status) {
case 0:
return { text: '未加载', color: '#999999' };
case 1:
return { text: '初始化完成', color: '#FFC107' };
case 2:
return { text: '正常', color: '#4CAF50' };
case 3:
return { text: '异常', color: '#f44336' };
default:
return { text: '未知状态', color: '#f44336' };
}
}
getThreadStatusDisplay(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 input = this.shadowRoot.querySelector('.domain-input');
const startButton = this.shadowRoot.querySelector('.start-button');
const stopButton = this.shadowRoot.querySelector('.stop-button');
const statusDisplay = this.shadowRoot.querySelector('.status-display');
const engineInfo = this.shadowRoot.querySelector('#engine-info');
const coreStatus = this.shadowRoot.querySelector('#core-status');
const threadTableBody = this.shadowRoot.querySelector('#thread-table-body');
if (this.monitorStatus.isMonitoring) {
input.disabled = true;
startButton.disabled = true;
stopButton.disabled = false;
} else {
input.disabled = false;
startButton.disabled = false;
stopButton.disabled = true;
}
// 更新状态显示
statusDisplay.textContent = `监控状态: ${this.monitorStatus.isMonitoring ? '运行中' : '已停止'}`;
if (this.monitorStatus.lastError) {
statusDisplay.textContent += ` (错误: ${this.monitorStatus.lastError})`;
}
// 更新引擎信息
const engineInfoFields = [
{ label: '引擎名称', key: 'name' },
{ label: '引擎ID', key: 'id' },
{ label: '引擎状态', key: 'status' },
{ label: '引擎亲和性', key: 'affinity' },
{ label: '线程数', key: 'threadCount' }
];
engineInfo.innerHTML = `
<div class="status-grid">
${engineInfoFields.map(field => {
let value = '未知';
let color = '#666';
if (this.monitorStatus.isMonitoring && this.systemInfo?.engineInfo) {
if (field.key === 'status') {
const status = this.systemInfo.engineInfo[field.key];
const statusInfo = this.getStatusDisplay(status);
value = statusInfo.text;
color = statusInfo.color;
} else {
value = this.systemInfo.engineInfo[field.key] || '未知';
}
} else if (field.key === 'status') {
const statusInfo = this.getStatusDisplay(0);
value = statusInfo.text;
color = statusInfo.color;
}
return `
<div class="status-item">
<div class="status-label">${field.label}</div>
<div class="status-value" style="color: ${color}">${value}</div>
</div>
`;
}).join('')}
</div>
`;
// 更新核心状态
const coreStatusFields = [
{ label: '主框架状态', key: 'fw' },
{ label: '时间管理器状态', key: 'tm' },
{ label: '事件管理器状态', key: 'em' },
{ label: '环境管理器状态', key: 'sd' },
{ label: '线程管理器状态', key: 'thm' },
{ label: '模型管理器状态', key: 'mm' },
{ label: '服务管理器状态', key: 'sm' },
{ label: 'DDS管理器状态', key: 'dm' }
];
// 确保coreStatus元素存在
if (!coreStatus) {
console.error('找不到核心状态元素');
return;
}
// 无论是否有数据,都显示状态项
coreStatus.innerHTML = `
<div class="status-grid">
${coreStatusFields.map(field => {
const status = this.monitorStatus.isMonitoring && this.systemInfo?.coreStatus ?
this.systemInfo.coreStatus[field.key] : 0;
const statusInfo = this.getCoreStatusDisplay(status);
return `
<div class="status-item">
<div class="status-label">${field.label}</div>
<div class="status-value" style="color: ${statusInfo.color}">${statusInfo.text}</div>
</div>
`;
}).join('')}
</div>
`;
// 更新线程表格
if (this.monitorStatus.isMonitoring && this.threadInfo && Array.isArray(this.threadInfo)) {
threadTableBody.innerHTML = this.threadInfo.map(thread => {
const statusInfo = this.getThreadStatusDisplay(thread.status);
return `
<tr>
<td>${thread.name || '未知'}</td>
<td>${thread.id || '未知'}</td>
<td style="color: ${statusInfo.color}">${statusInfo.text}</td>
<td>${thread.priority || '未知'}</td>
<td>${thread.runCount || '0'}</td>
<td>${(thread.setFrequency || 0).toFixed(2)}</td>
<td>${(thread.avgFrequency || 0).toFixed(2)}</td>
<td>${(thread.maxFrequency || 0).toFixed(2)}</td>
<td>${(thread.minFrequency || 0).toFixed(2)}</td>
<td>${(thread.setPeriod || 0).toFixed(2)}</td>
<td>${(thread.avgPeriod || 0).toFixed(2)}</td>
<td>${(thread.maxPeriod || 0).toFixed(2)}</td>
<td>${(thread.minPeriod || 0).toFixed(2)}</td>
</tr>
`;
}).join('');
// 更新图表数据
this.updateChartData();
} else {
threadTableBody.innerHTML = '<tr><td colspan="13" style="text-align: center;">暂无线程信息</td></tr>';
// 清空图表数据
if (this.chart) {
this.chart.data.labels = [];
this.chart.data.datasets = [];
this.chart.update();
}
}
}
async startMonitoring() {
const domainId = this.shadowRoot.querySelector('.domain-input').value.trim();
// 验证域ID是否为有效的数字字符串
if (!/^\d+$/.test(domainId)) {
console.error('域ID必须是有效的数字');
return;
}
try {
// 首先检查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 })
});
if (!ddsInitResponse.ok) {
const errorData = await ddsInitResponse.json();
console.error('DDS监控初始化失败:', errorData.error);
return;
}
}
// 启动系统监控
const response = await fetch('/api/system-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();
} else {
console.error('启动监控失败:', data.error);
}
} catch (error) {
console.error('启动监控失败:', error);
}
}
async stopMonitoring() {
try {
const response = await fetch('/api/system-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: grid;
grid-template-columns: 300px 1fr;
gap: 16px;
flex: 1;
}
.left-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.right-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.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;
}
.domain-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 200px;
}
.domain-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.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;
}
.info-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-item {
background-color: #f8f9fa;
padding: 12px;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.info-item .label {
color: #666;
font-size: 13px;
display: block;
margin-bottom: 4px;
}
.info-item .value {
color: #333;
font-size: 16px;
font-weight: 500;
}
h3 {
margin: 0 0 12px 0;
color: #333;
font-size: 16px;
}
.thread-table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
}
.thread-table th,
.thread-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.thread-table th {
background-color: #f8f9fa;
font-weight: 500;
color: #333;
}
.thread-table tr:hover {
background-color: #f8f9fa;
}
.chart-container {
position: relative;
width: 100%;
height: 300px;
margin: 0;
padding: 0;
}
#thread-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);
}
.status-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.status-item {
display: flex;
align-items: center;
padding: 8px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.status-label {
flex: 1;
color: #666;
font-size: 14px;
}
.status-value {
flex: 1;
color: #333;
font-size: 14px;
font-weight: 500;
text-align: right;
}
</style>
<div class="monitor-container">
<div class="toolbar-section">
<div class="toolbar">
<div class="toolbar-left">
<div class="input-group">
<label class="input-label">DDS通信域ID</label>
<input type="text" class="domain-input" value="10">
</div>
<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="left-panel">
<div class="panel-section">
<h3>引擎信息</h3>
<div class="info-grid" id="engine-info"></div>
</div>
<div class="panel-section">
<h3>核心状态</h3>
<div class="info-grid" id="core-status"></div>
</div>
</div>
<div class="right-panel">
<div class="panel-section">
<h3>线程信息</h3>
<table class="thread-table">
<thead>
<tr>
<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="thread-table-body"></tbody>
</table>
</div>
<div class="panel-section">
<h3>线程监控</h3>
<div class="chart-container">
<canvas id="thread-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>
</div>
`;
}
initChart() {
const chartElement = this.shadowRoot.querySelector('#thread-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 = 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.threadInfo || !Array.isArray(this.threadInfo)) 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.threadInfo.map((thread, index) => {
const dataset = this.chart.data.datasets[index] || {
label: thread.name,
data: [],
borderColor: this.getRandomColor(),
fill: false
};
const value = isFrequency ? (thread.currentFrequency || 0) : (thread.currentPeriod || 0);
dataset.data = [...dataset.data, value].slice(-30);
return dataset;
})
};
// 使用新数据更新图表
this.chart.data = newData;
this.chart.update('none'); // 使用 'none' 模式更新,避免触发动画和事件
}
updateChartDisplayType(isFrequency) {
if (!this.threadInfo) return;
const datasets = this.threadInfo.map(thread => ({
label: thread.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('simulation-monitor', SimulationMonitor);