320 lines
11 KiB
TypeScript
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.

import { NextResponse } from 'next/server'
import os from 'os'
import { exec } from 'child_process'
import { promisify } from 'util'
import { uploadSessions } from '@/lib/upload-sessions'
import { transcodeQueue } from '@/lib/video-transcode-queue'
import { prisma } from '@/lib/prisma'
import { getNetworkSpeed, calculateUploadSpeedFromSessions } from '@/lib/network-bandwidth-monitor'
const execAsync = promisify(exec)
// 性能监控 - 快速统计接口(用于悬停卡片)
// GET /api/performance/quick
export const dynamic = 'force-dynamic'
export const revalidate = 0
// 获取CPU使用率(正确的方法:需要两次采样)
async function getCpuUsage(): Promise<number> {
try {
// 方法1:使用 /proc/stat 两次采样计算CPU使用率(最准确)
const getCpuStats = async () => {
const { stdout } = await execAsync("cat /proc/stat | grep '^cpu '")
const parts = stdout.trim().split(/\s+/)
return {
user: parseInt(parts[1]) || 0,
nice: parseInt(parts[2]) || 0,
system: parseInt(parts[3]) || 0,
idle: parseInt(parts[4]) || 0,
iowait: parseInt(parts[5]) || 0,
irq: parseInt(parts[6]) || 0,
softirq: parseInt(parts[7]) || 0,
}
}
const stats1 = await getCpuStats()
await new Promise(resolve => setTimeout(resolve, 1000)) // 等待1秒
const stats2 = await getCpuStats()
const total1 = stats1.user + stats1.nice + stats1.system + stats1.idle + stats1.iowait + stats1.irq + stats1.softirq
const total2 = stats2.user + stats2.nice + stats2.system + stats2.idle + stats2.iowait + stats2.irq + stats2.softirq
const totalDiff = total2 - total1
if (totalDiff > 0) {
const idleDiff = stats2.idle - stats1.idle
const usedDiff = totalDiff - idleDiff
const cpuUsage = (usedDiff / totalDiff) * 100
return Math.min(Math.max(Math.round(cpuUsage), 0), 100)
}
} catch (error) {
console.warn('[PerformanceQuick] Failed to get CPU usage from /proc/stat:', error)
}
try {
// 方法2:尝试使用 top 命令(批处理模式,更准确)
const { stdout } = await execAsync("top -bn2 -d1 | grep 'Cpu(s)' | tail -1 | awk '{print $2}' | sed 's/%us,//'", { timeout: 3000 })
const cpuUsage = parseFloat(stdout.trim())
if (!isNaN(cpuUsage) && cpuUsage >= 0 && cpuUsage <= 100) {
return Math.round(cpuUsage)
}
} catch (error) {
console.warn('[PerformanceQuick] Failed to get CPU usage from top:', error)
}
try {
// 方法3:使用 vmstat(如果可用)
const { stdout } = await execAsync("vmstat 1 2 | tail -1 | awk '{print 100-$15}'", { timeout: 3000 })
const cpuUsage = parseFloat(stdout.trim())
if (!isNaN(cpuUsage) && cpuUsage >= 0 && cpuUsage <= 100) {
return Math.round(cpuUsage)
}
} catch (error) {
console.warn('[PerformanceQuick] Failed to get CPU usage from vmstat:', error)
}
// 降级方案:使用负载平均值估算(不准确,但总比没有好)
const loadAvg = os.loadavg()[0]
const cpuCount = os.cpus().length
// 负载平均值反映系统负载,但不等同于CPU使用率
// 负载平均值 = CPU核心数时,表示CPU满载
// 这里做一个粗略估算:负载平均值 / CPU核心数 * 100,但限制在100%以内
const estimatedUsage = Math.min(Math.round((loadAvg / cpuCount) * 100), 100)
console.warn(`[PerformanceQuick] Using loadavg estimation: load=${loadAvg}, cores=${cpuCount}, estimated=${estimatedUsage}%`)
return estimatedUsage
}
export async function GET() {
try {
// 1. 系统性能(先用 Node.js os 模块,后续可接 TrueNAS)
const loadAvg = os.loadavg() // [1min, 5min, 15min]
const totalMem = os.totalmem()
const freeMem = os.freemem()
const usedMem = totalMem - freeMem
const memPercentage = totalMem > 0 ? Math.round((usedMem / totalMem) * 100) : 0
// 获取更准确的CPU使用率
const cpuUsage = await getCpuUsage()
const cpu = {
usage: cpuUsage,
loadAverage: loadAvg,
}
const memory = {
used: Math.round(usedMem / (1024 * 1024)), // MB
total: Math.round(totalMem / (1024 * 1024)), // MB
percentage: memPercentage,
}
// 获取网络带宽使用情况
const networkSpeed = await getNetworkSpeed()
const uploadSpeedFromSessions = calculateUploadSpeedFromSessions(uploadSessions)
// 优先使用系统级别的网络统计,如果没有则使用上传会话的速度
const network = {
uploadSpeed: networkSpeed?.uploadSpeed || uploadSpeedFromSessions || 0, // 字节/秒
downloadSpeed: networkSpeed?.downloadSpeed || 0, // 字节/秒
}
// 2. 上传队列(基于 uploadSessions)
const uploadProcessing: any[] = []
const uploadWaiting: any[] = []
const uploadUserIds = new Set<string>()
const uploadIds = new Set<string>()
// 只显示真正进行中的上传(进度 < 100%)
uploadSessions.forEach((session, uploadId) => {
const completedChunks = session.chunks.size
const totalChunks = session.totalChunks || 1
const progress = Math.min(
100,
Math.round((completedChunks / totalChunks) * 100)
)
// 只显示未完成的上传(进度 < 100%)
if (progress < 100) {
uploadUserIds.add(session.userId)
uploadIds.add(uploadId)
uploadProcessing.push({
uploadId,
fileName: session.fileName,
progress,
userId: session.userId,
currentSpeed: (session as any).currentSpeed || 0, // 上传速度(字节/秒)
})
}
})
// 批量查询用户信息
const userInfoMap = new Map<string, { username: string; realName?: string }>()
if (uploadUserIds.size > 0) {
try {
const users = await prisma.user.findMany({
where: {
id: { in: Array.from(uploadUserIds) },
} as any,
select: {
id: true,
username: true,
realName: true,
} as any,
})
users.forEach((user: any) => {
userInfoMap.set(user.id, {
username: user.username || '未知',
realName: user.realName || undefined,
})
})
} catch (error) {
console.error('[PerformanceQuick] Failed to fetch user info:', error)
}
}
// 尝试通过uploadId查询已创建的material记录(获取materialType)
const uploadMaterialInfoMap = new Map<string, { materialType: string }>()
if (uploadIds.size > 0) {
try {
const materials = await prisma.material.findMany({
where: {
uploadId: { in: Array.from(uploadIds) },
deletedAt: null,
} as any,
select: {
uploadId: true,
materialType: true,
} as any,
})
materials.forEach((m: any) => {
if (m.uploadId) {
uploadMaterialInfoMap.set(m.uploadId, {
materialType: m.materialType || '未知类型',
})
}
})
} catch (error) {
console.error('[PerformanceQuick] Failed to fetch upload material info:', error)
}
}
// 添加用户名和素材类型到上传队列
const uploadProcessingWithUsers = uploadProcessing.map((task) => {
const userInfo = userInfoMap.get(task.userId) || { username: '未知用户' }
const materialInfo = uploadMaterialInfoMap.get(task.uploadId) || { materialType: '未知类型' }
return {
...task,
username: userInfo.realName || userInfo.username || '未知用户',
materialType: materialInfo.materialType,
}
})
const uploadQueue = {
processing: uploadProcessingWithUsers,
waiting: uploadWaiting,
total: uploadProcessingWithUsers.length + uploadWaiting.length,
}
// 3. 转码队列(基于 transcodeQueue)
const status = transcodeQueue.getStatus()
const progressObj: Record<string, number> = {}
const stageObj: Record<string, string> = {}
// Map 无法直接序列化,转成普通对象
status.progress.forEach((value, key) => {
progressObj[key] = value
})
status.stages.forEach((value, key) => {
stageObj[key] = value
})
// 查询转码队列中的素材信息(文件名、类型、上传人等)
const processingMaterialIds = status.processingIds
const waitingMaterialIds = status.waiting
const allMaterialIds = [...processingMaterialIds, ...waitingMaterialIds]
const materialInfoMap = new Map<string, any>()
if (allMaterialIds.length > 0) {
try {
const materials = await prisma.material.findMany({
where: {
id: { in: allMaterialIds },
deletedAt: null,
} as any,
select: {
id: true,
zipFileName: true,
materialType: true,
authorName: true,
user: {
select: {
username: true,
},
},
} as any,
})
materials.forEach((m: any) => {
materialInfoMap.set(m.id, {
fileName: m.zipFileName || '未知文件',
materialType: m.materialType || '未知类型',
authorName: m.authorName || m.user?.username || '未知用户',
})
})
} catch (error) {
console.error('[PerformanceQuick] Failed to fetch material info:', error)
}
}
const transcodeQueueData = {
processing: processingMaterialIds.map((id) => {
const info = materialInfoMap.get(id) || {
fileName: '未知文件',
materialType: '未知类型',
authorName: '未知用户',
}
return {
materialId: id,
fileName: info.fileName,
materialType: info.materialType,
authorName: info.authorName,
progress: progressObj[id] ?? null,
stage: stageObj[id] ?? null,
}
}),
waiting: waitingMaterialIds.map((id, index) => {
const info = materialInfoMap.get(id) || {
fileName: '未知文件',
materialType: '未知类型',
authorName: '未知用户',
}
return {
materialId: id,
fileName: info.fileName,
materialType: info.materialType,
authorName: info.authorName,
queuePosition: index + 1,
}
}),
total: status.queueLength,
maxConcurrent: status.maxConcurrent,
}
return NextResponse.json({
cpu,
memory,
network,
uploadQueue,
transcodeQueue: transcodeQueueData,
})
} catch (error) {
console.error('[PerformanceQuick] 获取性能数据失败:', error)
return NextResponse.json(
{ error: '获取性能数据失败' },
{ status: 500 }
)
}
}