diff --git a/.env.shell-analysis b/.env.shell-analysis new file mode 100644 index 00000000..c07ade79 --- /dev/null +++ b/.env.shell-analysis @@ -0,0 +1,2 @@ +VITE_BUILD_TYPE = DEBUG +VITE_BUILD_PLATFORM = Shell diff --git a/package-lock.json b/package-lock.json index c6dbee5c..da300e1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,24 @@ { "name": "napcat", - "version": "4.8.93", + "version": "4.8.95", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "napcat", - "version": "4.8.93", + "version": "4.8.95", "dependencies": { "express": "^5.0.0", "silk-wasm": "^3.6.1", "ws": "^8.18.0" }, "devDependencies": { + "@babel/core": "^7.28.0", + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/preset-typescript": "^7.24.7", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.31.0", @@ -41,9 +46,8 @@ "cors": "^2.8.5", "esbuild": "0.25.5", "eslint": "^9.14.0", - "eslint-import-resolver-typescript": "^4.0.0", "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.32.0", "express-rate-limit": "^7.5.0", "fast-xml-parser": "^4.3.6", "file-type": "^21.0.0", @@ -65,7 +69,6 @@ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -95,29 +98,27 @@ "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -133,16 +134,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -168,7 +169,6 @@ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", @@ -202,6 +202,16 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", @@ -339,7 +349,6 @@ "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" @@ -349,13 +358,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -469,38 +478,28 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1277,18 +1276,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1301,16 +1296,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1319,9 +1304,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3223,7 +3208,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -3402,8 +3386,7 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "CC-BY-4.0", - "peer": true + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", @@ -3579,8 +3562,7 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", @@ -3858,8 +3840,7 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz", "integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/enabled": { "version": "2.0.0", @@ -4080,7 +4061,6 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -4945,7 +4925,6 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -5958,7 +5937,6 @@ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "yallist": "^3.0.2" } @@ -6281,8 +6259,7 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", @@ -8115,7 +8092,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -8479,8 +8455,7 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/yazl": { "version": "2.5.1", diff --git a/package.json b/package.json index 45809070..e2c3c44a 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,19 @@ "dev:universal": "vite build --mode universal", "dev:framework": "vite build --mode framework", "dev:shell": "vite build --mode shell", + "dev:shell-analysis": "vite build --mode shell-analysis", "dev:webui": "cd napcat.webui && npm run dev", "lint": "eslint --fix src/**/*.{js,ts,vue}", "depend": "cd dist && npm install --omit=dev", "dev:depend": "npm i && cd napcat.webui && npm i" }, "devDependencies": { + "@babel/core": "^7.28.0", + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/preset-typescript": "^7.24.7", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.31.0", @@ -41,6 +47,7 @@ "ajv": "^8.13.0", "async-mutex": "^0.5.0", "commander": "^13.0.0", + "compressing": "^1.10.1", "cors": "^2.8.5", "esbuild": "0.25.5", "eslint": "^9.14.0", @@ -52,14 +59,13 @@ "globals": "^16.0.0", "json5": "^2.2.3", "multer": "^2.0.1", + "napcat.protobuf": "^1.1.4", "typescript": "^5.3.3", "typescript-eslint": "^8.35.1", "vite": "^6.0.1", "vite-plugin-cp": "^6.0.0", "vite-tsconfig-paths": "^5.1.0", - "napcat.protobuf": "^1.1.4", - "winston": "^3.17.0", - "compressing": "^1.10.1" + "winston": "^3.17.0" }, "dependencies": { "express": "^5.0.0", diff --git a/src/common/performance-monitor.ts b/src/common/performance-monitor.ts new file mode 100644 index 00000000..7d818033 --- /dev/null +++ b/src/common/performance-monitor.ts @@ -0,0 +1,316 @@ +/** + * 性能监控器 - 用于统计函数调用次数、耗时等信息 + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +export interface FunctionStats { + name: string; + callCount: number; + totalTime: number; + averageTime: number; + minTime: number; + maxTime: number; + fileName?: string; + lineNumber?: number; +} + +export class PerformanceMonitor { + private static instance: PerformanceMonitor; + private stats = new Map(); + private startTimes = new Map(); + private reportInterval: NodeJS.Timeout | null = null; + + static getInstance(): PerformanceMonitor { + if (!PerformanceMonitor.instance) { + PerformanceMonitor.instance = new PerformanceMonitor(); + // 启动定时统计报告 + PerformanceMonitor.instance.startPeriodicReport(); + } + return PerformanceMonitor.instance; + } + + /** + * 开始定时统计报告 (每60秒) + */ + private startPeriodicReport(): void { + if (this.reportInterval) { + clearInterval(this.reportInterval); + } + + this.reportInterval = setInterval(() => { + if (this.stats.size > 0) { + this.printPeriodicReport(); + this.writeDetailedLogToFile(); + } + }, 60000); // 60秒 + } + + /** + * 停止定时统计报告 + */ + stopPeriodicReport(): void { + if (this.reportInterval) { + clearInterval(this.reportInterval); + this.reportInterval = null; + } + } + + /** + * 打印定时统计报告 (简化版本) + */ + private printPeriodicReport(): void { + const now = new Date().toLocaleString(); + console.log(`\n=== 性能监控定时报告 [${now}] ===`); + + const totalFunctions = this.stats.size; + const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0); + const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0); + + console.log(`📊 总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms`); + + // 显示Top 5最活跃的函数 + console.log('\n🔥 最活跃函数 (Top 5):'); + this.getTopByCallCount(5).forEach((stat, index) => { + console.log(`${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms`); + }); + + // 显示Top 5最耗时的函数 + console.log('\n⏱️ 最耗时函数 (Top 5):'); + this.getTopByTotalTime(5).forEach((stat, index) => { + console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms`); + }); + + console.log('===============================\n'); + } + + /** + * 将详细统计数据写入日志文件 + */ + private writeDetailedLogToFile(): void { + try { + const now = new Date(); + const dateStr = now.toISOString().replace(/[:.]/g, '-').split('T')[0]; + const timeStr = now.toTimeString().split(' ')[0]?.replace(/:/g, '-') || 'unknown-time'; + const timestamp = `${dateStr}_${timeStr}`; + const fileName = `${timestamp}.log.txt`; + const logPath = path.join(process.cwd(), 'logs', fileName); + + // 确保logs目录存在 + const logsDir = path.dirname(logPath); + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + + const totalFunctions = this.stats.size; + const totalCalls = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.callCount, 0); + const totalTime = Array.from(this.stats.values()).reduce((sum, stat) => sum + stat.totalTime, 0); + + let logContent = ''; + logContent += `=== 性能监控详细报告 ===\n`; + logContent += `生成时间: ${now.toLocaleString()}\n`; + logContent += `统计周期: 60秒\n`; + logContent += `总览: ${totalFunctions} 个函数, ${totalCalls} 次调用, 总耗时: ${totalTime.toFixed(2)}ms\n\n`; + + // 详细函数统计 + logContent += `=== 所有函数详细统计 ===\n`; + const allStats = this.getStats().sort((a, b) => b.totalTime - a.totalTime); + + allStats.forEach((stat, index) => { + logContent += `${index + 1}. 函数: ${stat.name}\n`; + logContent += ` 文件: ${stat.fileName || 'N/A'}\n`; + logContent += ` 行号: ${stat.lineNumber || 'N/A'}\n`; + logContent += ` 调用次数: ${stat.callCount}\n`; + logContent += ` 总耗时: ${stat.totalTime.toFixed(4)}ms\n`; + logContent += ` 平均耗时: ${stat.averageTime.toFixed(4)}ms\n`; + logContent += ` 最小耗时: ${stat.minTime === Infinity ? 'N/A' : stat.minTime.toFixed(4)}ms\n`; + logContent += ` 最大耗时: ${stat.maxTime.toFixed(4)}ms\n`; + logContent += ` 性能占比: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`; + logContent += `\n`; + }); + + // 排行榜统计 + logContent += `=== 总耗时排行榜 (Top 20) ===\n`; + this.getTopByTotalTime(20).forEach((stat, index) => { + logContent += `${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 平均: ${stat.averageTime.toFixed(2)}ms\n`; + }); + + logContent += `\n=== 调用次数排行榜 (Top 20) ===\n`; + this.getTopByCallCount(20).forEach((stat, index) => { + logContent += `${index + 1}. ${stat.name} - 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均: ${stat.averageTime.toFixed(2)}ms\n`; + }); + + logContent += `\n=== 平均耗时排行榜 (Top 20) ===\n`; + this.getTopByAverageTime(20).forEach((stat, index) => { + logContent += `${index + 1}. ${stat.name} - 平均: ${stat.averageTime.toFixed(2)}ms, 调用: ${stat.callCount}次, 总耗时: ${stat.totalTime.toFixed(2)}ms\n`; + }); + + logContent += `\n=== 性能热点分析 ===\n`; + // 找出最耗时的前10个函数 + const hotSpots = this.getTopByTotalTime(10); + hotSpots.forEach((stat, index) => { + const efficiency = stat.callCount / stat.totalTime; // 每毫秒的调用次数 + logContent += `${index + 1}. ${stat.name}\n`; + logContent += ` 性能影响: ${((stat.totalTime / totalTime) * 100).toFixed(2)}%\n`; + logContent += ` 调用效率: ${efficiency.toFixed(4)} 调用/ms\n`; + logContent += ` 优化建议: ${stat.averageTime > 10 ? '考虑优化此函数的执行效率' : + stat.callCount > 1000 ? '考虑减少此函数的调用频率' : + '性能表现良好'}\n\n`; + }); + + logContent += `=== 报告结束 ===\n`; + + // 写入文件 + fs.writeFileSync(logPath, logContent, 'utf8'); + console.log(`📄 详细性能报告已保存到: ${logPath}`); + + } catch (error) { + console.error('写入性能日志文件时出错:', error); + } + } + + /** + * 开始记录函数调用 + */ + startFunction(functionName: string, fileName?: string, lineNumber?: number): string { + const callId = `${functionName}_${Date.now()}_${Math.random()}`; + this.startTimes.set(callId, performance.now()); + + // 初始化或更新统计信息 + if (!this.stats.has(functionName)) { + this.stats.set(functionName, { + name: functionName, + callCount: 0, + totalTime: 0, + averageTime: 0, + minTime: Infinity, + maxTime: 0, + fileName, + lineNumber + }); + } + + const stat = this.stats.get(functionName)!; + stat.callCount++; + + return callId; + } + + /** + * 结束记录函数调用 + */ + endFunction(callId: string, functionName: string): void { + const startTime = this.startTimes.get(callId); + if (!startTime) return; + + const endTime = performance.now(); + const duration = endTime - startTime; + + this.startTimes.delete(callId); + + const stat = this.stats.get(functionName); + if (!stat) return; + + stat.totalTime += duration; + stat.averageTime = stat.totalTime / stat.callCount; + stat.minTime = Math.min(stat.minTime, duration); + stat.maxTime = Math.max(stat.maxTime, duration); + } + + /** + * 获取所有统计信息 + */ + getStats(): FunctionStats[] { + return Array.from(this.stats.values()); + } + + /** + * 获取排行榜 - 按总耗时排序 + */ + getTopByTotalTime(limit = 20): FunctionStats[] { + return this.getStats() + .sort((a, b) => b.totalTime - a.totalTime) + .slice(0, limit); + } + + /** + * 获取排行榜 - 按调用次数排序 + */ + getTopByCallCount(limit = 20): FunctionStats[] { + return this.getStats() + .sort((a, b) => b.callCount - a.callCount) + .slice(0, limit); + } + + /** + * 获取排行榜 - 按平均耗时排序 + */ + getTopByAverageTime(limit = 20): FunctionStats[] { + return this.getStats() + .sort((a, b) => b.averageTime - a.averageTime) + .slice(0, limit); + } + + /** + * 清空统计数据 + */ + clear(): void { + this.stats.clear(); + this.startTimes.clear(); + } + + /** + * 打印统计报告 + */ + printReport(): void { + console.log('\n=== 函数性能监控报告 ==='); + + console.log('\n🔥 总耗时排行榜 (Top 10):'); + this.getTopByTotalTime(10).forEach((stat, index) => { + console.log(`${index + 1}. ${stat.name} - 总耗时: ${stat.totalTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 平均耗时: ${stat.averageTime.toFixed(2)}ms`); + }); + + console.log('\n📈 调用次数排行榜 (Top 10):'); + this.getTopByCallCount(10).forEach((stat, index) => { + console.log(`${index + 1}. ${stat.name} - 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms, 平均耗时: ${stat.averageTime.toFixed(2)}ms`); + }); + + console.log('\n⏱️ 平均耗时排行榜 (Top 10):'); + this.getTopByAverageTime(10).forEach((stat, index) => { + console.log(`${index + 1}. ${stat.name} - 平均耗时: ${stat.averageTime.toFixed(2)}ms, 调用次数: ${stat.callCount}, 总耗时: ${stat.totalTime.toFixed(2)}ms`); + }); + + console.log('\n========================\n'); + } + + /** + * 获取JSON格式的统计数据 + */ + toJSON(): FunctionStats[] { + return this.getStats(); + } +} + +// 全局性能监控器实例 +export const performanceMonitor = PerformanceMonitor.getInstance(); + +// 在进程退出时打印报告并停止定时器 +if (typeof process !== 'undefined') { + process.on('exit', () => { + performanceMonitor.stopPeriodicReport(); + performanceMonitor.printReport(); + }); + + process.on('SIGINT', () => { + performanceMonitor.stopPeriodicReport(); + performanceMonitor.printReport(); + process.exit(0); + }); + + process.on('SIGTERM', () => { + performanceMonitor.stopPeriodicReport(); + performanceMonitor.printReport(); + process.exit(0); + }); +} diff --git a/src/common/qq-basic-info.ts b/src/common/qq-basic-info.ts index b468e908..cc9a9482 100644 --- a/src/common/qq-basic-info.ts +++ b/src/common/qq-basic-info.ts @@ -42,7 +42,7 @@ export class QQBasicInfoWrapper { return this.isQuickUpdate ? this.QQVersionConfig?.buildId : this.QQPackageInfo?.buildVersion; } - getFullQQVesion() { + getFullQQVersion() { const version = this.isQuickUpdate ? this.QQVersionConfig?.curVersion : this.QQPackageInfo?.version; if (!version) throw new Error('QQ版本获取失败'); return version; @@ -57,9 +57,9 @@ export class QQBasicInfoWrapper { //此方法不要直接使用 getQUAFallback() { const platformMapping: Partial> = { - win32: `V1_WIN_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`, - darwin: `V1_MAC_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`, - linux: `V1_LNX_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`, + win32: `V1_WIN_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`, + darwin: `V1_MAC_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`, + linux: `V1_LNX_${this.getFullQQVersion()}_${this.getQQBuildStr()}_GW_B`, }; return platformMapping[systemPlatform] ?? (platformMapping.win32)!; } @@ -76,7 +76,7 @@ export class QQBasicInfoWrapper { getAppidV2(): { appid: string; qua: string } { // 通过已有表 性能好 const appidTbale = AppidTable as unknown as QQAppidTableType; - const fullVersion = this.getFullQQVesion(); + const fullVersion = this.getFullQQVersion(); if (fullVersion) { const data = appidTbale[fullVersion]; if (data) { diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index b92eee23..4725aaf8 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -65,7 +65,7 @@ export class NTQQFileApi { } async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined) { - if (this.core.apis.PacketApi.available) { + if (this.core.apis.PacketApi.packetStatus) { try { if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) { return this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+peer, fileUUID); @@ -80,7 +80,7 @@ export class NTQQFileApi { } async getPttUrl(peer: string, fileUUID?: string) { - if (this.core.apis.PacketApi.available && fileUUID) { + if (this.core.apis.PacketApi.packetStatus && fileUUID) { let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid; try { if (appid && appid === 1403) { @@ -108,7 +108,7 @@ export class NTQQFileApi { } async getVideoUrlPacket(peer: string, fileUUID?: string) { - if (this.core.apis.PacketApi.available && fileUUID) { + if (this.core.apis.PacketApi.packetStatus && fileUUID) { let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid; try { if (appid && appid === 1415) { @@ -502,7 +502,7 @@ export class NTQQFileApi { }; try { - if (this.core.apis.PacketApi.available) { + if (this.core.apis.PacketApi.packetStatus) { const rkey_expired_private = !this.packetRkey || (this.packetRkey[0] && this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000); const rkey_expired_group = !this.packetRkey || (this.packetRkey[0] && this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000); if (rkey_expired_private || rkey_expired_group) { diff --git a/src/core/apis/msg.ts b/src/core/apis/msg.ts index 7c2d3614..3dd1e365 100644 --- a/src/core/apis/msg.ts +++ b/src/core/apis/msg.ts @@ -142,7 +142,6 @@ export class NTQQMsgApi { } async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) { - console.log(peer, SendersUid); return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { chatInfo: peer, filterMsgType: [], diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 8b573631..99f6a6bd 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -21,20 +21,26 @@ export class NTQQPacketApi { qqVersion: string | undefined; pkt!: PacketClientSession; errStack: string[] = []; + packetStatus: boolean = false; constructor(context: InstanceContext, core: NapCatCore) { this.context = context; this.core = core; this.logger = core.context.logger; } + async initApi() { - await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion()) - .then() + this.packetStatus = (await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVersion()) + .then((result) => { + return result; + }) .catch((err) => { this.logger.logError(err); this.errStack.push(err); - }); + return false; + })) && this.pkt?.available; } + get available(): boolean { return this.pkt?.available ?? false; } @@ -61,6 +67,12 @@ export class NTQQPacketApi { } this.pkt = new PacketClientSession(this.core); await this.pkt.init(process.pid, table.recv, table.send); + try { + await this.pkt.operation.FetchRkey(); + } catch (error) { + this.logger.logError('测试Packet状态异常', error); + return false; + } return true; } } diff --git a/src/core/listeners/NodeIKernelBuddyListener.ts b/src/core/listeners/NodeIKernelBuddyListener.ts index c85883ad..c7f6303f 100644 --- a/src/core/listeners/NodeIKernelBuddyListener.ts +++ b/src/core/listeners/NodeIKernelBuddyListener.ts @@ -40,7 +40,6 @@ export class NodeIKernelBuddyListener { } onDelBatchBuddyInfos(_arg: unknown): any { - console.log('onDelBatchBuddyInfos not implemented', ...arguments); } onDoubtBuddyReqChange(_arg: diff --git a/src/framework/napcat.ts b/src/framework/napcat.ts index 82f5e7e3..10e7fb88 100644 --- a/src/framework/napcat.ts +++ b/src/framework/napcat.ts @@ -37,7 +37,7 @@ export async function NCoreInitFramework( const pathWrapper = new NapCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); - const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion()); + const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); if (!process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) { downloadFFmpegIfNotExists(logger).then(({ path, reset }) => { if (reset && path) { diff --git a/src/onebot/action/msg/SendMsg.ts b/src/onebot/action/msg/SendMsg.ts index 99fd8cb1..60ef8322 100644 --- a/src/onebot/action/msg/SendMsg.ts +++ b/src/onebot/action/msg/SendMsg.ts @@ -130,7 +130,7 @@ export class SendMsgBase extends OneBotAction { ); if (getSpecialMsgNum(payload, OB11MessageDataType.node)) { - const packetMode = this.core.apis.PacketApi.available; + const packetMode = this.core.apis.PacketApi.packetStatus; let returnMsgAndResId: { message: RawMessage | null, res_id?: string } | null; try { returnMsgAndResId = packetMode diff --git a/src/onebot/action/packet/GetPacketStatus.ts b/src/onebot/action/packet/GetPacketStatus.ts index 2630163b..bf0bb3c3 100644 --- a/src/onebot/action/packet/GetPacketStatus.ts +++ b/src/onebot/action/packet/GetPacketStatus.ts @@ -4,7 +4,7 @@ import { ActionName, BaseCheckResult } from '@/onebot/action/router'; export abstract class GetPacketStatusDepends extends OneBotAction { protected override async check(payload: PT): Promise{ - if (!this.core.apis.PacketApi.available) { + if (!this.core.apis.PacketApi.packetStatus) { return { valid: false, message: 'packetBackend不可用,请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置!' + diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index 00eed17a..888cd53a 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -147,7 +147,7 @@ export class OneBotMsgApi { }; FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid); FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName); - if (this.core.apis.PacketApi.available) { + if (this.core.apis.PacketApi.packetStatus) { let url; try { url = await this.core.apis.FileApi.getFileUrl(msg.chatType, msg.peerUid, element.fileUuid, element.file10MMd5) @@ -391,7 +391,7 @@ export class OneBotMsgApi { //开始兜底 if (!videoDownUrl) { - if (this.core.apis.PacketApi.available) { + if (this.core.apis.PacketApi.packetStatus) { try { videoDownUrl = await this.core.apis.FileApi.getVideoUrlPacket(msg.peerUid, element.fileUuid); } catch (e) { @@ -422,7 +422,7 @@ export class OneBotMsgApi { }; const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, '', element.fileName); let pttUrl = ''; - if (this.core.apis.PacketApi.available) { + if (this.core.apis.PacketApi.packetStatus) { try { pttUrl = await this.core.apis.FileApi.getPttUrl(msg.peerUid, element.fileUuid); } catch (e) { diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 1cacec7c..95c7d32e 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -50,6 +50,7 @@ import { import { OB11Message } from './types'; import { IOB11NetworkAdapter } from '@/onebot/network/adapter'; import { OB11HttpSSEServerAdapter } from './network/http-server-sse'; +import { OB11PluginAdapter } from './network/plugin'; //OneBot实现类 export class NapCatOneBot11Adapter { @@ -169,7 +170,7 @@ export class NapCatOneBot11Adapter { this.initBuddyListener(); this.initGroupListener(); - WebUiDataRuntime.setQQVersion(this.core.context.basicInfoWrapper.getFullQQVesion()); + WebUiDataRuntime.setQQVersion(this.core.context.basicInfoWrapper.getFullQQVersion()); WebUiDataRuntime.setQQLoginInfo(selfInfo); WebUiDataRuntime.setQQLoginStatus(true); WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => { diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 484a58b5..288fc897 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -1,11 +1,16 @@ import { NapCatOneBot11Adapter, OB11Message } from '@/onebot'; -import { NapCatCore } from '@/core'; +import { ChatType, NapCatCore } from '@/core'; import { ActionMap } from '@/onebot/action'; import { OB11PluginAdapter } from '@/onebot/network/plugin'; export const plugin_onmessage = async (adapter: string, _core: NapCatCore, _obCtx: NapCatOneBot11Adapter, message: OB11Message, action: ActionMap, instance: OB11PluginAdapter) => { - if (message.raw_message === 'ping') { - const ret = await action.get('send_group_msg')?.handle({ group_id: String(message.group_id), message: 'pong' }, adapter, instance.config); - console.log(ret); + const uid = await _core.apis.UserApi.getUidByUinV2(message.sender.user_id.toString()); + const msgs = (await _core.apis.MsgApi.queryFirstMsgBySender({ + peerUid: message.group_id ? String(message.group_id) : String(uid), + chatType: ChatType.KCHATTYPEGROUP, + }, [uid])).msgList; + console.log('parse message ', message.sender.user_id, msgs.length); + for (const msg of msgs) { + await _obCtx.apis.MsgApi.parseMessageV2(msg) } -}; +} \ No newline at end of file diff --git a/src/shell/base.ts b/src/shell/base.ts index 17258697..708ac2a7 100644 --- a/src/shell/base.ts +++ b/src/shell/base.ts @@ -80,7 +80,7 @@ async function initializeEngine( base_path_prefix: '', platform_type: systemPlatform, app_type: 4, - app_version: basicInfoWrapper.getFullQQVesion(), + app_version: basicInfoWrapper.getFullQQVersion(), os_version: systemVersion, use_xlog: false, qua: basicInfoWrapper.QQVersionQua ?? '', @@ -105,7 +105,7 @@ async function initializeLoginService( appid: basicInfoWrapper.QQVersionAppid ?? '', platVer: systemVersion, commonPath: dataPathGlobal, - clientVer: basicInfoWrapper.getFullQQVesion(), + clientVer: basicInfoWrapper.getFullQQVersion(), hostName: hostname, }); } @@ -324,7 +324,7 @@ export async function NCoreInitShell() { }); } const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); - const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion()); + const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); const o3Service = wrapper.NodeIO3MiscService.get(); o3Service.addO3MiscListener(new NodeIO3MiscListener()); @@ -363,7 +363,7 @@ export async function NCoreInitShell() { const sessionConfig = await genSessionConfig( guid, basicInfoWrapper.QQVersionAppid, - basicInfoWrapper.getFullQQVesion(), + basicInfoWrapper.getFullQQVersion(), selfInfo.uin, selfInfo.uid, dataPath, diff --git a/src/shell/napcat.ts b/src/shell/napcat.ts index 7b300cb0..333b99f2 100644 --- a/src/shell/napcat.ts +++ b/src/shell/napcat.ts @@ -1,2 +1,4 @@ +//export * from '@/common/performance-monitor'; import { NCoreInitShell } from './base'; + NCoreInitShell(); \ No newline at end of file diff --git a/vite-plugin-performance-monitor.ts b/vite-plugin-performance-monitor.ts new file mode 100644 index 00000000..401527ec --- /dev/null +++ b/vite-plugin-performance-monitor.ts @@ -0,0 +1,306 @@ +import { Plugin } from 'vite'; +import { parse } from '@babel/parser'; +import traverseDefault from '@babel/traverse'; +import generateDefault from '@babel/generator'; +import * as t from '@babel/types'; +import { resolve } from 'path'; + +// @ts-ignore +const traverse = traverseDefault.default || traverseDefault; +// @ts-ignore +const generate = generateDefault.default || generateDefault; + +interface PerformancePluginOptions { +} + +/** + * Vite插件:自动在函数中插入性能监控代码 + */ +export function performanceMonitorPlugin(options: PerformancePluginOptions): Plugin { + const exclude = [/node_modules/, /\.min\./, /performance-monitor\.ts$/, /packet/]; + + return { + name: 'performance-monitor', + transform(code: string, id: string) { + const fileName = id.replace(process.cwd(), '').replace(/\\/g, '/'); + + // 排除规则检查 + if (exclude.some(pattern => pattern.test(id))) { + return null; + } + + try { + // 解析AST + const ast = parse(code, { + sourceType: 'module', + plugins: [ + 'typescript', + 'decorators-legacy', + 'classProperties', + 'asyncGenerators', + 'bigInt', + 'dynamicImport', + 'exportDefaultFrom', + 'exportNamespaceFrom', + 'nullishCoalescingOperator', + 'numericSeparator', + 'optionalCatchBinding', + 'optionalChaining', + 'topLevelAwait' + ] + }); + + let hasMonitorImport = false; + let hasMonitorExport = false; + let needsMonitor = false; + // 遍历AST + traverse(ast, { + // 检查是否已经导入了性能监控器 + ImportDeclaration(path) { + if (path.node.source.value.includes('performance-monitor')) { + hasMonitorImport = true; + } + }, + + // 检查是否已经导出了性能监控器 + ExportNamedDeclaration(path) { + if (path.node.declaration && t.isVariableDeclaration(path.node.declaration)) { + path.node.declaration.declarations.forEach(declarator => { + if (t.isIdentifier(declarator.id) && declarator.id.name === 'performanceMonitor') { + hasMonitorExport = true; + } + }); + } + }, + + // 检查变量声明 + VariableDeclaration(path) { + path.node.declarations.forEach(declarator => { + if (t.isIdentifier(declarator.id) && declarator.id.name === 'performanceMonitor') { + hasMonitorExport = true; + } + }); + }, + + // 处理函数声明 + FunctionDeclaration(path) { + const functionName = path.node.id?.name || 'anonymous'; + const lineNumber = path.node.loc?.start.line || 0; + + instrumentFunction(path, functionName, fileName, lineNumber); + needsMonitor = true; + }, + + // 处理箭头函数 + ArrowFunctionExpression(path) { + const parent = path.parent; + let functionName = 'anonymous'; + + if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) { + functionName = parent.id.name; + } else if (t.isProperty(parent) && t.isIdentifier(parent.key)) { + functionName = parent.key.name; + } else if (t.isAssignmentExpression(parent) && t.isMemberExpression(parent.left)) { + const property = parent.left.property; + if (t.isIdentifier(property)) { + functionName = property.name; + } + } + + const lineNumber = path.node.loc?.start.line || 0; + instrumentFunction(path, functionName, fileName, lineNumber); + needsMonitor = true; + }, + + // 处理函数表达式 + FunctionExpression(path) { + const functionName = path.node.id?.name || 'anonymous'; + const lineNumber = path.node.loc?.start.line || 0; + + instrumentFunction(path, functionName, fileName, lineNumber); + needsMonitor = true; + }, + + // 处理类方法 + ClassMethod(path) { + const methodName = t.isIdentifier(path.node.key) ? path.node.key.name : 'anonymous'; + const className = getClassName(path); + const functionName = `${className}.${methodName}`; + const lineNumber = path.node.loc?.start.line || 0; + + instrumentFunction(path, functionName, fileName, lineNumber); + needsMonitor = true; + }, + + // 处理对象方法 + ObjectMethod(path) { + const methodName = t.isIdentifier(path.node.key) ? path.node.key.name : 'anonymous'; + const lineNumber = path.node.loc?.start.line || 0; + + instrumentFunction(path, methodName, fileName, lineNumber); + needsMonitor = true; + } + }); + + if (!needsMonitor) { + return null; + } + + // 如果需要监控但还没有导入且没有导出,则添加导入语句 + if (!hasMonitorImport && !hasMonitorExport) { + const importDeclaration = t.importDeclaration( + [t.importSpecifier(t.identifier('performanceMonitor'), t.identifier('performanceMonitor'))], + t.stringLiteral('@/common/performance-monitor') + ); + ast.program.body.unshift(importDeclaration); + } + + // 生成新代码 + const result = generate(ast, { + retainLines: true, + compact: false + }); + + return { + code: result.code, + map: result.map + }; + + } catch (error) { + console.warn(`性能监控插件处理文件 ${id} 时出错:`, error); + return null; + } + } + }; +} + +/** + * 为函数添加性能监控代码 + */ +function instrumentFunction( + path: any, + functionName: string, + fileName: string, + lineNumber: number +) { + // 跳过已经被监控的函数 + if (functionName.includes('__perf_monitor__')) { + return; + } + + const isAsync = path.node.async; + const body = path.node.body; + + // 确保函数体是块语句 + if (!t.isBlockStatement(body)) { + // 对于箭头函数的表达式体,转换为块语句 + const returnStatement = t.returnStatement(body); + path.node.body = t.blockStatement([returnStatement]); + } + + const blockBody = path.node.body as t.BlockStatement; + + // 生成唯一的调用ID变量名 + const callIdVar = `__perf_monitor_${functionName.replace(/[^a-zA-Z0-9]/g, '_')}_${lineNumber}__`; + + // 创建开始监控的语句 + const startMonitoring = t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(callIdVar), + t.callExpression( + t.memberExpression( + t.identifier('performanceMonitor'), + t.identifier('startFunction') + ), + [ + t.stringLiteral(functionName), + t.stringLiteral(fileName), + t.numericLiteral(lineNumber) + ] + ) + ) + ]); + + // 创建结束监控的语句 + const endMonitoring = t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier('performanceMonitor'), + t.identifier('endFunction') + ), + [ + t.identifier(callIdVar), + t.stringLiteral(functionName) + ] + ) + ); + + if (isAsync) { + // 对于异步函数,需要在所有可能的返回点添加监控结束 + instrumentAsyncFunction(blockBody, startMonitoring, endMonitoring, callIdVar, functionName); + } else { + // 对于同步函数,使用try-finally确保监控结束 + instrumentSyncFunction(blockBody, startMonitoring, endMonitoring); + } +} + +/** + * 为同步函数添加监控 + */ +function instrumentSyncFunction( + blockBody: t.BlockStatement, + startMonitoring: t.VariableDeclaration, + endMonitoring: t.ExpressionStatement +) { + const originalStatements = [...blockBody.body]; + + const tryStatement = t.tryStatement( + t.blockStatement(originalStatements), + null, + t.blockStatement([endMonitoring]) + ); + + blockBody.body = [startMonitoring, tryStatement]; +} + +/** + * 为异步函数添加监控 + */ +function instrumentAsyncFunction( + blockBody: t.BlockStatement, + startMonitoring: t.VariableDeclaration, + endMonitoring: t.ExpressionStatement, + callIdVar: string, + functionName: string +) { + const originalStatements = [...blockBody.body]; + + // 创建包装的异步执行体 + const asyncTryStatement = t.tryStatement( + t.blockStatement(originalStatements), + null, + t.blockStatement([endMonitoring]) + ); + + blockBody.body = [startMonitoring, asyncTryStatement]; +} + +/** + * 获取类名 + */ +function getClassName(path: any): string { + let current = path; + while (current) { + if (current.isClassDeclaration && current.isClassDeclaration()) { + return current.node.id?.name || 'AnonymousClass'; + } else if (current.isClassExpression && current.isClassExpression()) { + return current.node.id?.name || 'AnonymousClass'; + } else if (current.node && (t.isClassDeclaration(current.node) || t.isClassExpression(current.node))) { + return current.node.id?.name || 'AnonymousClass'; + } + current = current.parent; + } + return 'UnknownClass'; +} + +export default performanceMonitorPlugin; diff --git a/vite.config.ts b/vite.config.ts index 92ac954c..65e662d2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import { defineConfig, PluginOption, UserConfig } from 'vite'; import { resolve } from 'path'; import nodeResolve from '@rollup/plugin-node-resolve'; import { builtinModules } from 'module'; +import { performanceMonitorPlugin } from './vite-plugin-performance-monitor'; //依赖排除 const external = [ 'silk-wasm', @@ -21,6 +22,11 @@ if (process.env.NAPCAT_BUILDSYS == 'linux') { } const UniversalBaseConfigPlugin: PluginOption[] = [ + // performanceMonitorPlugin({ + // enabled: process.env.NODE_ENV !== 'production', + // exclude: [/node_modules/, /\.min\./, /performance-monitor/], + // include: [/\.ts$/, /\.js$/] + // }), cp({ targets: [ { src: './manifest.json', dest: 'dist' }, @@ -44,6 +50,11 @@ const UniversalBaseConfigPlugin: PluginOption[] = [ ]; const FrameworkBaseConfigPlugin: PluginOption[] = [ + // performanceMonitorPlugin({ + // enabled: process.env.NODE_ENV !== 'production', + // exclude: [/node_modules/, /\.min\./, /performance-monitor/], + // include: [/\.ts$/, /\.js$/] + // }), cp({ targets: [ { src: './manifest.json', dest: 'dist' }, @@ -64,6 +75,11 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [ ]; const ShellBaseConfigPlugin: PluginOption[] = [ + // performanceMonitorPlugin({ + // enabled: process.env.NODE_ENV !== 'production', + // exclude: [/node_modules/, /\.min\./, /performance-monitor/], + // include: [/\.ts$/, /\.js$/] + // }), cp({ targets: [ { src: './src/native/packet', dest: 'dist/moehoo', flatten: false }, @@ -177,10 +193,20 @@ export default defineConfig(({ mode }): UserConfig => { ...UniversalBaseConfig(), plugins: [...UniversalBaseConfigPlugin], }; - } else { + } else if (mode == 'shell-analysis') { + return { + ...ShellBaseConfig(), + plugins: [ + performanceMonitorPlugin({ + exclude: [/node_modules/, /\.min\./, /performance-monitor\.ts$/, /packet/], + include: [/\.ts$/, /\.js$/] + }), + ...ShellBaseConfigPlugin + ], + }; + } else return { ...FrameworkBaseConfig(), plugins: [...FrameworkBaseConfigPlugin], }; - } });