242 lines
7.9 KiB
TypeScript
242 lines
7.9 KiB
TypeScript
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'
|
||
|
||
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,
|
||
}
|
||
|
||
// 2. 上传队列(基于 uploadSessions)
|
||
const uploadProcessing: any[] = []
|
||
const uploadWaiting: any[] = []
|
||
|
||
// 只显示真正进行中的上传(进度 < 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) {
|
||
uploadProcessing.push({
|
||
uploadId,
|
||
fileName: session.fileName,
|
||
materialType: '未知', // 目前 UploadSession 未区分素材类型,后续可扩展
|
||
progress,
|
||
userId: session.userId,
|
||
})
|
||
}
|
||
})
|
||
|
||
const uploadQueue = {
|
||
processing: uploadProcessing,
|
||
waiting: uploadWaiting,
|
||
total: uploadProcessing.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,
|
||
uploadQueue,
|
||
transcodeQueue: transcodeQueueData,
|
||
})
|
||
} catch (error) {
|
||
console.error('[PerformanceQuick] 获取性能数据失败:', error)
|
||
return NextResponse.json(
|
||
{ error: '获取性能数据失败' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
|
||
|