/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef } from 'react';
import {
Banner,
Button,
Col,
Form,
InputNumber,
Row,
Spin,
Progress,
Descriptions,
Tag,
Popconfirm,
RadioGroup,
Radio,
Typography,
} from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
// 格式化字节大小
function formatBytes(bytes, decimals = 2) {
if (bytes === null || bytes === undefined || isNaN(bytes)) return '0 Bytes';
if (bytes === 0) return '0 Bytes';
if (bytes < 0) return '-' + formatBytes(-bytes, decimals);
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
if (i < 0 || i >= sizes.length) return bytes + ' Bytes';
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
export default function SettingsPerformance(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [statsLoading, setStatsLoading] = useState(false);
const [stats, setStats] = useState(null);
const [inputs, setInputs] = useState({
'performance_setting.disk_cache_enabled': false,
'performance_setting.disk_cache_threshold_mb': 10,
'performance_setting.disk_cache_max_size_mb': 1024,
'performance_setting.disk_cache_path': '',
'performance_setting.monitor_enabled': false,
'performance_setting.monitor_cpu_threshold': 90,
'performance_setting.monitor_memory_threshold': 90,
'performance_setting.monitor_disk_threshold': 90,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
const [logInfo, setLogInfo] = useState(null);
const [logCleanupMode, setLogCleanupMode] = useState('by_count');
const [logCleanupValue, setLogCleanupValue] = useState(10);
const [logCleanupLoading, setLogCleanupLoading] = useState(false);
function handleFieldChange(fieldName) {
return (value) => {
setInputs((inputs) => ({ ...inputs, [fieldName]: value }));
};
}
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
value = String(inputs[item.key]);
} else {
value = String(inputs[item.key]);
}
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
}
showSuccess(t('保存成功'));
props.refresh();
fetchStats();
})
.catch(() => {
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}
async function fetchStats() {
setStatsLoading(true);
try {
const res = await API.get('/api/performance/stats');
if (res.data.success) {
setStats(res.data.data);
}
} catch (error) {
console.error('Failed to fetch performance stats:', error);
} finally {
setStatsLoading(false);
}
}
async function clearDiskCache() {
try {
const res = await API.delete('/api/performance/disk_cache');
if (res.data.success) {
showSuccess(t('磁盘缓存已清理'));
fetchStats();
} else {
showError(res.data.message || t('清理失败'));
}
} catch (error) {
showError(t('清理失败'));
}
}
async function resetStats() {
try {
const res = await API.post('/api/performance/reset_stats');
if (res.data.success) {
showSuccess(t('统计已重置'));
fetchStats();
}
} catch (error) {
showError(t('重置失败'));
}
}
async function forceGC() {
try {
const res = await API.post('/api/performance/gc');
if (res.data.success) {
showSuccess(t('GC 已执行'));
fetchStats();
}
} catch (error) {
showError(t('GC 执行失败'));
}
}
async function fetchLogInfo() {
try {
const res = await API.get('/api/performance/logs');
if (res.data.success) {
setLogInfo(res.data.data);
}
} catch (error) {
console.error('Failed to fetch log info:', error);
}
}
async function cleanupLogFiles() {
if (logCleanupValue == null || isNaN(logCleanupValue) || logCleanupValue < 1) {
showError(t('请输入有效的数值'));
return;
}
setLogCleanupLoading(true);
try {
const res = await API.delete(
`/api/performance/logs?mode=${logCleanupMode}&value=${logCleanupValue}`,
);
if (res.data.success) {
const { deleted_count, freed_bytes } = res.data.data;
showSuccess(
t('已清理 {{count}} 个日志文件,释放 {{size}}', {
count: deleted_count,
size: formatBytes(freed_bytes),
}),
);
} else {
showError(res.data.message || t('清理失败'));
}
fetchLogInfo();
} catch (error) {
showError(t('清理失败'));
} finally {
setLogCleanupLoading(false);
}
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
if (typeof inputs[key] === 'boolean') {
currentInputs[key] =
props.options[key] === 'true' || props.options[key] === true;
} else if (typeof inputs[key] === 'number') {
currentInputs[key] = parseInt(props.options[key]) || inputs[key];
} else {
currentInputs[key] = props.options[key];
}
}
}
setInputs({ ...inputs, ...currentInputs });
setInputsRow({ ...inputs, ...currentInputs });
if (refForm.current) {
refForm.current.setValues({ ...inputs, ...currentInputs });
}
fetchStats();
fetchLogInfo();
}, [props.options]);
const diskCacheUsagePercent =
stats?.cache_stats?.disk_cache_max_bytes > 0
? (
(stats.cache_stats.current_disk_usage_bytes /
stats.cache_stats.disk_cache_max_bytes) *
100
).toFixed(1)
: 0;
return (
<>
0
? t('可用空间: {{free}} / 总空间: {{total}}', {
free: formatBytes(stats.disk_space_info.free),
total: formatBytes(stats.disk_space_info.total),
})
: t('磁盘缓存占用的最大空间')
}
min={100}
max={102400}
onChange={handleFieldChange(
'performance_setting.disk_cache_max_size_mb',
)}
disabled={!inputs['performance_setting.disk_cache_enabled']}
/>
{/* 只在非容器环境显示缓存目录配置 */}
{!stats?.config?.is_running_in_container && (
)}
{/* 服务器日志管理 */}
{logInfo === null ? null : logInfo.enabled ? (
<>
{t('清理方式')}
setLogCleanupMode(e.target.value)}
>
{t('保留最近N个文件')}
{t('保留最近N天')}
{logCleanupMode === 'by_count'
? t('保留文件数')
: t('保留天数')}
setLogCleanupValue(value)}
style={{ width: '100%' }}
/>
>
) : (
)}
{/* 性能统计 */}
{stats && (
<>
{/* 缓存使用情况 */}
{t('请求体磁盘缓存')}
{t('请求体内存缓存')}
{t('当前缓存大小')}:{' '}
{formatBytes(
stats.cache_stats.current_memory_usage_bytes,
)}
{t('活跃缓存数')}:{' '}
{stats.cache_stats.active_memory_buffers}
{t('内存命中')}: {stats.cache_stats.memory_cache_hits}
{/* 缓存目录磁盘空间 */}
{stats.disk_space_info?.total > 0 && (
{t('缓存目录磁盘空间')}
)}
{/* 系统内存统计 */}
>
)}
>
);
}