diff --git a/internal/apps/benchmark/init.go b/internal/apps/benchmark/init.go new file mode 100644 index 0000000000..46d98a4785 --- /dev/null +++ b/internal/apps/benchmark/init.go @@ -0,0 +1,18 @@ +package benchmark + +import ( + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/apploader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + apploader.Register(&types.App{ + Slug: "benchmark", + Route: func(r chi.Router) { + service := NewService() + r.Post("/test", service.Test) + }, + }) +} diff --git a/internal/apps/benchmark/request.go b/internal/apps/benchmark/request.go new file mode 100644 index 0000000000..3aa37e67a4 --- /dev/null +++ b/internal/apps/benchmark/request.go @@ -0,0 +1,6 @@ +package benchmark + +type Test struct { + Name string `json:"name" validate:"required,oneof=image machine compile encryption compression physics json memory disk"` + Multi bool `json:"multi"` +} diff --git a/internal/apps/benchmark/service.go b/internal/apps/benchmark/service.go new file mode 100644 index 0000000000..63d5d63fe9 --- /dev/null +++ b/internal/apps/benchmark/service.go @@ -0,0 +1,686 @@ +package benchmark + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + cryptorand "crypto/rand" + "encoding/json" + "fmt" + "image" + "image/color" + "io" + "math" + "math/big" + "math/rand/v2" + "net/http" + "os" + "runtime" + "strings" + "sync" + "time" + + "github.com/klauspost/compress/zstd" + + "github.com/TheTNB/panel/internal/service" +) + +type Service struct { +} + +func NewService() *Service { + return &Service{} +} + +// Test 运行测试 +func (s *Service) Test(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[Test](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + switch req.Name { + case "image": + result := s.imageProcessing(req.Multi) + service.Success(w, result) + case "machine": + result := s.machineLearning(req.Multi) + service.Success(w, result) + case "compile": + result := s.compileSimulationSingle(req.Multi) + service.Success(w, result) + case "encryption": + result := s.encryptionTest(req.Multi) + service.Success(w, result) + case "compression": + result := s.compressionTest(req.Multi) + service.Success(w, result) + case "physics": + result := s.physicsSimulation(req.Multi) + service.Success(w, result) + case "json": + result := s.jsonProcessing(req.Multi) + service.Success(w, result) + case "disk": + result := s.diskTestTask() + service.Success(w, result) + case "memory": + result := s.memoryTestTask() + service.Success(w, result) + default: + service.Error(w, http.StatusUnprocessableEntity, "未知测试类型") + } +} + +// calculateCpuScore 计算CPU成绩 +func (s *Service) calculateCpuScore(duration time.Duration) int { + score := int((10 / duration.Seconds()) * float64(3000)) + + if score < 0 { + score = 0 + } + return score +} + +// calculateScore 计算内存/硬盘成绩 +func (s *Service) calculateScore(duration time.Duration) int { + score := int((10 / duration.Seconds()) * float64(10000)) + + if score < 0 { + score = 0 + } + return score +} + +// 图像处理 + +func (s *Service) imageProcessing(multi bool) int { + n := 1 + if multi { + n = runtime.NumCPU() + } + start := time.Now() + if err := s.imageProcessingTask(n); err != nil { + return 0 + } + duration := time.Since(start) + return s.calculateCpuScore(duration) +} + +func (s *Service) imageProcessingTask(numThreads int) error { + img := image.NewRGBA(image.Rect(0, 0, 4000, 4000)) + for x := 0; x < 4000; x++ { + for y := 0; y < 4000; y++ { + img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), A: 255}) + } + } + + var wg sync.WaitGroup + dx := img.Bounds().Dx() + dy := img.Bounds().Dy() + chunkSize := dy / numThreads + + for i := 0; i < numThreads; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + startY := i * chunkSize + endY := startY + chunkSize + if i == numThreads-1 { + endY = dy + } + for x := 1; x < dx-1; x++ { + for y := startY + 1; y < endY-1; y++ { + // 卷积操作(模糊) + rTotal, gTotal, bTotal := 0, 0, 0 + for k := -1; k <= 1; k++ { + for l := -1; l <= 1; l++ { + r, g, b, _ := img.At(x+k, y+l).RGBA() + rTotal += int(r) + gTotal += int(g) + bTotal += int(b) + } + } + rAvg := uint8(rTotal / 9 / 256) + gAvg := uint8(gTotal / 9 / 256) + bAvg := uint8(bTotal / 9 / 256) + img.Set(x, y, color.RGBA{R: rAvg, G: gAvg, B: bAvg, A: 255}) + } + } + }(i) + } + + wg.Wait() + return nil +} + +// 机器学习(矩阵乘法) + +func (s *Service) machineLearning(multi bool) int { + n := 1 + if multi { + n = runtime.NumCPU() + } + start := time.Now() + s.machineLearningTask(n) + duration := time.Since(start) + return s.calculateCpuScore(duration) +} + +func (s *Service) machineLearningTask(numThreads int) { + size := 850 + a := make([][]float64, size) + b := make([][]float64, size) + for i := 0; i < size; i++ { + a[i] = make([]float64, size) + b[i] = make([]float64, size) + for j := 0; j < size; j++ { + a[i][j] = rand.Float64() + b[i][j] = rand.Float64() + } + } + + c := make([][]float64, size) + for i := 0; i < size; i++ { + c[i] = make([]float64, size) + } + + var wg sync.WaitGroup + chunkSize := size / numThreads + + for k := 0; k < numThreads; k++ { + wg.Add(1) + go func(k int) { + defer wg.Done() + start := k * chunkSize + end := start + chunkSize + if k == numThreads-1 { + end = size + } + for i := start; i < end; i++ { + for j := 0; j < size; j++ { + sum := 0.0 + for l := 0; l < size; l++ { + sum += a[i][l] * b[l][j] + } + c[i][j] = sum + } + } + }(k) + } + + wg.Wait() +} + +// 数学问题(计算斐波那契数) + +func (s *Service) compileSimulationSingle(multi bool) int { + n := 1 + if multi { + n = runtime.NumCPU() + } + start := time.Now() + totalCalculations := 1000 + fibNumber := 20000 + + calculationsPerThread := totalCalculations / n + remainder := totalCalculations % n + + var wg sync.WaitGroup + + for i := 0; i < n; i++ { + tasks := calculationsPerThread + if i < remainder { + tasks++ // 处理无法均分的剩余任务 + } + wg.Add(1) + go func(tasks int) { + defer wg.Done() + for j := 0; j < tasks; j++ { + s.fib(fibNumber) + } + }(tasks) + } + + wg.Wait() + duration := time.Since(start) + return s.calculateCpuScore(duration) +} + +// 斐波那契函数 +func (s *Service) fib(n int) *big.Int { + if n < 2 { + return big.NewInt(int64(n)) + } + a := big.NewInt(0) + b := big.NewInt(1) + temp := big.NewInt(0) + for i := 2; i <= n; i++ { + temp.Add(a, b) + a.Set(b) + b.Set(temp) + } + return b +} + +// AES加密 + +func (s *Service) encryptionTest(multi bool) int { + n := 1 + if multi { + n = runtime.NumCPU() + } + start := time.Now() + if err := s.encryptionTestTask(n); err != nil { + return 0 + } + duration := time.Since(start) + return s.calculateCpuScore(duration) +} + +func (s *Service) encryptionTestTask(numThreads int) error { + key := []byte("abcdefghijklmnopqrstuvwxyz123456") + dataSize := 1 * 1024 * 1024 * 1024 // 1GB + plaintext := []byte(strings.Repeat("A", dataSize)) + block, err := aes.NewCipher(key) + if err != nil { + return err + } + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return err + } + + chunkSize := dataSize / numThreads + + var wg sync.WaitGroup + + for i := 0; i < numThreads; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + start := i * chunkSize + end := start + chunkSize + if i == numThreads-1 { + end = dataSize + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err = cryptorand.Read(nonce); err != nil { + return + } + + aesGCM.Seal(nil, nonce, plaintext[start:end], nil) + }(i) + } + + wg.Wait() + return nil +} + +// 压缩/解压缩 + +func (s *Service) compressionTest(multi bool) int { + n := 1 + if multi { + n = runtime.NumCPU() + } + start := time.Now() + s.compressionTestTask(n) + duration := time.Since(start) + return s.calculateCpuScore(duration) +} + +func (s *Service) compressionTestTask(numThreads int) { + data := []byte(strings.Repeat("耗子面板", 50000000)) + chunkSize := len(data) / numThreads + + var wg sync.WaitGroup + + compressedChunks := make([]bytes.Buffer, numThreads) + + // 压缩 + for i := 0; i < numThreads; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + start := i * chunkSize + end := start + chunkSize + if i == numThreads-1 { + end = len(data) + } + var buf bytes.Buffer + zw, _ := zstd.NewWriter(&buf) + _, _ = zw.Write(data[start:end]) + _ = zw.Close() + compressedChunks[i] = buf + }(i) + } + + wg.Wait() + + // 解压缩 + for i := 0; i < numThreads; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + zr, err := zstd.NewReader(&compressedChunks[i]) + if err != nil { + return + } + _, err = io.Copy(io.Discard, zr) + if err != nil { + return + } + zr.Close() + }(i) + } + + wg.Wait() +} + +// 物理仿真(N体问题) + +func (s *Service) physicsSimulation(multi bool) int { + n := 1 + if multi { + n = runtime.NumCPU() + } + start := time.Now() + s.physicsSimulationTask(n) + duration := time.Since(start) + return s.calculateCpuScore(duration) +} + +func (s *Service) physicsSimulationTask(numThreads int) { + const ( + numBodies = 4000 + steps = 30 + ) + + type Body struct { + x, y, z, vx, vy, vz float64 + } + + bodies := make([]Body, numBodies) + for i := 0; i < numBodies; i++ { + bodies[i] = Body{ + x: rand.Float64(), + y: rand.Float64(), + z: rand.Float64(), + vx: rand.Float64(), + vy: rand.Float64(), + vz: rand.Float64(), + } + } + + chunkSize := numBodies / numThreads + + for step := 0; step < steps; step++ { + var wg sync.WaitGroup + + // 更新速度 + for k := 0; k < numThreads; k++ { + wg.Add(1) + go func(k int) { + defer wg.Done() + start := k * chunkSize + end := start + chunkSize + if k == numThreads-1 { + end = numBodies + } + for i := start; i < end; i++ { + bi := &bodies[i] + for j := 0; j < numBodies; j++ { + if i == j { + continue + } + bj := &bodies[j] + dx := bj.x - bi.x + dy := bj.y - bi.y + dz := bj.z - bi.z + dist := math.Sqrt(dx*dx + dy*dy + dz*dz) + if dist == 0 { + continue + } + force := 1 / (dist * dist) + bi.vx += force * dx / dist + bi.vy += force * dy / dist + bi.vz += force * dz / dist + } + } + }(k) + } + + wg.Wait() + + // 更新位置 + for k := 0; k < numThreads; k++ { + wg.Add(1) + go func(k int) { + defer wg.Done() + start := k * chunkSize + end := start + chunkSize + if k == numThreads-1 { + end = numBodies + } + for i := start; i < end; i++ { + bi := &bodies[i] + bi.x += bi.vx + bi.y += bi.vy + bi.z += bi.vz + } + }(k) + } + + wg.Wait() + } +} + +// JSON解析 + +func (s *Service) jsonProcessing(multi bool) int { + n := 1 + if multi { + n = runtime.NumCPU() + } + start := time.Now() + s.jsonProcessingTask(n) + duration := time.Since(start) + return s.calculateCpuScore(duration) +} + +func (s *Service) jsonProcessingTask(numThreads int) { + numElements := 1000000 + elementsPerThread := numElements / numThreads + + var wg sync.WaitGroup + for i := 0; i < numThreads; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + + start := i * elementsPerThread + end := start + elementsPerThread + if i == numThreads-1 { + end = numElements + } + + elements := make([]map[string]any, 0, end-start) + for j := start; j < end; j++ { + elements = append(elements, map[string]any{ + "id": j, + "value": fmt.Sprintf("Value%d", j), + }) + } + encoded, err := json.Marshal(elements) + if err != nil { + return + } + + var parsed []map[string]any + err = json.Unmarshal(encoded, &parsed) + if err != nil { + return + } + }(i) + } + + wg.Wait() +} + +// 内存性能 + +func (s *Service) memoryTestTask() map[string]any { + results := make(map[string]any) + dataSize := 500 * 1024 * 1024 // 500 MB + data := make([]byte, dataSize) + _, _ = cryptorand.Read(data) + + start := time.Now() + // 内存读写速度 + results["bandwidth"] = s.memoryBandwidthTest(data) + // 内存访问延迟 + data = data[:100*1024*1024] // 100 MB + results["latency"] = s.memoryLatencyTest(data) + duration := time.Since(start) + results["score"] = s.calculateScore(duration) + + return results +} + +func (s *Service) memoryBandwidthTest(data []byte) string { + dataSize := len(data) + + startTime := time.Now() + + for i := 0; i < dataSize; i++ { + data[i] ^= 0xFF + } + + duration := time.Since(startTime).Seconds() + if duration == 0 { + return "N/A" + } + speed := float64(dataSize) / duration / (1024 * 1024) + return fmt.Sprintf("%.2f MB/s", speed) +} + +func (s *Service) memoryLatencyTest(data []byte) string { + dataSize := len(data) + indices := rand.Perm(dataSize) + + startTime := time.Now() + sum := byte(0) + for _, idx := range indices { + sum ^= data[idx] + } + duration := time.Since(startTime).Seconds() + if duration == 0 { + return "N/A" + } + avgLatency := duration * 1e9 / float64(dataSize) + return fmt.Sprintf("%.2f ns", avgLatency) +} + +// 硬盘IO + +func (s *Service) diskTestTask() map[string]any { + results := make(map[string]any) + blockSizes := []int64{4 * 1024, 64 * 1024, 512 * 1024, 1 * 1024 * 1024} // 4K, 64K, 512K, 1M + fileSize := int64(100 * 1024 * 1024) // 100MB 文件 + + start := time.Now() + for _, blockSize := range blockSizes { + result := s.diskIOTest(blockSize, fileSize) + results[fmt.Sprintf("%d", blockSize/1024)] = result + } + duration := time.Since(start) + results["score"] = s.calculateScore(duration) + + return results +} + +func (s *Service) diskIOTest(blockSize int64, fileSize int64) map[string]any { + result := make(map[string]any) + tempFile := fmt.Sprintf("tempfile_%d", blockSize) + defer os.Remove(tempFile) + + // 写测试 + writeSpeed, writeIOPS := s.diskWriteTest(tempFile, blockSize, fileSize) + // 读测试 + readSpeed, readIOPS := s.diskReadTest(tempFile, blockSize, fileSize) + + result["write_speed"] = fmt.Sprintf("%.2f MB/s", writeSpeed) + result["write_iops"] = fmt.Sprintf("%.2f IOPS", writeIOPS) + result["read_speed"] = fmt.Sprintf("%.2f MB/s", readSpeed) + result["read_iops"] = fmt.Sprintf("%.2f IOPS", readIOPS) + + return result +} + +func (s *Service) diskWriteTest(fileName string, blockSize int64, fileSize int64) (float64, float64) { + totalBlocks := fileSize / blockSize + + data := make([]byte, blockSize) + _, _ = cryptorand.Read(data) + + file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_SYNC, 0644) + if err != nil { + return 0, 0 + } + defer file.Close() + + start := time.Now() + + for i := int64(0); i < totalBlocks; i++ { + // 生成随机偏移 + offset := rand.Int64N(fileSize - blockSize + 1) + _, err := file.WriteAt(data, offset) + if err != nil { + return 0, 0 + } + } + + _ = file.Sync() + + duration := time.Since(start).Seconds() + if duration == 0 { + duration = 1 + } + speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024) + iops := float64(totalBlocks) / duration + return speed, iops +} + +func (s *Service) diskReadTest(fileName string, blockSize int64, fileSize int64) (float64, float64) { + totalBlocks := fileSize / blockSize + + data := make([]byte, blockSize) + + file, err := os.OpenFile(fileName, os.O_RDONLY|os.O_SYNC, 0644) + if err != nil { + return 0, 0 + } + defer file.Close() + + start := time.Now() + + for i := int64(0); i < totalBlocks; i++ { + // 生成随机偏移 + offset := rand.Int64N(fileSize - blockSize + 1) + _, err := file.ReadAt(data, offset) + if err != nil && err != io.EOF { + return 0, 0 + } + } + + duration := time.Since(start).Seconds() + if duration == 0 { + duration = 1 + } + speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024) + iops := float64(totalBlocks) / duration + return speed, iops +} diff --git a/web/src/api/apps/benchmark/index.ts b/web/src/api/apps/benchmark/index.ts new file mode 100644 index 0000000000..9e49e13b92 --- /dev/null +++ b/web/src/api/apps/benchmark/index.ts @@ -0,0 +1,9 @@ +import type { AxiosResponse } from 'axios' + +import { request } from '@/utils' + +export default { + // 运行评分 + test: (name: string, multi: boolean): Promise> => + request.post('/apps/benchmark/test', { name, multi }) +} diff --git a/web/src/api/panel/dashboard/index.ts b/web/src/api/panel/dashboard/index.ts index 4ec639498f..5cf5c1c276 100644 --- a/web/src/api/panel/dashboard/index.ts +++ b/web/src/api/panel/dashboard/index.ts @@ -1,7 +1,8 @@ -import type { AxiosResponse } from 'axios' - import { request } from '@/utils' +import type { AxiosResponse } from 'axios' +import type { RequestConfig } from '~/types/axios' + export default { // 面板信息 panel: (): Promise => fetch('/api/dashboard/panel'), @@ -11,7 +12,7 @@ export default { homeApps: (): Promise> => request.get('/dashboard/homeApps'), // 实时信息 current: (nets: string[], disks: string[]): Promise> => - request.post('/dashboard/current', { nets, disks }), + request.post('/dashboard/current', { nets, disks }, { noNeedTip: true } as RequestConfig), // 系统信息 systemInfo: (): Promise> => request.get('/dashboard/systemInfo'), // 统计信息 diff --git a/web/src/views/app/IndexView.vue b/web/src/views/app/IndexView.vue index b8582b5d11..594359ae4f 100644 --- a/web/src/views/app/IndexView.vue +++ b/web/src/views/app/IndexView.vue @@ -29,7 +29,8 @@ const columns: any = [ render(row: any) { return h(TheIcon, { icon: row.icon, - size: 26 + size: 26, + color: `var(--primary-color)` }) } }, diff --git a/web/src/views/apps/benchmark/IndexView.vue b/web/src/views/apps/benchmark/IndexView.vue new file mode 100644 index 0000000000..e970e2fed4 --- /dev/null +++ b/web/src/views/apps/benchmark/IndexView.vue @@ -0,0 +1,288 @@ + + + diff --git a/web/src/views/apps/benchmark/route.ts b/web/src/views/apps/benchmark/route.ts new file mode 100644 index 0000000000..01cbc0dda2 --- /dev/null +++ b/web/src/views/apps/benchmark/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'benchmark', + path: '/apps/benchmark', + component: Layout, + isHidden: true, + children: [ + { + name: 'apps-benchmark-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: '耗子跑分', + icon: 'dashicons:performance', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/cert/route.ts b/web/src/views/cert/route.ts index 7067c1b8f1..13e45f093a 100644 --- a/web/src/views/cert/route.ts +++ b/web/src/views/cert/route.ts @@ -16,7 +16,7 @@ export default { component: () => import('./IndexView.vue'), meta: { title: 'certIndex.title', - icon: 'mdi:certificate', + icon: 'mdi:certificate-outline', role: ['admin'], requireAuth: true } diff --git a/web/src/views/cron/route.ts b/web/src/views/cron/route.ts index 621954eb6c..6a2d463cca 100644 --- a/web/src/views/cron/route.ts +++ b/web/src/views/cron/route.ts @@ -16,7 +16,7 @@ export default { component: () => import('./IndexView.vue'), meta: { title: 'cronIndex.title', - icon: 'mdi:clock-outline', + icon: 'mdi:timer-outline', role: ['admin'], requireAuth: true } diff --git a/web/src/views/file/route.ts b/web/src/views/file/route.ts index 4d01f62518..35cb851015 100644 --- a/web/src/views/file/route.ts +++ b/web/src/views/file/route.ts @@ -16,7 +16,7 @@ export default { component: () => import('./IndexView.vue'), meta: { title: 'fileIndex.title', - icon: 'mdi:file-tree', + icon: 'mdi:folder-open-outline', role: ['admin'], requireAuth: true } diff --git a/web/src/views/home/IndexView.vue b/web/src/views/home/IndexView.vue index f753d4414b..4d140f55df 100644 --- a/web/src/views/home/IndexView.vue +++ b/web/src/views/home/IndexView.vue @@ -674,7 +674,11 @@ if (import.meta.hot) {