[feat] fps benchmark one-key testing
This commit is contained in:
@@ -12,28 +12,34 @@ import { Refresh, SettingConfig } from "@icon-park/react"
|
||||
// import { version } from "@tauri-apps/plugin-os"
|
||||
import { useEffect, useState } from "react"
|
||||
import { type AllSystemInfo, allSysInfo } from "tauri-plugin-system-info-api"
|
||||
import { FpsTest } from "@/components/cstb/FpsTest"
|
||||
export default function Page() {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<CardIcon type="menu">
|
||||
<SettingConfig /> 硬件
|
||||
</CardIcon>
|
||||
<CardTool>
|
||||
{/* <ToolButton>
|
||||
<UploadOne />
|
||||
云同步
|
||||
</ToolButton> */}
|
||||
<ToolButton>
|
||||
<Refresh /> 刷新
|
||||
</ToolButton>
|
||||
</CardTool>
|
||||
</CardHeader>
|
||||
<section className="flex flex-col gap-4 overflow-hidden rounded-lg">
|
||||
<div className="flex flex-col gap-4 overflow-y-auto h-full hide-scrollbar">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardIcon type="menu">
|
||||
<SettingConfig /> 硬件
|
||||
</CardIcon>
|
||||
<CardTool>
|
||||
{/* <ToolButton>
|
||||
<UploadOne />
|
||||
云同步
|
||||
</ToolButton> */}
|
||||
<ToolButton>
|
||||
<Refresh /> 刷新
|
||||
</ToolButton>
|
||||
</CardTool>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
<HardwareInfo />
|
||||
</CardBody>
|
||||
</Card>
|
||||
<CardBody>
|
||||
<HardwareInfo />
|
||||
</CardBody>
|
||||
</Card>
|
||||
<FpsTest />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,4 +24,8 @@ a {
|
||||
/* 隐藏滚动条 */
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
371
src/components/cstb/FpsTest.tsx
Normal file
371
src/components/cstb/FpsTest.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
"use client"
|
||||
import { useSteamStore } from "@/store/steam"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
|
||||
import { addToast, Button, Chip, Spinner, Switch } from "@heroui/react"
|
||||
import { useState, useEffect, useRef, useCallback } from "react"
|
||||
import { TestTube, Power } from "@icon-park/react"
|
||||
|
||||
const BENCHMARK_MAPS = [
|
||||
{
|
||||
name: "de_dust2_benchmark",
|
||||
workshopId: "3240880604",
|
||||
map: "de_dust2_benchmark",
|
||||
label: "Dust2 Benchmark",
|
||||
},
|
||||
{
|
||||
name: "de_ancient",
|
||||
workshopId: "3472126051",
|
||||
map: "de_ancient",
|
||||
label: "Ancient",
|
||||
},
|
||||
]
|
||||
|
||||
// 解析性能报告,提取时间戳和性能数据
|
||||
function parseVProfReport(rawReport: string): { timestamp: string; data: string } | null {
|
||||
if (!rawReport) return null
|
||||
|
||||
const lines = rawReport.split("\n")
|
||||
let timestamp = ""
|
||||
let inPerformanceSection = false
|
||||
const performanceLines: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
// 提取时间戳:格式如 "11/05 01:51:27 [VProf] -- Performance report --"
|
||||
const timestampMatch = line.match(
|
||||
/(\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[VProf\]\s+--\s+Performance\s+report\s+--/
|
||||
)
|
||||
if (timestampMatch) {
|
||||
timestamp = timestampMatch[1]
|
||||
inPerformanceSection = true
|
||||
// 也包含 Performance report 这一行,但移除时间戳
|
||||
const lineWithoutTimestamp = line.trim().replace(/^\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, "")
|
||||
performanceLines.push(lineWithoutTimestamp)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果在性能报告部分
|
||||
if (inPerformanceSection) {
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
// 只收集包含 [VProf] 的行
|
||||
if (trimmedLine.includes("[VProf]")) {
|
||||
// 移除行首的时间戳(格式:MM/DD HH:mm:ss )
|
||||
// 例如:"11/05 02:13:56 [VProf] ..." -> "[VProf] ..."
|
||||
const lineWithoutTimestamp = trimmedLine.replace(/^\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, "")
|
||||
performanceLines.push(lineWithoutTimestamp)
|
||||
}
|
||||
// 如果遇到空行且已经有数据,可能是报告结束,但不直接结束,因为可能还有更多数据
|
||||
// 如果后续没有 [VProf] 行的数据,空行会被过滤掉
|
||||
}
|
||||
}
|
||||
|
||||
if (performanceLines.length === 0) return null
|
||||
|
||||
return {
|
||||
timestamp,
|
||||
data: performanceLines.join("\n").trim(),
|
||||
}
|
||||
}
|
||||
|
||||
// 比较时间戳(格式:MM/DD HH:mm:ss)
|
||||
// 返回 true 如果 timestamp1 晚于 timestamp2
|
||||
function compareTimestamps(timestamp1: string, timestamp2: string): boolean {
|
||||
// 解析时间戳:MM/DD HH:mm:ss
|
||||
const parseTimestamp = (ts: string) => {
|
||||
const match = ts.match(/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/)
|
||||
if (!match) return null
|
||||
const [, month, day, hour, minute, second] = match.map(Number)
|
||||
return { month, day, hour, minute, second }
|
||||
}
|
||||
|
||||
const ts1 = parseTimestamp(timestamp1)
|
||||
const ts2 = parseTimestamp(timestamp2)
|
||||
|
||||
if (!ts1 || !ts2) return false
|
||||
|
||||
// 使用当前年份作为基准
|
||||
const now = new Date()
|
||||
const currentYear = now.getFullYear()
|
||||
|
||||
// 创建日期对象,尝试当前年份
|
||||
let date1 = new Date(currentYear, ts1.month - 1, ts1.day, ts1.hour, ts1.minute, ts1.second)
|
||||
let date2 = new Date(currentYear, ts2.month - 1, ts2.day, ts2.hour, ts2.minute, ts2.second)
|
||||
|
||||
// 如果 date1 早于 date2,可能是跨年了(比如 date1 是 1月,date2 是 12月)
|
||||
// 在这种情况下,给 date1 加一年
|
||||
if (date1 < date2) {
|
||||
// 检查是否可能是跨年(月份相差很大)
|
||||
const monthDiff = (ts1.month - ts2.month + 12) % 12
|
||||
if (monthDiff > 6) {
|
||||
// 可能是跨年,给 date1 加一年
|
||||
date1 = new Date(currentYear + 1, ts1.month - 1, ts1.day, ts1.hour, ts1.minute, ts1.second)
|
||||
}
|
||||
}
|
||||
|
||||
return date1 > date2
|
||||
}
|
||||
|
||||
export function FpsTest() {
|
||||
const steam = useSteamStore()
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testResult, setTestResult] = useState<string | null>(null)
|
||||
const [testTimestamp, setTestTimestamp] = useState<string | null>(null)
|
||||
const [selectedMap, setSelectedMap] = useState<string | null>(null)
|
||||
const [autoCloseGame, setAutoCloseGame] = useState(false)
|
||||
const [isMonitoring, setIsMonitoring] = useState(false)
|
||||
const monitoringIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
// 记录测试开始的时间戳(用于过滤旧数据)
|
||||
const testStartTimestampRef = useRef<string | null>(null)
|
||||
|
||||
// 读取结果函数
|
||||
const readResult = useCallback(
|
||||
async (silent = false): Promise<boolean> => {
|
||||
if (!steam.state.cs2Dir) {
|
||||
if (!silent) {
|
||||
addToast({ title: "请先配置 CS2 路径", variant: "flat" })
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取 console.log 路径
|
||||
const consoleLogPath = await invoke<string>("get_console_log_path", {
|
||||
csPath: steam.state.cs2Dir,
|
||||
})
|
||||
|
||||
// 读取 VProf 报告
|
||||
const report = await invoke<string>("read_vprof_report", {
|
||||
consoleLogPath: consoleLogPath,
|
||||
})
|
||||
|
||||
if (report && report.trim().length > 0) {
|
||||
const parsed = parseVProfReport(report)
|
||||
if (parsed) {
|
||||
// 如果设置了测试开始时间且是自动监听(silent=true),验证报告时间戳是否晚于测试开始时间
|
||||
// 手动读取(silent=false)时允许读取任何结果
|
||||
if (silent && testStartTimestampRef.current) {
|
||||
// 如果报告时间戳早于或等于测试开始时间,则视为旧数据,忽略
|
||||
if (!compareTimestamps(parsed.timestamp, testStartTimestampRef.current)) {
|
||||
// 这是旧数据,不处理
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
setTestResult(parsed.data)
|
||||
setTestTimestamp(parsed.timestamp)
|
||||
// 成功读取后,清除测试开始时间戳(测试已完成)
|
||||
testStartTimestampRef.current = null
|
||||
if (!silent) {
|
||||
addToast({ title: "已读取测试结果" })
|
||||
}
|
||||
return true
|
||||
} else if (!silent) {
|
||||
addToast({
|
||||
title: "未能解析测试结果",
|
||||
variant: "flat",
|
||||
})
|
||||
}
|
||||
} else if (!silent) {
|
||||
addToast({
|
||||
title: "未能读取测试结果,请确保测试已完成",
|
||||
variant: "flat",
|
||||
})
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
if (!silent) {
|
||||
console.error("读取结果失败:", error)
|
||||
addToast({
|
||||
title: `读取结果失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
variant: "flat",
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
[steam.state.cs2Dir]
|
||||
)
|
||||
|
||||
// 关闭游戏
|
||||
const closeGame = useCallback(async () => {
|
||||
try {
|
||||
await invoke("kill_game")
|
||||
addToast({ title: "已关闭CS2" })
|
||||
setIsMonitoring(false)
|
||||
if (monitoringIntervalRef.current) {
|
||||
clearInterval(monitoringIntervalRef.current)
|
||||
monitoringIntervalRef.current = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("关闭游戏失败:", error)
|
||||
addToast({
|
||||
title: `关闭游戏失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
variant: "flat",
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 开始监控文件更新
|
||||
useEffect(() => {
|
||||
if (isMonitoring && steam.state.cs2Dir) {
|
||||
// 每2秒检查一次文件更新
|
||||
monitoringIntervalRef.current = setInterval(async () => {
|
||||
const success = await readResult(true) // 静默读取
|
||||
if (success) {
|
||||
// 读取成功,停止监控
|
||||
setIsMonitoring(false)
|
||||
if (monitoringIntervalRef.current) {
|
||||
clearInterval(monitoringIntervalRef.current)
|
||||
monitoringIntervalRef.current = null
|
||||
}
|
||||
|
||||
// 如果启用了自动关闭游戏,则关闭游戏
|
||||
if (autoCloseGame) {
|
||||
setTimeout(() => {
|
||||
void closeGame()
|
||||
}, 2000) // 延迟2秒关闭,让用户看到结果
|
||||
}
|
||||
}
|
||||
}, 2000) // 每2秒检查一次
|
||||
} else {
|
||||
// 停止监控
|
||||
if (monitoringIntervalRef.current) {
|
||||
clearInterval(monitoringIntervalRef.current)
|
||||
monitoringIntervalRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (monitoringIntervalRef.current) {
|
||||
clearInterval(monitoringIntervalRef.current)
|
||||
monitoringIntervalRef.current = null
|
||||
}
|
||||
}
|
||||
}, [isMonitoring, steam.state.cs2Dir, autoCloseGame, readResult, closeGame])
|
||||
|
||||
const startTest = async (mapConfig: (typeof BENCHMARK_MAPS)[0]) => {
|
||||
if (!steam.state.steamDir || !steam.state.cs2Dir) {
|
||||
addToast({ title: "请先配置 Steam 和 CS2 路径", variant: "flat", color: "warning" })
|
||||
return
|
||||
}
|
||||
|
||||
setTesting(true)
|
||||
setSelectedMap(mapConfig.name)
|
||||
setTestResult(null)
|
||||
setTestTimestamp(null)
|
||||
|
||||
// 记录测试开始时间戳(格式:MM/DD HH:mm:ss)
|
||||
const now = new Date()
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0")
|
||||
const day = String(now.getDate()).padStart(2, "0")
|
||||
const hour = String(now.getHours()).padStart(2, "0")
|
||||
const minute = String(now.getMinutes()).padStart(2, "0")
|
||||
const second = String(now.getSeconds()).padStart(2, "0")
|
||||
testStartTimestampRef.current = `${month}/${day} ${hour}:${minute}:${second}`
|
||||
|
||||
try {
|
||||
const launchOption = `-allow_third_party_software -condebug -conclearlog +map_workshop ${mapConfig.workshopId} ${mapConfig.map}`
|
||||
|
||||
// 启动游戏
|
||||
await invoke("launch_game", {
|
||||
steamPath: `${steam.state.steamDir}\\steam.exe`,
|
||||
launchOption: launchOption,
|
||||
server: "worldwide",
|
||||
})
|
||||
|
||||
addToast({ title: `已启动 ${mapConfig.label} 测试,正在自动监听结果...` })
|
||||
setTesting(false)
|
||||
|
||||
// 开始自动监听文件更新
|
||||
setIsMonitoring(true)
|
||||
} catch (error) {
|
||||
console.error("启动测试失败:", error)
|
||||
addToast({
|
||||
title: `启动测试失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
variant: "flat",
|
||||
})
|
||||
setTesting(false)
|
||||
setIsMonitoring(false)
|
||||
// 启动失败,清除测试开始时间戳
|
||||
testStartTimestampRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardIcon>
|
||||
<TestTube size={16} /> 帧数测试
|
||||
</CardIcon>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{BENCHMARK_MAPS.map((mapConfig) => (
|
||||
<Button
|
||||
key={mapConfig.name}
|
||||
size="sm"
|
||||
isDisabled={testing || isMonitoring}
|
||||
onPress={() => {
|
||||
void startTest(mapConfig)
|
||||
}}
|
||||
className="px-4 font-medium py-1.5 transition bg-blue-200 dark:bg-blue-900/60 rounded-full select-none"
|
||||
>
|
||||
{testing && selectedMap === mapConfig.name ? (
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
) : null}
|
||||
{mapConfig.label}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
size="sm"
|
||||
isDisabled={isMonitoring}
|
||||
onPress={() => {
|
||||
void readResult()
|
||||
}}
|
||||
className="px-4 font-medium py-1.5 transition bg-green-200 dark:bg-green-900/60 rounded-full select-none"
|
||||
>
|
||||
手动读取结果
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
void closeGame()
|
||||
}}
|
||||
className="px-4 font-medium py-1.5 transition bg-orange-200 dark:bg-orange-900/60 rounded-full select-none"
|
||||
>
|
||||
<Power className="mr-1" size={14} />
|
||||
关闭游戏
|
||||
</Button>
|
||||
<Switch size="sm" isSelected={autoCloseGame} onValueChange={setAutoCloseGame} className="ml-4">
|
||||
测试完成自动关闭游戏
|
||||
</Switch>
|
||||
{isMonitoring && (
|
||||
<Chip size="sm" color="primary" variant="flat">
|
||||
正在监听中...
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Chip size="sm">测试结果</Chip>
|
||||
{testTimestamp && (
|
||||
<Chip size="sm" variant="flat" color="default">
|
||||
测试时间: {testTimestamp}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
<pre className="p-3 overflow-auto font-mono text-xs rounded-md bg-black/5 dark:bg-white/5">
|
||||
{testResult}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { appConfigDir } from "@tauri-apps/api/path"
|
||||
import { setStoreCollectionPath } from "@tauri-store/valtio"
|
||||
import { commands } from "@tauri-store/shared"
|
||||
import { appStore } from "./app"
|
||||
import { steamStore } from "./steam"
|
||||
import { toolStore } from "./tool"
|
||||
@@ -10,5 +10,6 @@ export async function init() {
|
||||
await toolStore.start()
|
||||
await steamStore.start()
|
||||
const appConfigDirPath = await appConfigDir()
|
||||
await setStoreCollectionPath(path.resolve(appConfigDirPath, "cstb"))
|
||||
const setPath = commands.setStoreCollectionPath("valtio")
|
||||
await setPath(path.resolve(appConfigDirPath, "cstb"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user