performance-section.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  1. import { useCallback, useEffect, useState } from 'react'
  2. import * as z from 'zod'
  3. import { useForm } from 'react-hook-form'
  4. import { zodResolver } from '@hookform/resolvers/zod'
  5. import { useTranslation } from 'react-i18next'
  6. import { toast } from 'sonner'
  7. import { api } from '@/lib/api'
  8. import dayjs from '@/lib/dayjs'
  9. import { Alert, AlertDescription } from '@/components/ui/alert'
  10. import {
  11. AlertDialog,
  12. AlertDialogAction,
  13. AlertDialogCancel,
  14. AlertDialogContent,
  15. AlertDialogDescription,
  16. AlertDialogFooter,
  17. AlertDialogHeader,
  18. AlertDialogTitle,
  19. AlertDialogTrigger,
  20. } from '@/components/ui/alert-dialog'
  21. import { Button } from '@/components/ui/button'
  22. import {
  23. Form,
  24. FormControl,
  25. FormDescription,
  26. FormField,
  27. FormItem,
  28. FormLabel,
  29. } from '@/components/ui/form'
  30. import { Input } from '@/components/ui/input'
  31. import { Label } from '@/components/ui/label'
  32. import { Progress } from '@/components/ui/progress'
  33. import {
  34. Select,
  35. SelectContent,
  36. SelectItem,
  37. SelectTrigger,
  38. SelectValue,
  39. } from '@/components/ui/select'
  40. import { Separator } from '@/components/ui/separator'
  41. import { Switch } from '@/components/ui/switch'
  42. import { StatusBadge } from '@/components/status-badge'
  43. import { SettingsSection } from '../components/settings-section'
  44. import { useResetForm } from '../hooks/use-reset-form'
  45. import { useUpdateOption } from '../hooks/use-update-option'
  46. const perfSchema = z.object({
  47. 'performance_setting.disk_cache_enabled': z.boolean(),
  48. 'performance_setting.disk_cache_threshold_mb': z.coerce.number().min(1),
  49. 'performance_setting.disk_cache_max_size_mb': z.coerce.number().min(100),
  50. 'performance_setting.disk_cache_path': z.string().optional(),
  51. 'performance_setting.monitor_enabled': z.boolean(),
  52. 'performance_setting.monitor_cpu_threshold': z.coerce.number().min(0),
  53. 'performance_setting.monitor_memory_threshold': z.coerce
  54. .number()
  55. .min(0)
  56. .max(100),
  57. 'performance_setting.monitor_disk_threshold': z.coerce
  58. .number()
  59. .min(0)
  60. .max(100),
  61. 'perf_metrics_setting.enabled': z.boolean(),
  62. 'perf_metrics_setting.flush_interval': z.coerce.number().min(1),
  63. 'perf_metrics_setting.bucket_time': z.enum(['minute', '5min', 'hour']),
  64. 'perf_metrics_setting.retention_days': z.coerce.number().min(0),
  65. })
  66. type PerfFormValues = z.infer<typeof perfSchema>
  67. function formatBytes(bytes: number, decimals = 2): string {
  68. if (!bytes || isNaN(bytes)) return '0 Bytes'
  69. if (bytes === 0) return '0 Bytes'
  70. if (bytes < 0) return '-' + formatBytes(-bytes, decimals)
  71. const k = 1024
  72. const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
  73. const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k))
  74. if (i < 0 || i >= sizes.length) return bytes + ' Bytes'
  75. return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]
  76. }
  77. interface Props {
  78. defaultValues: PerfFormValues
  79. }
  80. type LogInfo = {
  81. enabled: boolean
  82. log_dir: string
  83. file_count: number
  84. total_size: number
  85. oldest_time?: string
  86. newest_time?: string
  87. }
  88. type PerformanceStats = {
  89. cache_stats?: {
  90. current_disk_usage_bytes: number
  91. disk_cache_max_bytes: number
  92. active_disk_files: number
  93. disk_cache_hits: number
  94. current_memory_usage_bytes: number
  95. active_memory_buffers: number
  96. memory_cache_hits: number
  97. }
  98. disk_space_info?: {
  99. total: number
  100. free: number
  101. used: number
  102. used_percent: number
  103. }
  104. memory_stats?: {
  105. alloc: number
  106. total_alloc: number
  107. sys: number
  108. num_gc: number
  109. num_goroutine: number
  110. }
  111. disk_cache_info?: {
  112. path: string
  113. file_count: number
  114. total_size: number
  115. }
  116. config?: {
  117. is_running_in_container: boolean
  118. }
  119. }
  120. export function PerformanceSection(props: Props) {
  121. const { t } = useTranslation()
  122. const updateOption = useUpdateOption()
  123. const [stats, setStats] = useState<PerformanceStats | null>(null)
  124. const [logInfo, setLogInfo] = useState<LogInfo | null>(null)
  125. const [logCleanupMode, setLogCleanupMode] = useState('by_count')
  126. const [logCleanupValue, setLogCleanupValue] = useState(10)
  127. const [logCleanupLoading, setLogCleanupLoading] = useState(false)
  128. const form = useForm<PerfFormValues>({
  129. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  130. resolver: zodResolver(perfSchema) as any,
  131. defaultValues: props.defaultValues,
  132. })
  133. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  134. useResetForm(form as any, props.defaultValues)
  135. const fetchStats = useCallback(async () => {
  136. try {
  137. const res = await api.get('/api/performance/stats')
  138. if (res.data.success) setStats(res.data.data)
  139. } catch {
  140. /* ignore */
  141. }
  142. }, [])
  143. const fetchLogInfo = useCallback(async () => {
  144. try {
  145. const res = await api.get('/api/performance/logs')
  146. if (res.data.success) setLogInfo(res.data.data)
  147. } catch {
  148. /* ignore */
  149. }
  150. }, [])
  151. useEffect(() => {
  152. fetchStats()
  153. fetchLogInfo()
  154. }, [fetchStats, fetchLogInfo])
  155. const onSubmit = async (data: PerfFormValues) => {
  156. const entries = Object.entries(data) as [string, unknown][]
  157. const updates = entries.filter(
  158. ([key, value]) =>
  159. value !== (props.defaultValues[key as keyof PerfFormValues] as unknown)
  160. )
  161. if (updates.length === 0) {
  162. toast.info(t('No changes to save'))
  163. return
  164. }
  165. for (const [key, value] of updates) {
  166. await updateOption.mutateAsync({
  167. key,
  168. value: value as string | number | boolean,
  169. })
  170. }
  171. toast.success(t('Saved successfully'))
  172. fetchStats()
  173. }
  174. const clearDiskCache = async () => {
  175. try {
  176. const res = await api.delete('/api/performance/disk_cache')
  177. if (res.data.success) {
  178. toast.success(t('Disk cache cleared'))
  179. fetchStats()
  180. }
  181. } catch {
  182. toast.error(t('Cleanup failed'))
  183. }
  184. }
  185. const resetStats = async () => {
  186. try {
  187. const res = await api.post('/api/performance/reset_stats')
  188. if (res.data.success) {
  189. toast.success(t('Statistics reset'))
  190. fetchStats()
  191. }
  192. } catch {
  193. toast.error(t('Reset failed'))
  194. }
  195. }
  196. const forceGC = async () => {
  197. try {
  198. const res = await api.post('/api/performance/gc')
  199. if (res.data.success) {
  200. toast.success(t('GC executed'))
  201. fetchStats()
  202. }
  203. } catch {
  204. toast.error(t('GC execution failed'))
  205. }
  206. }
  207. const cleanupLogFiles = async () => {
  208. if (!logCleanupValue || isNaN(logCleanupValue) || logCleanupValue < 1) {
  209. toast.error(t('Please enter a valid number'))
  210. return
  211. }
  212. setLogCleanupLoading(true)
  213. try {
  214. const res = await api.delete(
  215. `/api/performance/logs?mode=${logCleanupMode}&value=${logCleanupValue}`
  216. )
  217. if (res.data.success) {
  218. const { deleted_count, freed_bytes } = res.data.data
  219. toast.success(
  220. t('Cleaned up {{count}} log files, freed {{size}}', {
  221. count: deleted_count,
  222. size: formatBytes(freed_bytes),
  223. })
  224. )
  225. } else {
  226. toast.error(res.data.message || t('Cleanup failed'))
  227. }
  228. fetchLogInfo()
  229. } catch {
  230. toast.error(t('Cleanup failed'))
  231. } finally {
  232. setLogCleanupLoading(false)
  233. }
  234. }
  235. const diskEnabled = form.watch('performance_setting.disk_cache_enabled')
  236. const monitorEnabled = form.watch('performance_setting.monitor_enabled')
  237. const perfMetricsEnabled = form.watch('perf_metrics_setting.enabled')
  238. const maxCacheSizeMb = form.watch(
  239. 'performance_setting.disk_cache_max_size_mb'
  240. )
  241. const lowDiskSpace =
  242. diskEnabled &&
  243. stats?.disk_space_info &&
  244. stats.disk_space_info.free > 0 &&
  245. maxCacheSizeMb > 0 &&
  246. stats.disk_space_info.free < maxCacheSizeMb * 1024 * 1024
  247. const diskCachePercent =
  248. stats?.cache_stats?.disk_cache_max_bytes &&
  249. stats.cache_stats.disk_cache_max_bytes > 0
  250. ? Math.round(
  251. (stats.cache_stats.current_disk_usage_bytes /
  252. stats.cache_stats.disk_cache_max_bytes) *
  253. 100
  254. )
  255. : 0
  256. return (
  257. <SettingsSection
  258. title={t('Performance Settings')}
  259. description={t(
  260. 'Disk cache, system performance monitoring, and operation statistics'
  261. )}
  262. >
  263. <Form {...form}>
  264. <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
  265. {/* Disk Cache Settings */}
  266. <div>
  267. <h4 className='font-medium'>{t('Disk Cache Settings')}</h4>
  268. <p className='text-muted-foreground mt-1 text-xs'>
  269. {t(
  270. 'When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.'
  271. )}
  272. </p>
  273. </div>
  274. <div className='grid grid-cols-1 gap-4 md:grid-cols-3'>
  275. <FormField
  276. control={form.control}
  277. name='performance_setting.disk_cache_enabled'
  278. render={({ field }) => (
  279. <FormItem className='flex items-center gap-2'>
  280. <FormControl>
  281. <Switch
  282. checked={field.value}
  283. onCheckedChange={field.onChange}
  284. />
  285. </FormControl>
  286. <FormLabel>{t('Enable Disk Cache')}</FormLabel>
  287. </FormItem>
  288. )}
  289. />
  290. <FormField
  291. control={form.control}
  292. name='performance_setting.disk_cache_threshold_mb'
  293. render={({ field }) => (
  294. <FormItem>
  295. <FormLabel>{t('Disk Cache Threshold (MB)')}</FormLabel>
  296. <FormControl>
  297. <Input type='number' {...field} disabled={!diskEnabled} />
  298. </FormControl>
  299. <FormDescription>
  300. {t('Use disk cache when request body exceeds this size')}
  301. </FormDescription>
  302. </FormItem>
  303. )}
  304. />
  305. <FormField
  306. control={form.control}
  307. name='performance_setting.disk_cache_max_size_mb'
  308. render={({ field }) => (
  309. <FormItem>
  310. <FormLabel>{t('Max Disk Cache Size (MB)')}</FormLabel>
  311. <FormControl>
  312. <Input type='number' {...field} disabled={!diskEnabled} />
  313. </FormControl>
  314. {stats?.disk_space_info &&
  315. stats.disk_space_info.total > 0 && (
  316. <FormDescription>
  317. {t('Free: {{free}} / Total: {{total}}', {
  318. free: formatBytes(stats.disk_space_info.free),
  319. total: formatBytes(stats.disk_space_info.total),
  320. })}
  321. </FormDescription>
  322. )}
  323. </FormItem>
  324. )}
  325. />
  326. </div>
  327. {lowDiskSpace && (
  328. <Alert variant='destructive'>
  329. <AlertDescription>
  330. {`${t('Warning')}: ${t('Available disk space')} (${formatBytes(stats?.disk_space_info?.free ?? 0)}) ${t('is less than the configured maximum cache size')} (${maxCacheSizeMb} MB). ${t('This may cause cache failures.')}`}
  331. </AlertDescription>
  332. </Alert>
  333. )}
  334. {!stats?.config?.is_running_in_container && (
  335. <FormField
  336. control={form.control}
  337. name='performance_setting.disk_cache_path'
  338. render={({ field }) => (
  339. <FormItem className='max-w-md'>
  340. <FormLabel>{t('Cache Directory')}</FormLabel>
  341. <FormControl>
  342. <Input
  343. placeholder={t(
  344. 'Leave empty to use system temp directory'
  345. )}
  346. {...field}
  347. value={field.value ?? ''}
  348. disabled={!diskEnabled}
  349. />
  350. </FormControl>
  351. </FormItem>
  352. )}
  353. />
  354. )}
  355. <Separator />
  356. {/* System Performance Monitor */}
  357. <div>
  358. <h4 className='font-medium'>
  359. {t('System Performance Monitoring')}
  360. </h4>
  361. <p className='text-muted-foreground mt-1 text-xs'>
  362. {t(
  363. 'When performance monitoring is enabled and system resource usage exceeds the set threshold, new Relay requests will be rejected.'
  364. )}
  365. </p>
  366. </div>
  367. <div className='grid grid-cols-1 gap-4 md:grid-cols-4'>
  368. <FormField
  369. control={form.control}
  370. name='performance_setting.monitor_enabled'
  371. render={({ field }) => (
  372. <FormItem className='flex items-center gap-2'>
  373. <FormControl>
  374. <Switch
  375. checked={field.value}
  376. onCheckedChange={field.onChange}
  377. />
  378. </FormControl>
  379. <FormLabel>{t('Enable Performance Monitoring')}</FormLabel>
  380. </FormItem>
  381. )}
  382. />
  383. <FormField
  384. control={form.control}
  385. name='performance_setting.monitor_cpu_threshold'
  386. render={({ field }) => (
  387. <FormItem>
  388. <FormLabel>{t('CPU Threshold (%)')}</FormLabel>
  389. <FormControl>
  390. <Input
  391. type='number'
  392. {...field}
  393. disabled={!monitorEnabled}
  394. />
  395. </FormControl>
  396. </FormItem>
  397. )}
  398. />
  399. <FormField
  400. control={form.control}
  401. name='performance_setting.monitor_memory_threshold'
  402. render={({ field }) => (
  403. <FormItem>
  404. <FormLabel>{t('Memory Threshold (%)')}</FormLabel>
  405. <FormControl>
  406. <Input
  407. type='number'
  408. {...field}
  409. disabled={!monitorEnabled}
  410. />
  411. </FormControl>
  412. </FormItem>
  413. )}
  414. />
  415. <FormField
  416. control={form.control}
  417. name='performance_setting.monitor_disk_threshold'
  418. render={({ field }) => (
  419. <FormItem>
  420. <FormLabel>{t('Disk Threshold (%)')}</FormLabel>
  421. <FormControl>
  422. <Input
  423. type='number'
  424. {...field}
  425. disabled={!monitorEnabled}
  426. />
  427. </FormControl>
  428. </FormItem>
  429. )}
  430. />
  431. </div>
  432. <Separator />
  433. <div>
  434. <h4 className='font-medium'>{t('Model performance metrics')}</h4>
  435. <p className='text-muted-foreground mt-1 text-xs'>
  436. {t(
  437. 'Collect relay latency and success-rate metrics for the model square.'
  438. )}
  439. </p>
  440. </div>
  441. <div className='grid grid-cols-1 gap-4 md:grid-cols-4'>
  442. <FormField
  443. control={form.control}
  444. name='perf_metrics_setting.enabled'
  445. render={({ field }) => (
  446. <FormItem className='flex items-center gap-2'>
  447. <FormControl>
  448. <Switch
  449. checked={field.value}
  450. onCheckedChange={field.onChange}
  451. />
  452. </FormControl>
  453. <FormLabel>{t('Enable model performance metrics')}</FormLabel>
  454. </FormItem>
  455. )}
  456. />
  457. <FormField
  458. control={form.control}
  459. name='perf_metrics_setting.flush_interval'
  460. render={({ field }) => (
  461. <FormItem>
  462. <FormLabel>{t('Flush interval (minutes)')}</FormLabel>
  463. <FormControl>
  464. <Input
  465. type='number'
  466. min={1}
  467. {...field}
  468. disabled={!perfMetricsEnabled}
  469. />
  470. </FormControl>
  471. </FormItem>
  472. )}
  473. />
  474. <FormField
  475. control={form.control}
  476. name='perf_metrics_setting.bucket_time'
  477. render={({ field }) => (
  478. <FormItem>
  479. <FormLabel>{t('Aggregation bucket')}</FormLabel>
  480. <Select
  481. value={field.value}
  482. onValueChange={field.onChange}
  483. disabled={!perfMetricsEnabled}
  484. >
  485. <FormControl>
  486. <SelectTrigger>
  487. <SelectValue />
  488. </SelectTrigger>
  489. </FormControl>
  490. <SelectContent>
  491. <SelectItem value='minute'>{t('1 minute')}</SelectItem>
  492. <SelectItem value='5min'>{t('5 minutes')}</SelectItem>
  493. <SelectItem value='hour'>{t('1 hour')}</SelectItem>
  494. </SelectContent>
  495. </Select>
  496. </FormItem>
  497. )}
  498. />
  499. <FormField
  500. control={form.control}
  501. name='perf_metrics_setting.retention_days'
  502. render={({ field }) => (
  503. <FormItem>
  504. <FormLabel>{t('Retention days')}</FormLabel>
  505. <FormControl>
  506. <Input
  507. type='number'
  508. min={0}
  509. {...field}
  510. disabled={!perfMetricsEnabled}
  511. />
  512. </FormControl>
  513. <FormDescription>
  514. {t('0 means data is kept permanently')}
  515. </FormDescription>
  516. </FormItem>
  517. )}
  518. />
  519. </div>
  520. <Button type='submit' disabled={updateOption.isPending}>
  521. {updateOption.isPending ? t('Saving...') : t('Save Changes')}
  522. </Button>
  523. </form>
  524. </Form>
  525. <Separator />
  526. {/* Server Log Management */}
  527. <div className='space-y-4'>
  528. <div>
  529. <h4 className='font-medium'>{t('Server Log Management')}</h4>
  530. <p className='text-muted-foreground mt-1 text-xs'>
  531. {t(
  532. 'Manage server log files. Log files accumulate over time; regular cleanup is recommended to free disk space.'
  533. )}
  534. </p>
  535. </div>
  536. {logInfo === null ? null : logInfo.enabled ? (
  537. <div className='space-y-4'>
  538. <div className='rounded-lg border p-4'>
  539. <div className='grid grid-cols-2 gap-2 text-sm md:grid-cols-4'>
  540. <div>
  541. <span className='text-muted-foreground'>
  542. {t('Log Directory')}:
  543. </span>{' '}
  544. <span className='font-mono text-xs'>{logInfo.log_dir}</span>
  545. </div>
  546. <div>
  547. <span className='text-muted-foreground'>
  548. {t('Log File Count')}:
  549. </span>{' '}
  550. {logInfo.file_count}
  551. </div>
  552. <div>
  553. <span className='text-muted-foreground'>
  554. {t('Total Log Size')}:
  555. </span>{' '}
  556. {formatBytes(logInfo.total_size)}
  557. </div>
  558. {logInfo.oldest_time && logInfo.newest_time && (
  559. <div>
  560. <span className='text-muted-foreground'>
  561. {t('Date Range')}:
  562. </span>{' '}
  563. {dayjs(logInfo.oldest_time).format('YYYY-MM-DD')} ~{' '}
  564. {dayjs(logInfo.newest_time).format('YYYY-MM-DD')}
  565. </div>
  566. )}
  567. </div>
  568. </div>
  569. <div className='flex flex-wrap items-end gap-3'>
  570. <div className='grid gap-1.5'>
  571. <Label className='text-xs'>{t('Cleanup Mode')}</Label>
  572. <Select
  573. value={logCleanupMode}
  574. onValueChange={(v) => v !== null && setLogCleanupMode(v)}
  575. >
  576. <SelectTrigger className='w-[160px]'>
  577. <SelectValue />
  578. </SelectTrigger>
  579. <SelectContent>
  580. <SelectItem value='by_count'>
  581. {t('Retain last N files')}
  582. </SelectItem>
  583. <SelectItem value='by_days'>
  584. {t('Retain last N days')}
  585. </SelectItem>
  586. </SelectContent>
  587. </Select>
  588. </div>
  589. <div className='grid gap-1.5'>
  590. <Label className='text-xs'>
  591. {logCleanupMode === 'by_count'
  592. ? t('Files to Retain')
  593. : t('Days to Retain')}
  594. </Label>
  595. <Input
  596. type='number'
  597. min={1}
  598. max={logCleanupMode === 'by_count' ? 1000 : 3650}
  599. value={logCleanupValue}
  600. onChange={(e) => setLogCleanupValue(Number(e.target.value))}
  601. className='w-[120px]'
  602. />
  603. </div>
  604. <AlertDialog>
  605. <AlertDialogTrigger
  606. render={
  607. <Button
  608. variant='destructive'
  609. size='sm'
  610. disabled={logCleanupLoading}
  611. />
  612. }
  613. >
  614. {logCleanupLoading
  615. ? t('Cleaning...')
  616. : t('Clean Up Log Files')}
  617. </AlertDialogTrigger>
  618. <AlertDialogContent>
  619. <AlertDialogHeader>
  620. <AlertDialogTitle>
  621. {t('Confirm log file cleanup?')}
  622. </AlertDialogTitle>
  623. <AlertDialogDescription>
  624. {logCleanupMode === 'by_count'
  625. ? t(
  626. 'Only the last {{value}} log files will be retained; the rest will be deleted.',
  627. {
  628. value: logCleanupValue,
  629. }
  630. )
  631. : t(
  632. 'Log files older than {{value}} days will be deleted.',
  633. {
  634. value: logCleanupValue,
  635. }
  636. )}
  637. </AlertDialogDescription>
  638. </AlertDialogHeader>
  639. <AlertDialogFooter>
  640. <AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
  641. <AlertDialogAction onClick={cleanupLogFiles}>
  642. {t('Confirm Cleanup')}
  643. </AlertDialogAction>
  644. </AlertDialogFooter>
  645. </AlertDialogContent>
  646. </AlertDialog>
  647. </div>
  648. </div>
  649. ) : (
  650. <Alert>
  651. <AlertDescription>
  652. {t(
  653. 'Server logging is not enabled (log directory not configured)'
  654. )}
  655. </AlertDescription>
  656. </Alert>
  657. )}
  658. </div>
  659. <Separator />
  660. {/* Performance Stats Dashboard */}
  661. <div className='space-y-4'>
  662. <div className='flex items-center gap-2'>
  663. <h4 className='font-medium'>{t('Performance Monitor')}</h4>
  664. <Button variant='outline' size='sm' onClick={fetchStats}>
  665. {t('Refresh Stats')}
  666. </Button>
  667. <AlertDialog>
  668. <AlertDialogTrigger render={<Button variant='outline' size='sm' />}>
  669. {t('Clean up inactive cache')}
  670. </AlertDialogTrigger>
  671. <AlertDialogContent>
  672. <AlertDialogHeader>
  673. <AlertDialogTitle>
  674. {t('Confirm cleanup of inactive disk cache?')}
  675. </AlertDialogTitle>
  676. <AlertDialogDescription>
  677. {t(
  678. 'This will delete temporary cache files that have not been used for more than 10 minutes'
  679. )}
  680. </AlertDialogDescription>
  681. </AlertDialogHeader>
  682. <AlertDialogFooter>
  683. <AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
  684. <AlertDialogAction onClick={clearDiskCache}>
  685. {t('Confirm')}
  686. </AlertDialogAction>
  687. </AlertDialogFooter>
  688. </AlertDialogContent>
  689. </AlertDialog>
  690. <Button variant='outline' size='sm' onClick={resetStats}>
  691. {t('Reset Stats')}
  692. </Button>
  693. <Button variant='outline' size='sm' onClick={forceGC}>
  694. {t('Run GC')}
  695. </Button>
  696. </div>
  697. {stats && (
  698. <>
  699. <div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
  700. <div className='space-y-2 rounded-lg border p-4'>
  701. <p className='text-sm font-medium'>
  702. {t('Request Body Disk Cache')}
  703. </p>
  704. <Progress value={diskCachePercent} />
  705. <div className='text-muted-foreground flex justify-between text-xs'>
  706. <span>
  707. {formatBytes(
  708. stats.cache_stats?.current_disk_usage_bytes ?? 0
  709. )}{' '}
  710. /{' '}
  711. {formatBytes(stats.cache_stats?.disk_cache_max_bytes ?? 0)}
  712. </span>
  713. <span>
  714. {t('Active Files')}:{' '}
  715. {stats.cache_stats?.active_disk_files ?? 0}
  716. </span>
  717. </div>
  718. <StatusBadge variant='neutral' copyable={false}>
  719. {t('Disk Hits')}: {stats.cache_stats?.disk_cache_hits ?? 0}
  720. </StatusBadge>
  721. </div>
  722. <div className='space-y-2 rounded-lg border p-4'>
  723. <p className='text-sm font-medium'>
  724. {t('Request Body Memory Cache')}
  725. </p>
  726. <div className='text-muted-foreground flex justify-between text-xs'>
  727. <span>
  728. {t('Current Cache Size')}:{' '}
  729. {formatBytes(
  730. stats.cache_stats?.current_memory_usage_bytes ?? 0
  731. )}
  732. </span>
  733. <span>
  734. {t('Active Cache Count')}:{' '}
  735. {stats.cache_stats?.active_memory_buffers ?? 0}
  736. </span>
  737. </div>
  738. <StatusBadge variant='neutral' copyable={false}>
  739. {t('Memory Hits')}:{' '}
  740. {stats.cache_stats?.memory_cache_hits ?? 0}
  741. </StatusBadge>
  742. </div>
  743. </div>
  744. {stats.disk_space_info && stats.disk_space_info.total > 0 && (
  745. <div className='rounded-lg border p-4'>
  746. <p className='mb-2 text-sm font-medium'>
  747. {t('Cache Directory Disk Space')}
  748. </p>
  749. <Progress
  750. value={Math.round(stats.disk_space_info.used_percent)}
  751. />
  752. <div className='text-muted-foreground mt-2 flex justify-between text-xs'>
  753. <span>
  754. {t('Used')}: {formatBytes(stats.disk_space_info.used)}
  755. </span>
  756. <span>
  757. {t('Available')}: {formatBytes(stats.disk_space_info.free)}
  758. </span>
  759. <span>
  760. {t('Total')}: {formatBytes(stats.disk_space_info.total)}
  761. </span>
  762. </div>
  763. </div>
  764. )}
  765. {stats.memory_stats && (
  766. <div className='rounded-lg border p-4'>
  767. <p className='mb-2 text-sm font-medium'>
  768. {t('System Memory Stats')}
  769. </p>
  770. <div className='grid grid-cols-2 gap-2 text-xs md:grid-cols-5'>
  771. <div>
  772. <span className='text-muted-foreground'>
  773. {t('Allocated Memory')}:
  774. </span>{' '}
  775. {formatBytes(stats.memory_stats.alloc)}
  776. </div>
  777. <div>
  778. <span className='text-muted-foreground'>
  779. {t('Total Allocated')}:
  780. </span>{' '}
  781. {formatBytes(stats.memory_stats.total_alloc)}
  782. </div>
  783. <div>
  784. <span className='text-muted-foreground'>
  785. {t('System Memory')}:
  786. </span>{' '}
  787. {formatBytes(stats.memory_stats.sys)}
  788. </div>
  789. <div>
  790. <span className='text-muted-foreground'>
  791. {t('GC Count')}:
  792. </span>{' '}
  793. {stats.memory_stats.num_gc}
  794. </div>
  795. <div>
  796. <span className='text-muted-foreground'>Goroutines:</span>{' '}
  797. {stats.memory_stats.num_goroutine}
  798. </div>
  799. </div>
  800. </div>
  801. )}
  802. {stats.disk_cache_info && (
  803. <div className='rounded-lg border p-4'>
  804. <p className='mb-2 text-sm font-medium'>
  805. {t('Cache Directory Info')}
  806. </p>
  807. <div className='grid grid-cols-3 gap-2 text-xs'>
  808. <div>
  809. <span className='text-muted-foreground'>
  810. {t('Cache Directory')}:
  811. </span>{' '}
  812. <span className='font-mono'>
  813. {stats.disk_cache_info.path}
  814. </span>
  815. </div>
  816. <div>
  817. <span className='text-muted-foreground'>
  818. {t('Directory File Count')}:
  819. </span>{' '}
  820. {stats.disk_cache_info.file_count}
  821. </div>
  822. <div>
  823. <span className='text-muted-foreground'>
  824. {t('Directory Total Size')}:
  825. </span>{' '}
  826. {formatBytes(stats.disk_cache_info.total_size)}
  827. </div>
  828. </div>
  829. </div>
  830. )}
  831. </>
  832. )}
  833. </div>
  834. </SettingsSection>
  835. )
  836. }