342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
import { NextResponse } from 'next/server'
|
||
import os from 'os'
|
||
import { exec } from 'child_process'
|
||
import { promisify } from 'util'
|
||
import { getAllSessions } from '@/lib/upload-sessions-redis'
|
||
import { transcodeQueue } from '@/lib/video-transcode-queue'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { getNetworkSpeed } 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 network = {
|
||
uploadSpeed: networkSpeed?.uploadSpeed || 0, // 字节/秒
|
||
downloadSpeed: networkSpeed?.downloadSpeed || 0, // 字节/秒
|
||
interface: networkSpeed?.interface || 'unknown', // 使用的网卡
|
||
}
|
||
|
||
// 2. 上传队列(从 Redis 获取,只显示真正活跃的上传)
|
||
const uploadProcessing: any[] = []
|
||
const uploadWaiting: any[] = []
|
||
const uploadUserIds = new Set<string>()
|
||
const uploadIds = new Set<string>()
|
||
|
||
// 从 Redis 获取所有上传会话
|
||
const uploadSessions = await getAllSessions()
|
||
|
||
// 只显示真正进行中的上传(进度 < 100% 且最近有活动)
|
||
const now = Date.now()
|
||
// 说明:
|
||
// - lastActivityAt:每个 chunk 上传完成后更新(更能代表“是否真的在上传”)
|
||
// - createdAt:只能代表会话创建时间,不能代表持续活跃
|
||
const ACTIVE_THRESHOLD = 5 * 60 * 1000 // 5分钟内有活动 → 认为“活跃”
|
||
const MAX_SHOW_THRESHOLD = 60 * 60 * 1000 // 超过1小时无活动 → 不再展示(避免长期误导)
|
||
|
||
uploadSessions.forEach((session, uploadId) => {
|
||
const completedChunks = session.chunks.size
|
||
const totalChunks = session.totalChunks || 1
|
||
const progress = Math.min(
|
||
100,
|
||
Math.round((completedChunks / totalChunks) * 100)
|
||
)
|
||
|
||
const lastActivityAt = (session as any).lastActivityAt || session.createdAt || 0
|
||
const inactiveMs = lastActivityAt > 0 ? now - lastActivityAt : Number.POSITIVE_INFINITY
|
||
const isActive = inactiveMs >= 0 && inactiveMs < ACTIVE_THRESHOLD
|
||
const isTooOld = inactiveMs >= MAX_SHOW_THRESHOLD
|
||
|
||
// 只展示:未完成 + 未超过“最大无活动时间”
|
||
// 注意:即使不是“活跃”,也会展示(并返回 inactiveMs),用于解释“队列里有任务但网速很低”的情况
|
||
if (progress < 100) {
|
||
if (!isTooOld) {
|
||
uploadUserIds.add(session.userId)
|
||
uploadIds.add(uploadId)
|
||
uploadProcessing.push({
|
||
uploadId,
|
||
fileName: session.fileName,
|
||
fileSize: session.fileSize || 0,
|
||
progress,
|
||
userId: session.userId,
|
||
currentSpeed: (session as any).currentSpeed || 0, // 上传速度(字节/秒,估算)
|
||
uploadedBytes: (session as any).uploadedBytes || 0,
|
||
lastActivityAt,
|
||
inactiveMs,
|
||
isActive,
|
||
})
|
||
}
|
||
}
|
||
})
|
||
|
||
// 批量查询用户信息
|
||
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 }
|
||
)
|
||
}
|
||
}
|
||
|
||
|