428 lines
11 KiB
TypeScript
428 lines
11 KiB
TypeScript
/**
|
|
* 性能监控工具
|
|
*/
|
|
|
|
let lastLogTime = 0
|
|
const LOG_THROTTLE = 100 // 最多每 100ms 输出一次日志
|
|
|
|
// FPS 监控相关
|
|
let fpsFrameCount = 0
|
|
let fpsLastTime = performance.now()
|
|
let fpsValues: number[] = []
|
|
let fpsMonitorEnabled = false
|
|
let fpsRafId: number | null = null
|
|
|
|
// 绘制性能统计
|
|
interface DrawStats {
|
|
totalDraws: number
|
|
totalDuration: number
|
|
minDuration: number
|
|
maxDuration: number
|
|
avgDuration: number
|
|
recentDraws: Array<{ timestamp: number; duration: number; details?: unknown }>
|
|
}
|
|
|
|
const drawStats: DrawStats = {
|
|
totalDraws: 0,
|
|
totalDuration: 0,
|
|
minDuration: Infinity,
|
|
maxDuration: 0,
|
|
avgDuration: 0,
|
|
recentDraws: [],
|
|
}
|
|
|
|
const MAX_RECENT_DRAWS = 100 // 保留最近 100 次绘制记录
|
|
|
|
// ==================== 连接线拖拽性能监控 ====================
|
|
interface LinkDragStats {
|
|
coordUpdateCount: number
|
|
coordUpdateTotalTime: number
|
|
targetDetectCount: number
|
|
targetDetectTotalTime: number
|
|
lastReportTime: number
|
|
}
|
|
|
|
const linkDragStats: LinkDragStats = {
|
|
coordUpdateCount: 0,
|
|
coordUpdateTotalTime: 0,
|
|
targetDetectCount: 0,
|
|
targetDetectTotalTime: 0,
|
|
lastReportTime: 0,
|
|
}
|
|
|
|
// ==================== 帧时间监控 ====================
|
|
interface FrameMonitorStats {
|
|
frameMonitorId: number | null
|
|
lastFrameTime: number
|
|
longFrameCount: number
|
|
frameCount: number
|
|
}
|
|
|
|
const frameMonitorStats: FrameMonitorStats = {
|
|
frameMonitorId: null,
|
|
lastFrameTime: 0,
|
|
longFrameCount: 0,
|
|
frameCount: 0,
|
|
}
|
|
|
|
export const perfMonitor = {
|
|
/**
|
|
* 记录性能日志(自动节流)
|
|
*/
|
|
log(tag: string, data: unknown) {
|
|
const now = Date.now()
|
|
if (now - lastLogTime < LOG_THROTTLE) {
|
|
return
|
|
}
|
|
lastLogTime = now
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[Perf] ${tag}:`, data)
|
|
},
|
|
|
|
/**
|
|
* 测量函数执行时间
|
|
*/
|
|
measure<T>(tag: string, fn: () => T): T {
|
|
const start = performance.now()
|
|
const result = fn()
|
|
const end = performance.now()
|
|
const duration = end - start
|
|
|
|
if (duration > 5) {
|
|
// 只记录耗时超过 5ms 的操作
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[Perf] ${tag}: ${duration.toFixed(2)}ms`)
|
|
}
|
|
|
|
return result
|
|
},
|
|
|
|
/**
|
|
* 启动 FPS 监控
|
|
* @param intervalMs 刷新间隔(毫秒),默认 1000ms
|
|
*/
|
|
startFpsMonitor(intervalMs = 1000) {
|
|
if (fpsMonitorEnabled) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('[Perf] FPS 监控已在运行中')
|
|
return
|
|
}
|
|
|
|
fpsMonitorEnabled = true
|
|
fpsFrameCount = 0
|
|
fpsLastTime = performance.now()
|
|
fpsValues = []
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log('[Perf] FPS 监控已启动')
|
|
|
|
const updateFps = () => {
|
|
if (!fpsMonitorEnabled) return
|
|
|
|
fpsFrameCount++
|
|
const currentTime = performance.now()
|
|
const elapsed = currentTime - fpsLastTime
|
|
|
|
if (elapsed >= intervalMs) {
|
|
const fps = Math.round((fpsFrameCount * 1000) / elapsed)
|
|
fpsValues.push(fps)
|
|
|
|
// 保留最近 60 个 FPS 值用于计算平均值
|
|
if (fpsValues.length > 60) {
|
|
fpsValues.shift()
|
|
}
|
|
|
|
const avgFps = Math.round(fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length)
|
|
const minFps = Math.min(...fpsValues)
|
|
const maxFps = Math.max(...fpsValues)
|
|
|
|
// 根据 FPS 设置不同颜色
|
|
const color = fps >= 55 ? '#67c23a' : fps >= 30 ? '#e6a23c' : '#f56c6c'
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`%c[Perf] FPS: ${fps} | 平均: ${avgFps} | 最小: ${minFps} | 最大: ${maxFps}`,
|
|
`color: ${color}; font-weight: bold;`,
|
|
)
|
|
|
|
fpsFrameCount = 0
|
|
fpsLastTime = currentTime
|
|
}
|
|
|
|
fpsRafId = requestAnimationFrame(updateFps)
|
|
}
|
|
|
|
fpsRafId = requestAnimationFrame(updateFps)
|
|
},
|
|
|
|
/**
|
|
* 停止 FPS 监控
|
|
*/
|
|
stopFpsMonitor() {
|
|
if (!fpsMonitorEnabled) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('[Perf] FPS 监控未运行')
|
|
return
|
|
}
|
|
|
|
fpsMonitorEnabled = false
|
|
if (fpsRafId !== null) {
|
|
cancelAnimationFrame(fpsRafId)
|
|
fpsRafId = null
|
|
}
|
|
|
|
// 输出最终统计
|
|
if (fpsValues.length > 0) {
|
|
const avgFps = Math.round(fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length)
|
|
const minFps = Math.min(...fpsValues)
|
|
const maxFps = Math.max(...fpsValues)
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`%c[Perf] FPS 监控已停止 | 总采样: ${fpsValues.length} | 平均: ${avgFps} | 最小: ${minFps} | 最大: ${maxFps}`,
|
|
'color: #909399; font-weight: bold;',
|
|
)
|
|
}
|
|
|
|
fpsValues = []
|
|
},
|
|
|
|
/**
|
|
* 记录绘制性能
|
|
* @param duration 绘制耗时(毫秒)
|
|
* @param details 额外的详细信息
|
|
*/
|
|
recordDraw(duration: number, details?: unknown) {
|
|
drawStats.totalDraws++
|
|
drawStats.totalDuration += duration
|
|
drawStats.minDuration = Math.min(drawStats.minDuration, duration)
|
|
drawStats.maxDuration = Math.max(drawStats.maxDuration, duration)
|
|
drawStats.avgDuration = drawStats.totalDuration / drawStats.totalDraws
|
|
|
|
// 记录最近的绘制
|
|
drawStats.recentDraws.push({
|
|
timestamp: Date.now(),
|
|
duration,
|
|
details,
|
|
})
|
|
|
|
// 保持数组大小在限制内
|
|
if (drawStats.recentDraws.length > MAX_RECENT_DRAWS) {
|
|
drawStats.recentDraws.shift()
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 获取绘制统计信息
|
|
*/
|
|
getDrawStats() {
|
|
return {
|
|
...drawStats,
|
|
recentDraws: drawStats.recentDraws.slice(-10), // 只返回最近 10 次
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 打印绘制统计信息
|
|
*/
|
|
printDrawStats() {
|
|
if (drawStats.totalDraws === 0) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('[Perf] 暂无绘制数据')
|
|
return
|
|
}
|
|
|
|
const recent10 = drawStats.recentDraws.slice(-10)
|
|
const recent10Avg =
|
|
recent10.reduce((sum, item) => sum + item.duration, 0) / recent10.length
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
'%c[Perf] Canvas 绘制统计',
|
|
'color: #409eff; font-weight: bold; font-size: 14px;',
|
|
)
|
|
// eslint-disable-next-line no-console
|
|
console.table({
|
|
总绘制次数: drawStats.totalDraws,
|
|
平均耗时: `${drawStats.avgDuration.toFixed(2)}ms`,
|
|
最小耗时: `${drawStats.minDuration.toFixed(2)}ms`,
|
|
最大耗时: `${drawStats.maxDuration.toFixed(2)}ms`,
|
|
最近10次平均: `${recent10Avg.toFixed(2)}ms`,
|
|
})
|
|
},
|
|
|
|
/**
|
|
* 重置绘制统计
|
|
*/
|
|
resetDrawStats() {
|
|
drawStats.totalDraws = 0
|
|
drawStats.totalDuration = 0
|
|
drawStats.minDuration = Infinity
|
|
drawStats.maxDuration = 0
|
|
drawStats.avgDuration = 0
|
|
drawStats.recentDraws = []
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log('[Perf] 绘制统计已重置')
|
|
},
|
|
|
|
/**
|
|
* 开始完整的性能测试(FPS + 绘制统计)
|
|
* @param durationMs 测试持续时间(毫秒),默认 10000ms (10秒)
|
|
*/
|
|
startPerformanceTest(durationMs = 10000) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`,
|
|
'color: #409eff; font-weight: bold; font-size: 16px;',
|
|
)
|
|
|
|
this.resetDrawStats()
|
|
this.startFpsMonitor()
|
|
|
|
setTimeout(() => {
|
|
this.stopFpsMonitor()
|
|
this.printDrawStats()
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
'%c[Perf] 性能测试完成',
|
|
'color: #67c23a; font-weight: bold; font-size: 16px;',
|
|
)
|
|
}, durationMs)
|
|
},
|
|
|
|
// ==================== 连接线拖拽性能监控 API ====================
|
|
|
|
/**
|
|
* 记录坐标更新操作
|
|
* @param duration 操作耗时(毫秒)
|
|
* @returns 是否应该输出性能报告(每秒输出一次)
|
|
*/
|
|
recordLinkDragCoordUpdate(duration: number): boolean {
|
|
linkDragStats.coordUpdateCount++
|
|
linkDragStats.coordUpdateTotalTime += duration
|
|
|
|
const now = Date.now()
|
|
if (now - linkDragStats.lastReportTime > 1000) {
|
|
const avgCoordTime =
|
|
linkDragStats.coordUpdateCount > 0
|
|
? (linkDragStats.coordUpdateTotalTime / linkDragStats.coordUpdateCount).toFixed(3)
|
|
: 0
|
|
const avgTargetTime =
|
|
linkDragStats.targetDetectCount > 0
|
|
? (linkDragStats.targetDetectTotalTime / linkDragStats.targetDetectCount).toFixed(3)
|
|
: 0
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`[LinkDrag Perf] 坐标更新: ${linkDragStats.coordUpdateCount}次, 平均${avgCoordTime}ms | ` +
|
|
`目标检测: ${linkDragStats.targetDetectCount}次, 平均${avgTargetTime}ms`,
|
|
)
|
|
|
|
this.resetLinkDragStats()
|
|
linkDragStats.lastReportTime = now
|
|
return true
|
|
}
|
|
|
|
return false
|
|
},
|
|
|
|
/**
|
|
* 记录目标检测操作
|
|
* @param duration 操作耗时(毫秒)
|
|
*/
|
|
recordLinkDragTargetDetect(duration: number): void {
|
|
linkDragStats.targetDetectCount++
|
|
linkDragStats.targetDetectTotalTime += duration
|
|
},
|
|
|
|
/**
|
|
* 重置连接线拖拽性能统计
|
|
*/
|
|
resetLinkDragStats(): void {
|
|
linkDragStats.coordUpdateCount = 0
|
|
linkDragStats.coordUpdateTotalTime = 0
|
|
linkDragStats.targetDetectCount = 0
|
|
linkDragStats.targetDetectTotalTime = 0
|
|
},
|
|
|
|
/**
|
|
* 获取连接线拖拽性能统计数据
|
|
*/
|
|
getLinkDragStats() {
|
|
return {
|
|
coordUpdateCount: linkDragStats.coordUpdateCount,
|
|
coordUpdateTotalTime: linkDragStats.coordUpdateTotalTime,
|
|
targetDetectCount: linkDragStats.targetDetectCount,
|
|
targetDetectTotalTime: linkDragStats.targetDetectTotalTime,
|
|
}
|
|
},
|
|
|
|
// ==================== 帧时间监控 API ====================
|
|
|
|
/**
|
|
* 启动帧时间监控(诊断主线程阻塞)
|
|
* @param longFrameThreshold 长帧阈值(毫秒),默认 30ms
|
|
*/
|
|
startFrameMonitor(longFrameThreshold = 30): void {
|
|
frameMonitorStats.lastFrameTime = performance.now()
|
|
frameMonitorStats.longFrameCount = 0
|
|
frameMonitorStats.frameCount = 0
|
|
|
|
const checkFrame = () => {
|
|
const now = performance.now()
|
|
const frameTime = now - frameMonitorStats.lastFrameTime
|
|
frameMonitorStats.frameCount++
|
|
|
|
// 超过阈值的帧被认为是"长帧"
|
|
if (frameTime > longFrameThreshold) {
|
|
frameMonitorStats.longFrameCount++
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`[Frame Monitor] 长帧检测: ${frameTime.toFixed(1)}ms`)
|
|
}
|
|
|
|
frameMonitorStats.lastFrameTime = now
|
|
frameMonitorStats.frameMonitorId = requestAnimationFrame(checkFrame)
|
|
}
|
|
|
|
frameMonitorStats.frameMonitorId = requestAnimationFrame(checkFrame)
|
|
},
|
|
|
|
/**
|
|
* 停止帧时间监控并输出统计信息
|
|
*/
|
|
stopFrameMonitor(): void {
|
|
if (frameMonitorStats.frameMonitorId !== null) {
|
|
cancelAnimationFrame(frameMonitorStats.frameMonitorId)
|
|
frameMonitorStats.frameMonitorId = null
|
|
|
|
const percentage =
|
|
frameMonitorStats.frameCount > 0
|
|
? (frameMonitorStats.longFrameCount / frameMonitorStats.frameCount * 100).toFixed(1)
|
|
: 0
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`[Frame Monitor] 统计: ${frameMonitorStats.frameCount}帧, ` +
|
|
`长帧数: ${frameMonitorStats.longFrameCount} (${percentage}%)`,
|
|
)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 获取帧时间监控统计数据
|
|
*/
|
|
getFrameMonitorStats() {
|
|
return {
|
|
frameCount: frameMonitorStats.frameCount,
|
|
longFrameCount: frameMonitorStats.longFrameCount,
|
|
percentage:
|
|
frameMonitorStats.frameCount > 0
|
|
? (frameMonitorStats.longFrameCount / frameMonitorStats.frameCount * 100).toFixed(1)
|
|
: 0,
|
|
}
|
|
},
|
|
}
|