diff --git a/cli/package.json b/cli/package.json index 032d1e1f..e9dd53df 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "tailchat-cli", - "version": "1.3.0", + "version": "1.4.0", "description": "A Command line interface of tailchat", "bin": { "tailchat": "./bin/cli" @@ -40,13 +40,17 @@ "lodash": "^4.17.21", "node-plop": "^0.26.3", "ora": "5.4.1", + "p-all": "2.1.0", + "p-series": "2.1.0", "plop": "^3.0.5", + "pretty-ms": "7.0.1", "tailchat-server-sdk": "^0.0.12", "yargs": "^17.4.0" }, "devDependencies": { "@types/fs-extra": "^9.0.13", "@types/inquirer": "^8.2.1", + "@types/lodash": "^4.14.170", "@types/node": "16.11.7", "@types/yargs": "^17.0.10", "ts-node": "^10.7.0", diff --git a/cli/src/commands/bench.ts b/cli/src/commands/bench.ts new file mode 100644 index 00000000..db05ab67 --- /dev/null +++ b/cli/src/commands/bench.ts @@ -0,0 +1,166 @@ +import { CommandModule } from 'yargs'; +import { TcBroker } from 'tailchat-server-sdk'; +import defaultBrokerConfig from 'tailchat-server-sdk/dist/runner/moleculer.config'; +import { config } from 'dotenv'; +import _ from 'lodash'; +import os from 'os'; +import pAll from 'p-all'; +import pSeries from 'p-series'; +import ora from 'ora'; +import prettyMs from 'pretty-ms'; + +export const benchCommand: CommandModule = { + command: 'bench', + describe: '压力测试', + builder: (yargs) => + yargs + .command( + 'message', + '通过内网请求进行压力测试(适用于纯业务测试)', + (yargs) => + yargs + .positional('groupId', { + describe: '群组ID', + demandOption: true, + type: 'string', + }) + .positional('converseId', { + describe: '会话ID', + demandOption: true, + type: 'string', + }) + .positional('userId', { + describe: '用户ID', + demandOption: true, + type: 'string', + }) + .positional('num', { + describe: '测试次数', + type: 'number', + default: 100, + }) + .positional('parallel', { + describe: '是否并发', + type: 'boolean', + default: false, + }), + + async (args) => { + config(); + + const broker = new TcBroker({ + ...defaultBrokerConfig, + transporter: process.env.TRANSPORTER, + logger: false, + }); + await broker.start(); + + printSystemInfo(); + + console.log('==============='); + + await startBenchmark({ + parallel: args.parallel, + number: args.num, + task: async (i) => { + const start = process.hrtime(); + await broker.call( + 'chat.message.sendMessage', + { + converseId: args.converseId, + groupId: args.groupId, + content: `benchmessage ${i + 1}`, + }, + { + meta: { + userId: args.userId, + }, + } + ); + const usage = calcUsage(start); + + return usage; + }, + onCompleted: (res) => { + console.log(`测试数量: \t${res.length}`); + console.log(`最大用时: \t${prettyMs(Math.max(...res))}`); + console.log(`最小用时: \t${prettyMs(Math.min(...res))}`); + console.log(`平均用时: \t${prettyMs(_.mean(res))}`); + }, + }); + + await broker.stop(); + } + ) + .demandCommand(), + handler() {}, +}; + +/** + * 打印系统信息 + */ +function printSystemInfo() { + console.log(`主机: \t${os.hostname()}`); + console.log(`系统: \t${os.type()} - ${os.release()}`); + console.log(`架构: \t${os.arch()} - ${os.version()}`); + console.log(`CPU: \t${os.cpus().length}`); + console.log(`内存: \t${os.totalmem()}`); +} + +function calcUsage(startTime: [number, number]) { + const diff = process.hrtime(startTime); + const usage = (diff[0] + diff[1] / 1e9) * 1000; + + return usage; +} + +interface BenchmarkOptions { + parallel: boolean; // 是否并发 + parallelLimit?: number; // 并发上限, 默认不限制(Infinity) + task: (index: number) => Promise; + number?: number; + onCompleted: (res: T[]) => void; +} +/** + * 开始一次基准测试 + */ +async function startBenchmark(options: BenchmarkOptions) { + const { + parallel, + parallelLimit = Infinity, + task, + number = 100, + onCompleted, + } = options; + + const spinner = ora(); + + spinner.info( + `测试方式: ${parallel ? `并行, 上限 ${parallelLimit}` : `串行`}` + ); + spinner.info(`执行任务数: ${number}`); + spinner.start('正在执行基准测试...'); + try { + const startTime = process.hrtime(); + let res: T[] = []; + if (parallel) { + res = await pAll( + [...Array.from({ length: number }).map((_, i) => () => task(i))], + { + concurrency: parallelLimit, + } + ); + } else { + res = await pSeries([ + ...Array.from({ length: number }).map((_, i) => () => task(i)), + ]); + } + + spinner.succeed(`基准测试完毕, 用时 ${prettyMs(calcUsage(startTime))}`); + + onCompleted(res); + } catch (err) { + console.error(err); + spinner.fail(`基准测试出现问题`).stop(); + } +} diff --git a/cli/src/index.ts b/cli/src/index.ts index 84867cc2..381cf943 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -2,11 +2,13 @@ import yargs from 'yargs'; import { createCommand } from './commands/create'; import { connectCommand } from './commands/connect'; import { declarationCommand } from './commands/declaration'; +import { benchCommand } from './commands/bench'; yargs .demandCommand() .command(createCommand) .command(connectCommand) + .command(benchCommand) .command(declarationCommand) .alias('h', 'help') .scriptName('tailchat') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62b005f0..08603d7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,7 +60,9 @@ importers: specifiers: '@types/fs-extra': ^9.0.13 '@types/inquirer': ^8.2.1 + '@types/lodash': ^4.14.170 '@types/node': 16.11.7 + '@types/pretty-hrtime': ^1.0.1 '@types/yargs': ^17.0.10 dotenv: ^16.0.0 fs-extra: ^10.1.0 @@ -69,7 +71,10 @@ importers: lodash: ^4.17.21 node-plop: ^0.26.3 ora: 5.4.1 + p-all: 2.1.0 + p-series: 2.1.0 plop: ^3.0.5 + pretty-ms: 7.0.1 tailchat-server-sdk: ^0.0.12 ts-node: ^10.7.0 typescript: ^4.6.3 @@ -82,13 +87,18 @@ importers: lodash: 4.17.21 node-plop: 0.26.3 ora: 5.4.1 + p-all: 2.1.0 + p-series: 2.1.0 plop: 3.1.1 + pretty-ms: 7.0.1 tailchat-server-sdk: link:../server/packages/sdk yargs: 17.5.1 devDependencies: '@types/fs-extra': 9.0.13 '@types/inquirer': 8.2.3 + '@types/lodash': 4.14.184 '@types/node': 16.11.7 + '@types/pretty-hrtime': 1.0.1 '@types/yargs': 17.0.12 ts-node: 10.9.1_rk33jgomelnuriwr3foeinccb4 typescript: 4.8.2 @@ -4181,6 +4191,11 @@ packages: resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} engines: {node: '>=6'} + /@sindresorhus/is/0.15.0: + resolution: {integrity: sha512-lu8BpxjAtRCAo5ifytTpCPCj99LF7o/2Myn+NXyNCBqvPYn7Pjd76AMmUB5l7XF1U6t0hcWrlEM5ESufW7wAeA==} + engines: {node: '>=6'} + dev: false + /@sindresorhus/is/0.7.0: resolution: {integrity: sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==} engines: {node: '>=4'} @@ -18170,7 +18185,6 @@ packages: engines: {node: '>=6'} dependencies: p-map: 2.1.0 - dev: true /p-cancelable/0.4.1: resolution: {integrity: sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==} @@ -18276,6 +18290,11 @@ packages: eventemitter3: 4.0.7 p-timeout: 3.2.0 + /p-reduce/2.1.0: + resolution: {integrity: sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==} + engines: {node: '>=8'} + dev: false + /p-retry/4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} @@ -18283,6 +18302,14 @@ packages: '@types/retry': 0.12.0 retry: 0.13.1 + /p-series/2.1.0: + resolution: {integrity: sha512-vEAnkG1ikRT1kPBrKwpj7AFYQkd1hjt/oHeppxtpoPxy5gEt+OWiHZJN3tMqvFa+UJfVwO3lwHoMUpMYBLKnaQ==} + engines: {node: '>=8'} + dependencies: + '@sindresorhus/is': 0.15.0 + p-reduce: 2.1.0 + dev: false + /p-timeout/2.0.1: resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==} engines: {node: '>=4'} @@ -18391,6 +18418,11 @@ packages: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + /parse-ms/2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + dev: false + /parse-node-version/1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -19512,6 +19544,13 @@ packages: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} + /pretty-ms/7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + dependencies: + parse-ms: 2.1.0 + dev: false + /pretty-time/1.1.0: resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} engines: {node: '>=4'}