perf(cli): optimize the experience of the benchmark command

in the case of large data volume pressure test
pull/90/head
moonrailgun 2 years ago
parent 57f07c27f5
commit 2d6eaac96a

@ -1,6 +1,6 @@
{ {
"name": "tailchat-cli", "name": "tailchat-cli",
"version": "1.5.9", "version": "1.5.10",
"description": "A Command line interface of tailchat", "description": "A Command line interface of tailchat",
"bin": { "bin": {
"tailchat": "./bin/cli" "tailchat": "./bin/cli"
@ -54,6 +54,7 @@
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"ora": "5.4.1", "ora": "5.4.1",
"p-all": "2.1.0", "p-all": "2.1.0",
"p-map": "^4.0.0",
"p-series": "2.1.0", "p-series": "2.1.0",
"pidusage": "^3.0.2", "pidusage": "^3.0.2",
"plop": "^3.0.5", "plop": "^3.0.5",

@ -4,6 +4,7 @@ import msgpackParser from 'socket.io-msgpack-parser';
import fs from 'fs-extra'; import fs from 'fs-extra';
import ora from 'ora'; import ora from 'ora';
import randomString from 'crypto-random-string'; import randomString from 'crypto-random-string';
import pMap from 'p-map';
const CLIENT_CREATION_INTERVAL_IN_MS = 5; const CLIENT_CREATION_INTERVAL_IN_MS = 5;
@ -19,6 +20,11 @@ export const benchmarkConnectionsCommand: CommandModule = {
type: 'string', type: 'string',
default: './accounts', default: './accounts',
}) })
.option('concurrency', {
describe: 'Concurrency when create connection',
type: 'number',
default: 1,
})
.option('groupId', { .option('groupId', {
describe: 'Group Id which send Message', describe: 'Group Id which send Message',
type: 'string', type: 'string',
@ -26,12 +32,19 @@ export const benchmarkConnectionsCommand: CommandModule = {
.option('converseId', { .option('converseId', {
describe: 'Converse Id which send Message', describe: 'Converse Id which send Message',
type: 'string', type: 'string',
})
.option('messageNum', {
describe: 'Times which send Message',
type: 'number',
default: 1,
}), }),
async handler(args) { async handler(args) {
const url = args.url as string; const url = args.url as string;
const file = args.file as string; const file = args.file as string;
const groupId = args.groupId as string; const groupId = args.groupId as string;
const converseId = args.converseId as string; const converseId = args.converseId as string;
const messageNum = args.messageNum as number;
const concurrency = args.concurrency as number;
console.log('Reading account tokens from', file); console.log('Reading account tokens from', file);
const account = await fs.readFile(file as string, { const account = await fs.readFile(file as string, {
@ -39,64 +52,42 @@ export const benchmarkConnectionsCommand: CommandModule = {
}); });
const sockets = await createClients( const sockets = await createClients(
url as string, url as string,
account.split('\n').map((s) => s.trim()) account.split('\n').map((s) => s.trim()),
concurrency
); );
if (groupId && converseId) { if (groupId && converseId) {
// send message test // send message test
const randomMessage = randomString({ length: 16 }); for (let i = 0; i < messageNum; i++) {
const spinner = ora() console.log('Start send message test:', i + 1);
.info(`Start message receive test, message: ${randomMessage}`) await sendMessage(sockets, groupId, converseId);
.start();
const start = Date.now();
let receiveCount = 0;
const len = sockets.length;
function receivedCallback() {
receiveCount += 1;
spinner.text = `Receive: ${receiveCount}/${len}`;
if (receiveCount === len) {
spinner.succeed(
`All client received, usage: ${Date.now() - start}ms`
);
}
}
sockets.forEach((socket) => {
socket.on('notify:chat.message.add', (message) => {
const content = message.content;
if (message.converseId === converseId && randomMessage === content) {
receivedCallback();
} }
});
});
sockets[0].emit('chat.message.sendMessage', {
groupId,
converseId,
content: randomMessage,
});
} }
}, },
}; };
async function createClients( async function createClients(
url: string, url: string,
accountTokens: string[] accountTokens: string[],
concurrency: number
): Promise<Socket[]> { ): Promise<Socket[]> {
const maxCount = accountTokens.length; const maxCount = accountTokens.length;
const spinner = ora().info(`Create Client Connection to ${url}`).start(); const spinner = ora().info(`Create Client Connection to ${url}`).start();
let i = 0; let i = 0;
const sockets: Socket[] = []; const sockets: Socket[] = [];
for (const token of accountTokens) { await pMap(
accountTokens,
async (token) => {
await sleep(CLIENT_CREATION_INTERVAL_IN_MS); await sleep(CLIENT_CREATION_INTERVAL_IN_MS);
spinner.text = `Progress: ${++i}/${maxCount}`;
const socket = await createClient(url, token); const socket = await createClient(url, token);
spinner.text = `Progress: ${++i}/${maxCount}`;
sockets.push(socket); sockets.push(socket);
},
{
concurrency,
} }
);
spinner.succeed(`${maxCount} clients has been create.`); spinner.succeed(`${maxCount} clients has been create.`);
@ -131,7 +122,51 @@ function createClient(url: string, token: string): Promise<Socket> {
}); });
} }
export function sleep(milliseconds: number): Promise<void> { async function sendMessage(
sockets: Socket[],
groupId: string,
converseId: string
) {
return new Promise<void>((resolve) => {
const randomMessage = randomString({ length: 16 });
const spinner = ora()
.info(`Start message receive test, message: ${randomMessage}`)
.start();
const start = Date.now();
let receiveCount = 0;
const len = sockets.length;
function receivedCallback() {
receiveCount += 1;
spinner.text = `Receive: ${receiveCount}/${len}`;
if (receiveCount === len) {
spinner.succeed(`All client received, usage: ${Date.now() - start}ms`);
resolve();
}
}
sockets.forEach((socket) => {
socket.on('notify:chat.message.add', (message) => {
const content = message.content;
if (message.converseId === converseId && randomMessage === content) {
socket.off('notify:chat.message.add');
receivedCallback();
}
});
});
sockets[0].emit('chat.message.sendMessage', {
groupId,
converseId,
content: randomMessage,
});
});
}
function sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(resolve, milliseconds); setTimeout(resolve, milliseconds);
}); });

@ -2,6 +2,7 @@ import { CommandModule } from 'yargs';
import fs from 'fs-extra'; import fs from 'fs-extra';
import got from 'got'; import got from 'got';
import ora from 'ora'; import ora from 'ora';
import pMap from 'p-map';
export const benchmarkRegisterCommand: CommandModule = { export const benchmarkRegisterCommand: CommandModule = {
command: 'register <url>', command: 'register <url>',
@ -25,33 +26,58 @@ export const benchmarkRegisterCommand: CommandModule = {
type: 'number', type: 'number',
default: 100, default: 100,
}) })
.option('concurrency', {
describe: 'Concurrency when send request',
type: 'number',
default: 1,
})
.option('invite', { .option('invite', {
describe: 'Invite Code', describe: 'Invite Code',
type: 'string', type: 'string',
})
.option('append', {
describe: 'Append mode',
type: 'boolean',
}), }),
async handler(args) { async handler(args) {
const url = args.url as string; const url = args.url as string;
const file = args.file as string;
const count = args.count as number; const count = args.count as number;
const concurrency = args.concurrency as number;
const invite = args.invite as string | undefined; const invite = args.invite as string | undefined;
const append = (args.append ?? false) as boolean;
const tokens: string[] = []; const tokens: string[] = [];
const start = Date.now(); const start = Date.now();
const spinner = ora().info(`Register temporary account`).start(); const spinner = ora().info(`Register temporary account`).start();
for (let i = 0; i < count; i++) { let i = 0;
spinner.text = `Progress: ${i + 1}/${count}`; spinner.text = `Progress: ${i}/${count}`;
await pMap(
Array.from({ length: count }),
async () => {
const token = await registerTemporaryAccount(url, `benchUser-${i}`); const token = await registerTemporaryAccount(url, `benchUser-${i}`);
if (invite) { if (invite) {
// Apply group invite // Apply group invite
applyGroupInviteCode(url, token, invite); await applyGroupInviteCode(url, token, invite);
} }
if (append) {
await fs.appendFile(file, `\n${token}`);
}
spinner.text = `Progress: ${++i}/${count}`;
tokens.push(token); tokens.push(token);
},
{
concurrency,
} }
);
spinner.info(`Writing tokens into path: ${args.file}`); if (!append) {
spinner.info(`Writing tokens into path: ${file}`);
await fs.writeFile(args.file as string, tokens.join('\n')); await fs.writeFile(file, tokens.join('\n'));
}
spinner.succeed(`Register completed! Usage: ${Date.now() - start}ms`); spinner.succeed(`Register completed! Usage: ${Date.now() - start}ms`);
}, },
@ -66,6 +92,7 @@ async function registerTemporaryAccount(
json: { json: {
nickname, nickname,
}, },
retry: 5,
}) })
.json<{ data: { token: string } }>(); .json<{ data: { token: string } }>();
@ -81,6 +108,7 @@ async function applyGroupInviteCode(
json: { json: {
code: inviteCode, code: inviteCode,
}, },
retry: 5,
headers: { headers: {
'X-Token': token, 'X-Token': token,
}, },

@ -167,6 +167,9 @@ importers:
p-all: p-all:
specifier: 2.1.0 specifier: 2.1.0
version: 2.1.0 version: 2.1.0
p-map:
specifier: ^4.0.0
version: 4.0.0
p-series: p-series:
specifier: 2.1.0 specifier: 2.1.0
version: 2.1.0 version: 2.1.0
@ -568,7 +571,7 @@ importers:
version: 0.32.11 version: 0.32.11
zustand: zustand:
specifier: ^4.3.6 specifier: ^4.3.6
version: 4.3.6(immer@9.0.15)(react@18.2.0) version: 4.3.6(immer@9.0.21)(react@18.2.0)
devDependencies: devDependencies:
'@types/crc': '@types/crc':
specifier: ^3.4.0 specifier: ^3.4.0
@ -1836,7 +1839,7 @@ importers:
version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
zustand: zustand:
specifier: ^4.3.6 specifier: ^4.3.6
version: 4.3.6(immer@9.0.15)(react@18.2.0) version: 4.3.6(immer@9.0.21)(react@18.2.0)
server/plugins/com.msgbyte.getui: server/plugins/com.msgbyte.getui:
dependencies: dependencies:
@ -2014,7 +2017,7 @@ importers:
version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
zustand: zustand:
specifier: ^4.3.6 specifier: ^4.3.6
version: 4.3.6(immer@9.0.15)(react@18.2.0) version: 4.3.6(immer@9.0.21)(react@18.2.0)
server/plugins/com.msgbyte.welcome: server/plugins/com.msgbyte.welcome:
dependencies: dependencies:
@ -21874,10 +21877,10 @@ packages:
/immer@9.0.15: /immer@9.0.15:
resolution: {integrity: sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==} resolution: {integrity: sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==}
dev: false
/immer@9.0.21: /immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
dev: false
/import-fresh@2.0.0: /import-fresh@2.0.0:
resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}
@ -36599,6 +36602,7 @@ packages:
immer: 9.0.15 immer: 9.0.15
react: 18.2.0 react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/zustand@4.3.6(immer@9.0.21)(react@18.2.0): /zustand@4.3.6(immer@9.0.21)(react@18.2.0):
resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==} resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==}
@ -36615,7 +36619,6 @@ packages:
immer: 9.0.21 immer: 9.0.21
react: 18.2.0 react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/zwitch@1.0.5: /zwitch@1.0.5:
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}

Loading…
Cancel
Save