perf: bound runtime memory and storage usage

This commit is contained in:
whyour
2026-08-16 16:29:06 +08:00
parent 31f78e4d1f
commit 276dfc2382
30 changed files with 1350 additions and 68 deletions
+17
View File
@@ -366,6 +366,23 @@
"添加标签成功": "Tags added successfully",
"清空日志": "Clear Logs",
"清除依赖缓存": "Clean dependency cache",
"历史数据保留与手动清理": "History retention and manual cleanup",
"保留天数为0时禁用对应清理,预览不会删除数据": "Set retention to 0 to disable that cleanup. Preview never deletes data.",
"任务统计": "Task statistics",
"清除 Node 依赖缓存": "Clean Node dependency cache",
"清除 Python 依赖缓存": "Clean Python dependency cache",
"清理后压缩数据库": "Compact database after cleanup",
"保存设置": "Save settings",
"预览清理": "Preview cleanup",
"确认清理存储数据": "Confirm storage cleanup",
"确认清理": "Clean now",
"将删除历史运行实例": "Historical running instances to delete",
"将删除任务统计": "Task statistics to delete",
"将清除依赖缓存": "Dependency caches to clear",
"清理依赖缓存后相关依赖需要重新安装": "Dependencies must be reinstalled after their cache is cleared.",
"数据库压缩期间可能暂时阻塞请求": "Database compaction may briefly block requests.",
"此操作不可恢复,请确认已完成必要备份": "This action cannot be undone. Confirm that required backups are complete.",
"清理完成": "Cleanup complete",
"清除成功": "Clean successful",
"源文件": "Source File",
"激活成功": "Activation successful",
+17
View File
@@ -366,6 +366,23 @@
"添加标签成功": "添加标签成功",
"清空日志": "清空日志",
"清除依赖缓存": "清除依赖缓存",
"历史数据保留与手动清理": "历史数据保留与手动清理",
"保留天数为0时禁用对应清理,预览不会删除数据": "保留天数为0时禁用对应清理,预览不会删除数据",
"任务统计": "任务统计",
"清除 Node 依赖缓存": "清除 Node 依赖缓存",
"清除 Python 依赖缓存": "清除 Python 依赖缓存",
"清理后压缩数据库": "清理后压缩数据库",
"保存设置": "保存设置",
"预览清理": "预览清理",
"确认清理存储数据": "确认清理存储数据",
"确认清理": "确认清理",
"将删除历史运行实例": "将删除历史运行实例",
"将删除任务统计": "将删除任务统计",
"将清除依赖缓存": "将清除依赖缓存",
"清理依赖缓存后相关依赖需要重新安装": "清理依赖缓存后相关依赖需要重新安装",
"数据库压缩期间可能暂时阻塞请求": "数据库压缩期间可能暂时阻塞请求",
"此操作不可恢复,请确认已完成必要备份": "此操作不可恢复,请确认已完成必要备份",
"清理完成": "清理完成",
"清除成功": "清除成功",
"源文件": "源文件",
"激活成功": "激活成功",
+48 -23
View File
@@ -16,11 +16,12 @@ import {
CheckCircleOutlined,
} from "@ant-design/icons";
import { PageLoading } from "@ant-design/pro-layout";
import { logEnded } from "@/utils";
import { CrontabStatus } from "./type";
import Ansi from "ansi-to-react";
const { Countdown } = Statistic;
const LOG_CHUNK_BYTES = 256 * 1024;
const MAX_LOG_VIEW_CHARS = 1024 * 1024;
const CronLogModal = ({
cron,
@@ -38,35 +39,49 @@ const CronLogModal = ({
const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false);
const scrollInfoRef = useRef({ value: 0, down: true });
const logOffsetRef = useRef<number>();
const valueRef = useRef(value);
const pollTimerRef = useRef<ReturnType<typeof setTimeout>>();
const uniqPath = logUrl ? logUrl : String(cron?.id);
const getCronLog = (isFirst?: boolean) => {
if (isFirst) {
setLoading(true);
}
const baseUrl = logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`;
const separator = baseUrl.includes("?") ? "&" : "?";
const offset = isFirst ? undefined : logOffsetRef.current;
const pagination = `${separator}limit=${LOG_CHUNK_BYTES}${
isFirst ? "&tail=true" : offset !== undefined ? `&offset=${offset}` : ""
}`;
request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then(({ code, data, logStatus }) => {
if (
code === 200 &&
localStorage.getItem("logCron") === uniqPath &&
data !== value
) {
const log = (data as string) || intl.get("暂无日志");
setValue(log);
const hasNext = logStatus === 'running';
if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) {
setTimeout(() => {
autoScroll();
});
}
setExecuting(hasNext);
if (hasNext) {
setTimeout(() => {
autoScroll();
getCronLog();
}, 2000);
}
.get(`${baseUrl}${pagination}`)
.then(({ code, data, logStatus, nextOffset }) => {
if (code !== 200 || localStorage.getItem("logCron") !== uniqPath) {
return;
}
const hasNext = logStatus === "running";
const chunk = (data as string) || "";
let log = isFirst ? chunk : `${valueRef.current}${chunk}`;
if (!log && !hasNext) {
log = intl.get("暂无日志");
}
if (log.length > MAX_LOG_VIEW_CHARS) {
log = log.slice(-MAX_LOG_VIEW_CHARS);
}
valueRef.current = log;
setValue(log);
if (typeof nextOffset === "number") {
logOffsetRef.current = nextOffset;
}
setExecuting(hasNext);
if (chunk || !hasNext) {
autoScroll();
}
if (hasNext) {
pollTimerRef.current = setTimeout(() => getCronLog(), 2000);
}
})
.finally(() => {
@@ -89,6 +104,9 @@ const CronLogModal = ({
};
const cancel = () => {
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current);
}
localStorage.removeItem("logCron");
handleCancel();
};
@@ -117,12 +135,19 @@ const CronLogModal = ({
useEffect(() => {
if (cron && cron.id) {
logOffsetRef.current = undefined;
getCronLog(true);
}
return () => {
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current);
}
};
}, [cron]);
useEffect(() => {
if (data) {
valueRef.current = data;
setValue(data);
}
}, [data]);
+1 -1
View File
@@ -59,7 +59,7 @@ const Log = () => {
.get(
`${config.apiPrefix}logs/detail?file=${node.title}&path=${
node.parent || ''
}`,
}&tail=true&limit=${1024 * 1024}`,
)
.then(({ code, data }) => {
if (code === 200) {
-1
View File
@@ -51,7 +51,6 @@ const Setting = () => {
reloadTheme,
systemInfo,
} = useOutletContext<SharedContext>();
console.log('user', user);
const columns = [
{
title: intl.get('名称'),
+157
View File
@@ -62,6 +62,8 @@ const Other = ({
cronConcurrency?: number | null;
timezone?: string | null;
globalSshKey?: string | null;
runningInstanceRetentionDays?: number | null;
cronStatRetentionDays?: number | null;
}>();
const [form] = Form.useForm();
const [exportLoading, setExportLoading] = useState(false);
@@ -69,6 +71,11 @@ const Other = ({
const showDownloadProgress = useProgress(intl.get('下载'));
const [visible, setVisible] = useState(false);
const [selectedModules, setSelectedModules] = useState<string[]>(['base']);
const [cleanupLoading, setCleanupLoading] = useState(false);
const [dependenceCacheTypes, setDependenceCacheTypes] = useState<string[]>(
[],
);
const [compactDatabase, setCompactDatabase] = useState(false);
const {
enable: enableDarkMode,
@@ -136,6 +143,93 @@ const Other = ({
});
};
const retentionPayload = () => ({
runningInstanceRetentionDays:
systemConfig?.runningInstanceRetentionDays || 0,
cronStatRetentionDays: systemConfig?.cronStatRetentionDays || 0,
});
const saveRetentionPolicy = () => {
request
.put(
`${config.apiPrefix}system/storage-retention/config`,
retentionPayload(),
)
.then(({ code }) => {
if (code === 200) {
message.success(intl.get('更新成功'));
}
});
};
const formatBytes = (bytes: number) => {
if (!bytes) return '0 B';
const units = ['B', 'KiB', 'MiB', 'GiB'];
const unit = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
units.length - 1,
);
return `${(bytes / 1024 ** unit).toFixed(unit ? 1 : 0)} ${units[unit]}`;
};
const previewStorageCleanup = () => {
setCleanupLoading(true);
const payload = {
...retentionPayload(),
dependenceCacheTypes,
compactDatabase,
};
request
.post(`${config.apiPrefix}system/storage-retention/preview`, payload)
.then(({ code, data }) => {
if (code !== 200) return;
Modal.confirm({
width: 560,
centered: true,
title: intl.get('确认清理存储数据'),
okText: intl.get('确认清理'),
cancelText: intl.get('取消'),
okButtonProps: { danger: true },
content: (
<div>
<p>
{intl.get('将删除历史运行实例')}: {data.runningInstances}
</p>
<p>
{intl.get('将删除任务统计')}: {data.cronStats}
</p>
<p>
{intl.get('将清除依赖缓存')}: {data.dependenceCaches.length} (
{formatBytes(data.dependenceCacheBytes)})
</p>
{data.dependenceCaches.length > 0 && (
<p>{intl.get('清理依赖缓存后相关依赖需要重新安装')}</p>
)}
{compactDatabase && (
<p>{intl.get('数据库压缩期间可能暂时阻塞请求')}</p>
)}
<p>{intl.get('此操作不可恢复,请确认已完成必要备份')}</p>
</div>
),
onOk: () => {
setCleanupLoading(true);
return request
.post(`${config.apiPrefix}system/storage-retention/cleanup`, {
...payload,
confirmation: 'CLEAN',
})
.then(({ code }) => {
if (code === 200) {
message.success(intl.get('清理完成'));
}
})
.finally(() => setCleanupLoading(false));
},
});
})
.finally(() => setCleanupLoading(false));
};
const exportData = () => {
setExportLoading(true);
request
@@ -302,6 +396,69 @@ const Other = ({
</Button>
</Input.Group>
</Form.Item>
<Form.Item
label={intl.get('历史数据保留与手动清理')}
tooltip={intl.get('保留天数为0时禁用对应清理,预览不会删除数据')}
>
<div style={{ marginBottom: 8 }}>
<InputNumber
addonBefore={intl.get('运行实例')}
addonAfter={intl.get('天')}
min={0}
max={3650}
value={systemConfig?.runningInstanceRetentionDays || 0}
onChange={(value) => {
setSystemConfig({
...systemConfig,
runningInstanceRetentionDays: value,
});
}}
/>
</div>
<div style={{ marginBottom: 8 }}>
<InputNumber
addonBefore={intl.get('任务统计')}
addonAfter={intl.get('天')}
min={0}
max={3650}
value={systemConfig?.cronStatRetentionDays || 0}
onChange={(value) => {
setSystemConfig({
...systemConfig,
cronStatRetentionDays: value,
});
}}
/>
</div>
<div style={{ marginBottom: 8 }}>
<Checkbox.Group
value={dependenceCacheTypes}
options={[
{ label: intl.get('清除 Node 依赖缓存'), value: 'node' },
{ label: intl.get('清除 Python 依赖缓存'), value: 'python3' },
]}
onChange={(value) => setDependenceCacheTypes(value as string[])}
/>
</div>
<div style={{ marginBottom: 8 }}>
<Checkbox
checked={compactDatabase}
onChange={(event) => setCompactDatabase(event.target.checked)}
>
{intl.get('清理后压缩数据库')}
</Checkbox>
</div>
<Button onClick={saveRetentionPolicy} style={{ marginRight: 8 }}>
{intl.get('保存设置')}
</Button>
<Button
danger
loading={cleanupLoading}
onClick={previewStorageCleanup}
>
{intl.get('预览清理')}
</Button>
</Form.Item>
<Form.Item label={intl.get('定时任务并发数')} name="frequency">
<Input.Group compact>
<InputNumber
+3 -1
View File
@@ -29,7 +29,9 @@ const SystemLog = ({ height, theme }: any) => {
const { loading, refresh } = useRequest(
() => {
return request.get<Blob>(
`${config.apiPrefix}system/log?startTime=${range[0]}&endTime=${range[1]}`,
`${config.apiPrefix}system/log?startTime=${range[0]}&endTime=${
range[1]
}&limit=${1024 * 1024}`,
{
responseType: 'blob',
},