blob: f0f3d9f45353534367a545630d96e4965d9daa45 [file] [log] [blame]
TRM-coding85e5c322025-06-18 19:49:21 +08001import React, { useState, useEffect } from 'react';
2import {
3 ResponsiveContainer, LineChart, Line,
4 XAxis, YAxis, Tooltip, CartesianGrid, Legend
5} from 'recharts';
TRM-coding106204b2025-06-28 00:37:46 +08006import { fetchSysCost, fetchGpuUsage } from '../api/posts_trm';
TRM-coding85e5c322025-06-18 19:49:21 +08007
8function PerformanceLogs({ userId }) {
9 const [data, setData] = useState([]);
TRM-coding106204b2025-06-28 00:37:46 +080010 const [gpuData, setGpuData] = useState([]);
TRM-coding2a8fd602025-06-19 19:33:16 +080011 const [loading, setLoading] = useState(true);
trm9984ee52025-06-20 15:16:56 +000012 const [unauthorized, setUnauthorized] = useState(false);
TRM-coding85e5c322025-06-18 19:49:21 +080013
14 useEffect(() => {
TRM-coding2a8fd602025-06-19 19:33:16 +080015 setLoading(true);
TRM-coding106204b2025-06-28 00:37:46 +080016
17 // 获取系统性能数据
18 const fetchSystemData = fetchSysCost(userId)
trm9984ee52025-06-20 15:16:56 +000019 .then(result => {
20 // 检查是否是权限错误
trmfaf2af52025-06-27 17:05:44 +000021 if (typeof result === 'string' && result.startsWith('<!DOCTYPE')) {
22 // 返回的是 HTML,说明未登录或接口错误
23 setUnauthorized(true);
24 setData([]);
25 return;
26 }
trm9984ee52025-06-20 15:16:56 +000027 if (result && result.status === 'error' && result.message === 'Unauthorized') {
28 setUnauthorized(true);
29 setData([]);
30 return;
31 }
32
33 // 确保数据是数组格式
34 let list = [];
35 if (Array.isArray(result)) {
36 list = result;
37 } else if (Array.isArray(result.data)) {
38 list = result.data;
39 } else if (Array.isArray(result.syscost)) {
40 list = result.syscost;
41 }
42
TRM-coding882dc442025-06-18 20:13:21 +080043 const msList = list.map(item => ({
44 ...item,
45 elapsed_time: item.elapsed_time * 1000,
46 cpu_user: item.cpu_user * 1000,
47 cpu_system: item.cpu_system * 1000,
48 memory_rss: item.memory_rss / (1024 * 1024*8), // convert bytes to MB
49 record_time_ts: new Date(item.record_time).getTime() // add numeric timestamp
50 }));
51 console.log('Converted data:', msList[0]); // debug first item
52 setData(msList);
trm9984ee52025-06-20 15:16:56 +000053 setUnauthorized(false);
TRM-coding882dc442025-06-18 20:13:21 +080054 })
trm9984ee52025-06-20 15:16:56 +000055 .catch(err => {
56 console.error('fetchSysCost error:', err);
57 if (err.message === 'Unauthorized') {
58 setUnauthorized(true);
59 setData([]);
60 }
TRM-coding106204b2025-06-28 00:37:46 +080061 });
62
63 // 获取GPU数据
64 const fetchGpuData = fetchGpuUsage(100)
65 .then(result => {
trmfaf2af52025-06-27 17:05:44 +000066 if (typeof result === 'string' && result.startsWith('<!DOCTYPE')) {
67 // 返回的是 HTML,说明未登录或接口错误
68 setGpuData([]);
69 return;
70 }
TRM-coding106204b2025-06-28 00:37:46 +080071 console.log('GPU data:', result);
72 const processedData = processGpuData(result);
73 setGpuData(processedData);
trm9984ee52025-06-20 15:16:56 +000074 })
TRM-coding106204b2025-06-28 00:37:46 +080075 .catch(err => {
76 console.error('fetchGpuUsage error:', err);
77 });
78
79 Promise.all([fetchSystemData, fetchGpuData])
TRM-coding2a8fd602025-06-19 19:33:16 +080080 .finally(() => setLoading(false));
TRM-coding85e5c322025-06-18 19:49:21 +080081 }, [userId]);
82
TRM-coding106204b2025-06-28 00:37:46 +080083 // 处理GPU数据,按时间戳分组
84 const processGpuData = (rawData) => {
85 const timeMap = new Map();
86
87 rawData.forEach(record => {
88 const timestamp = new Date(record.timestamp).getTime();
89 if (!timeMap.has(timestamp)) {
90 timeMap.set(timestamp, { timestamp });
91 }
92 const timeEntry = timeMap.get(timestamp);
93 timeEntry[`gpu${record.gpu_id}_usage`] = record.gpu_usage;
94 timeEntry[`gpu${record.gpu_id}_memory`] = record.gpu_memory_usage;
95 });
96
97 return Array.from(timeMap.values()).sort((a, b) => a.timestamp - b.timestamp);
98 };
99
100 // 获取GPU颜色
101 const getGpuColors = () => {
102 return ['#8884d8', '#82ca9d', '#ffc658', '#ff7300', '#8dd1e1', '#d084d0', '#ffb347', '#87ceeb'];
103 };
104
105 // 获取唯一的GPU ID列表
106 const getGpuIds = () => {
107 const ids = new Set();
108 gpuData.forEach(entry => {
109 Object.keys(entry).forEach(key => {
110 if (key.includes('gpu') && key.includes('_usage')) {
111 const gpuId = key.replace('_usage', '').replace('gpu', '');
112 ids.add(gpuId);
113 }
114 });
115 });
116 return Array.from(ids).sort((a, b) => parseInt(a) - parseInt(b));
117 };
118
TRM-coding2a8fd602025-06-19 19:33:16 +0800119 if (loading) {
120 return (
121 <section className="dashboard-performance">
122 <div style={{
123 display: 'flex',
124 justifyContent: 'center',
125 alignItems: 'center',
126 height: '400px',
127 flexDirection: 'column'
128 }}>
129 <div style={{
130 border: '4px solid #f3f3f3',
131 borderTop: '4px solid #3498db',
132 borderRadius: '50%',
133 width: '50px',
134 height: '50px',
135 animation: 'spin 1s linear infinite'
136 }}></div>
137 <p style={{ marginTop: '20px', color: '#666' }}>加载中...</p>
138 <style>{`
139 @keyframes spin {
140 0% { transform: rotate(0deg); }
141 100% { transform: rotate(360deg); }
142 }
143 `}</style>
144 </div>
145 </section>
146 );
147 }
148
trm9984ee52025-06-20 15:16:56 +0000149 if (unauthorized) {
150 return (
151 <section className="dashboard-performance">
152 <div style={{
153 display: 'flex',
154 justifyContent: 'center',
155 alignItems: 'center',
156 height: '400px',
157 flexDirection: 'column',
158 textAlign: 'center'
159 }}>
160 <div style={{
161 fontSize: '18px',
162 color: '#ff4d4f',
163 marginBottom: '10px'
164 }}>
165 权限不足,无法访问性能监控数据
166 </div>
167 <div style={{
168 fontSize: '14px',
169 color: '#666'
170 }}>
171 请联系管理员获取相应权限
172 </div>
173 </div>
174 </section>
175 );
176 }
177
TRM-coding85e5c322025-06-18 19:49:21 +0800178 return (
179 <section className="dashboard-performance">
TRM-coding106204b2025-06-28 00:37:46 +0800180 {/* GPU使用率图表 */}
181 <div style={{ marginBottom: '30px' }}>
182 <h3>GPU 使用率</h3>
183 <ResponsiveContainer width="100%" height={300}>
184 <LineChart data={gpuData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
185 <CartesianGrid strokeDasharray="3 3" />
186 <XAxis
187 dataKey="timestamp"
188 type="number"
189 domain={['dataMin', 'dataMax']}
190 tickFormatter={ts => new Date(ts).toLocaleTimeString()}
191 />
192 <YAxis domain={[0, 100]} label={{ value: '使用率 (%)', angle: -90, position: 'insideLeft' }} />
193 <Tooltip
trmfaf2af52025-06-27 17:05:44 +0000194 formatter={(val, name) => [`${val != null && typeof val === 'number' ? val.toFixed(2) : val}%`, name]}
TRM-coding106204b2025-06-28 00:37:46 +0800195 labelFormatter={ts => new Date(ts).toLocaleString()}
196 />
197 <Legend />
198 {getGpuIds().map((gpuId, index) => (
199 <Line
200 key={`gpu${gpuId}_usage`}
201 type="monotone"
202 dataKey={`gpu${gpuId}_usage`}
203 stroke={getGpuColors()[index % getGpuColors().length]}
204 name={`GPU ${gpuId} 使用率`}
205 connectNulls={false}
206 />
207 ))}
208 </LineChart>
209 </ResponsiveContainer>
210 </div>
211
212 {/* GPU内存占用图表 */}
213 <div style={{ marginBottom: '30px' }}>
214 <h3>GPU 内存占用</h3>
215 <ResponsiveContainer width="100%" height={300}>
216 <LineChart data={gpuData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
217 <CartesianGrid strokeDasharray="3 3" />
218 <XAxis
219 dataKey="timestamp"
220 type="number"
221 domain={['dataMin', 'dataMax']}
222 tickFormatter={ts => new Date(ts).toLocaleTimeString()}
223 />
224 <YAxis domain={[0, 'auto']} label={{ value: '内存 (MB)', angle: -90, position: 'insideLeft' }} />
225 <Tooltip
trmfaf2af52025-06-27 17:05:44 +0000226 formatter={(val, name) => [`${val != null && typeof val === 'number' ? val.toFixed(2) : val} MB`, name]}
TRM-coding106204b2025-06-28 00:37:46 +0800227 labelFormatter={ts => new Date(ts).toLocaleString()}
228 />
229 <Legend />
230 {getGpuIds().map((gpuId, index) => (
231 <Line
232 key={`gpu${gpuId}_memory`}
233 type="monotone"
234 dataKey={`gpu${gpuId}_memory`}
235 stroke={getGpuColors()[index % getGpuColors().length]}
236 name={`GPU ${gpuId} 内存`}
237 connectNulls={false}
238 />
239 ))}
240 </LineChart>
241 </ResponsiveContainer>
242 </div>
243
TRM-coding882dc442025-06-18 20:13:21 +0800244 {/* 响应时间图表 */}
245 <div style={{ marginBottom: '30px' }}>
246 <h3>响应时间</h3>
247 <ResponsiveContainer width="100%" height={300}>
248 <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
249 <CartesianGrid strokeDasharray="3 3" />
250 <XAxis
251 dataKey="record_time_ts"
252 type="number"
253 domain={['dataMin', 'dataMax']}
254 tickFormatter={ts => new Date(ts).toLocaleTimeString()}
255 />
256 <YAxis domain={[0, 'auto']} label={{ value: '时间 (ms)', angle: -90, position: 'insideLeft' }} />
257 <Tooltip formatter={(val) => typeof val === 'number' ? `${val.toFixed(2)} ms` : val} />
258 <Legend />
259 <Line type="monotone" dataKey="elapsed_time" stroke="#8884d8" name="响应时间 (ms)" />
260 </LineChart>
261 </ResponsiveContainer>
262 </div>
263
264 {/* CPU时间图表 */}
265 <div style={{ marginBottom: '30px' }}>
266 <h3>CPU 使用时间</h3>
267 <ResponsiveContainer width="100%" height={300}>
268 <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
269 <CartesianGrid strokeDasharray="3 3" />
270 <XAxis
271 dataKey="record_time_ts"
272 type="number"
273 domain={['dataMin', 'dataMax']}
274 tickFormatter={ts => new Date(ts).toLocaleTimeString()}
275 />
276 <YAxis domain={[0, 'auto']} label={{ value: '时间 (ms)', angle: -90, position: 'insideLeft' }} />
277 <Tooltip formatter={(val) => typeof val === 'number' ? `${val.toFixed(2)} ms` : val} />
278 <Legend />
279 <Line type="monotone" dataKey="cpu_user" stroke="#82ca9d" name="CPU 用户时间 (ms)" />
280 <Line type="monotone" dataKey="cpu_system" stroke="#ffc658" name="CPU 系统时间 (ms)" />
281 </LineChart>
282 </ResponsiveContainer>
283 </div>
284
285 {/* 内存图表 */}
286 <div style={{ marginBottom: '30px' }}>
287 <h3>内存使用</h3>
288 <ResponsiveContainer width="100%" height={300}>
289 <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
290 <CartesianGrid strokeDasharray="3 3" />
291 <XAxis
292 dataKey="record_time_ts"
293 type="number"
294 domain={['dataMin', 'dataMax']}
295 tickFormatter={ts => new Date(ts).toLocaleTimeString()}
296 />
297 <YAxis domain={[0, 'auto']} label={{ value: '内存 (MB)', angle: -90, position: 'insideLeft' }} />
298 <Tooltip formatter={(val) => typeof val === 'number' ? `${val.toFixed(2)} MB` : val} />
299 <Legend />
300 <Line type="monotone" dataKey="memory_rss" stroke="#ff7300" name="内存 RSS" />
301 </LineChart>
302 </ResponsiveContainer>
303 </div>
TRM-coding85e5c322025-06-18 19:49:21 +0800304 </section>
305 );
306}
307
308export default PerformanceLogs;