From e8ef08cae2dd4cd9a9975519644972431f7f41c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Wed, 6 Aug 2025 20:37:45 +0800 Subject: [PATCH] Pr enhance (#1176) * fix * Refactor PacketApi status checks and fix typos Replaced all usages of PacketApi.available with PacketApi.packetStatus for more accurate status checking. Fixed a typo in getFullQQVesion to getFullQQVersion. Updated plugin_onmessage to fetch and parse messages for a user. These changes improve reliability and consistency in API status handling. * Fix typo in getFullQQVersion method name Corrected the method name from getFullQQVesion to getFullQQVersion in multiple locations to ensure consistency and prevent potential runtime errors. * Remove performance CLI and demo, fix typos, update proto Deleted the performance CLI and demo files. Fixed a typo in getFullQQVesion to getFullQQVersion across multiple files. Changed the 'time' field type from UINT32 to UINT64 in Oidb.0x9067_202 proto. Commented out performanceMonitorPlugin in vite.config.ts. Removed an unimplemented log statement in NodeIKernelBuddyListener. * Comment out default plugin adapter registration The default registration of OB11PluginAdapter in NapCatOneBot11Adapter was commented out, likely to prevent automatic plugin loading or to allow for more flexible plugin management. Also, removed an unnecessary blank line in the plugin_onmessage function. * fix * Add shell-analysis mode with performance monitoring Introduces a new .env.shell-analysis file and a dev:shell-analysis npm script for building in shell-analysis mode. Updates vite.config.ts to support the new mode, enabling the performance monitor plugin with an updated exclude list. Also extends the plugin's exclude patterns to filter out 'packet' files. * Delete performance-api.ts * Add commented export for performance-monitor Added a commented-out export statement for '@/common/performance-monitor' in napcat.ts, possibly for future use or reference. No functional changes to the file. --- .env.shell-analysis | 2 + package-lock.json | 145 ++++---- package.json | 12 +- src/common/performance-monitor.ts | 316 ++++++++++++++++++ src/common/qq-basic-info.ts | 10 +- src/core/apis/file.ts | 8 +- src/core/apis/msg.ts | 1 - src/core/apis/packet.ts | 18 +- .../listeners/NodeIKernelBuddyListener.ts | 1 - src/framework/napcat.ts | 2 +- src/onebot/action/msg/SendMsg.ts | 2 +- src/onebot/action/packet/GetPacketStatus.ts | 2 +- src/onebot/api/msg.ts | 6 +- src/onebot/index.ts | 3 +- src/plugin/index.ts | 15 +- src/shell/base.ts | 8 +- src/shell/napcat.ts | 2 + vite-plugin-performance-monitor.ts | 306 +++++++++++++++++ vite.config.ts | 30 +- 19 files changed, 769 insertions(+), 120 deletions(-) create mode 100644 .env.shell-analysis create mode 100644 src/common/performance-monitor.ts create mode 100644 vite-plugin-performance-monitor.ts 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], }; - } });