forked from datawhale/whale-town-end
515 lines
16 KiB
TypeScript
515 lines
16 KiB
TypeScript
/**
|
|
* 位置更新性能测试
|
|
*
|
|
* 功能描述:
|
|
* - 测试位置更新的性能指标
|
|
* - 验证系统在高负载下的表现
|
|
* - 确保响应时间满足要求
|
|
* - 提供性能基准数据
|
|
*
|
|
* 测试指标:
|
|
* - 位置更新响应时间
|
|
* - 并发用户处理能力
|
|
* - 内存使用情况
|
|
* - 系统吞吐量
|
|
*
|
|
* 最近修改:
|
|
* - 2026-01-08: 文件重命名 - 修正kebab-case为snake_case命名规范 (修改者: moyin)
|
|
*
|
|
* @author moyin
|
|
* @version 1.0.1
|
|
* @since 2026-01-08
|
|
* @lastModified 2026-01-08
|
|
*/
|
|
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { INestApplication } from '@nestjs/common';
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { LocationBroadcastModule } from '../../location_broadcast.module';
|
|
import { RedisModule } from '../../../../core/redis/redis.module';
|
|
import { LoginCoreModule } from '../../../../core/login_core/login_core.module';
|
|
import { UserProfilesModule } from '../../../../core/db/user_profiles/user_profiles.module';
|
|
|
|
describe('位置更新性能测试', () => {
|
|
let app: INestApplication;
|
|
let authToken: string;
|
|
|
|
beforeAll(async () => {
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [
|
|
LocationBroadcastModule,
|
|
RedisModule,
|
|
LoginCoreModule,
|
|
UserProfilesModule.forMemory(),
|
|
],
|
|
}).compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
await app.init();
|
|
await app.listen(0);
|
|
|
|
authToken = 'test-jwt-token';
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
describe('单用户位置更新性能', () => {
|
|
let client: Socket;
|
|
|
|
beforeEach((done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client.on('connect', () => {
|
|
client.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId: 'perf-session-001',
|
|
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
|
});
|
|
});
|
|
|
|
client.on('session_joined', () => {
|
|
done();
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (client) {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
it('应该在100ms内响应位置更新', (done) => {
|
|
const startTime = Date.now();
|
|
|
|
client.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 150,
|
|
y: 250,
|
|
mapId: 'plaza',
|
|
timestamp: startTime,
|
|
});
|
|
|
|
client.on('position_update_success', () => {
|
|
const responseTime = Date.now() - startTime;
|
|
console.log(`位置更新响应时间: ${responseTime}ms`);
|
|
|
|
expect(responseTime).toBeLessThan(100);
|
|
done();
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
done(error);
|
|
});
|
|
});
|
|
|
|
it('应该支持高频率位置更新', (done) => {
|
|
const updateCount = 100;
|
|
let completedUpdates = 0;
|
|
|
|
const startTime = Date.now();
|
|
|
|
for (let i = 0; i < updateCount; i++) {
|
|
const updateStartTime = Date.now();
|
|
|
|
client.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 100 + i,
|
|
y: 200 + i,
|
|
mapId: 'plaza',
|
|
timestamp: updateStartTime,
|
|
});
|
|
}
|
|
|
|
client.on('position_update_success', () => {
|
|
completedUpdates++;
|
|
|
|
if (completedUpdates === updateCount) {
|
|
const totalTime = Date.now() - startTime;
|
|
const avgTime = totalTime / updateCount;
|
|
|
|
console.log(`${updateCount}次位置更新总耗时: ${totalTime}ms`);
|
|
console.log(`平均每次更新耗时: ${avgTime}ms`);
|
|
console.log(`更新频率: ${(updateCount / totalTime * 1000).toFixed(2)} updates/sec`);
|
|
|
|
expect(avgTime).toBeLessThan(50); // 平均响应时间应小于50ms
|
|
done();
|
|
}
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
done(error);
|
|
});
|
|
|
|
// 超时保护
|
|
const timeoutId = setTimeout(() => {
|
|
if (completedUpdates < updateCount) {
|
|
done(new Error(`只完成了 ${completedUpdates}/${updateCount} 次更新`));
|
|
}
|
|
}, 10000);
|
|
});
|
|
});
|
|
|
|
describe('多用户并发性能', () => {
|
|
it('应该支持100个并发用户', (done) => {
|
|
const userCount = 100;
|
|
const clients: Socket[] = [];
|
|
const sessionId = 'perf-session-concurrent';
|
|
let connectedUsers = 0;
|
|
let joinedUsers = 0;
|
|
let updateResponses = 0;
|
|
|
|
const port = app.getHttpServer().address().port;
|
|
const startTime = Date.now();
|
|
|
|
// 创建多个客户端连接
|
|
for (let i = 0; i < userCount; i++) {
|
|
const client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
clients.push(client);
|
|
|
|
client.on('connect', () => {
|
|
connectedUsers++;
|
|
|
|
if (connectedUsers === userCount) {
|
|
const connectTime = Date.now() - startTime;
|
|
console.log(`${userCount}个用户连接耗时: ${connectTime}ms`);
|
|
|
|
// 所有用户加入会话
|
|
clients.forEach((c, index) => {
|
|
c.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: {
|
|
x: 100 + index,
|
|
y: 200 + index,
|
|
mapId: 'plaza',
|
|
},
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
client.on('session_joined', () => {
|
|
joinedUsers++;
|
|
|
|
if (joinedUsers === userCount) {
|
|
const joinTime = Date.now() - startTime;
|
|
console.log(`${userCount}个用户加入会话耗时: ${joinTime}ms`);
|
|
|
|
// 所有用户同时更新位置
|
|
clients.forEach((c, index) => {
|
|
c.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 200 + index,
|
|
y: 300 + index,
|
|
mapId: 'plaza',
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
client.on('position_update_success', () => {
|
|
updateResponses++;
|
|
|
|
if (updateResponses === userCount) {
|
|
const totalTime = Date.now() - startTime;
|
|
console.log(`${userCount}个用户完整流程耗时: ${totalTime}ms`);
|
|
console.log(`平均每用户耗时: ${(totalTime / userCount).toFixed(2)}ms`);
|
|
|
|
// 清理连接
|
|
clients.forEach(c => c.disconnect());
|
|
|
|
expect(totalTime).toBeLessThan(10000); // 总时间应小于10秒
|
|
done();
|
|
}
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
clients.forEach(c => c.disconnect());
|
|
done(error);
|
|
});
|
|
}
|
|
|
|
// 超时保护
|
|
const timeoutId = setTimeout(() => {
|
|
clients.forEach(c => c.disconnect());
|
|
done(new Error(`测试超时,连接用户: ${connectedUsers}, 加入用户: ${joinedUsers}, 更新响应: ${updateResponses}`));
|
|
}, 30000);
|
|
});
|
|
|
|
it('应该支持持续的位置广播', (done) => {
|
|
const userCount = 10;
|
|
const updatesPerUser = 50;
|
|
const clients: Socket[] = [];
|
|
const sessionId = 'perf-session-broadcast';
|
|
let totalBroadcasts = 0;
|
|
let expectedBroadcasts = userCount * updatesPerUser * (userCount - 1); // 每次更新广播给其他用户
|
|
|
|
const port = app.getHttpServer().address().port;
|
|
const startTime = Date.now();
|
|
|
|
// 创建多个客户端
|
|
for (let i = 0; i < userCount; i++) {
|
|
const client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
clients.push(client);
|
|
|
|
client.on('connect', () => {
|
|
client.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: {
|
|
x: 100 + i * 10,
|
|
y: 200 + i * 10,
|
|
mapId: 'plaza',
|
|
},
|
|
});
|
|
});
|
|
|
|
client.on('position_broadcast', () => {
|
|
totalBroadcasts++;
|
|
|
|
if (totalBroadcasts >= expectedBroadcasts * 0.8) { // 允许80%的广播成功
|
|
const totalTime = Date.now() - startTime;
|
|
const broadcastRate = totalBroadcasts / totalTime * 1000;
|
|
|
|
console.log(`广播测试完成: ${totalBroadcasts}/${expectedBroadcasts} 条广播`);
|
|
console.log(`总耗时: ${totalTime}ms`);
|
|
console.log(`广播频率: ${broadcastRate.toFixed(2)} broadcasts/sec`);
|
|
|
|
clients.forEach(c => c.disconnect());
|
|
done();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 等待所有用户连接后开始更新
|
|
const startUpdateTimer = setTimeout(() => {
|
|
clients.forEach((client, userIndex) => {
|
|
for (let updateIndex = 0; updateIndex < updatesPerUser; updateIndex++) {
|
|
const updateTimer = setTimeout(() => {
|
|
client.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 100 + userIndex * 10 + updateIndex,
|
|
y: 200 + userIndex * 10 + updateIndex,
|
|
mapId: 'plaza',
|
|
});
|
|
}, updateIndex * 10); // 每10ms发送一次更新
|
|
}
|
|
});
|
|
}, 1000);
|
|
|
|
// 超时保护
|
|
const timeoutId = setTimeout(() => {
|
|
clients.forEach(c => c.disconnect());
|
|
console.log(`测试超时,收到广播: ${totalBroadcasts}/${expectedBroadcasts}`);
|
|
done();
|
|
}, 20000);
|
|
});
|
|
});
|
|
|
|
describe('内存和资源使用', () => {
|
|
it('应该在合理范围内使用内存', async () => {
|
|
const initialMemory = process.memoryUsage();
|
|
const userCount = 50;
|
|
const clients: Socket[] = [];
|
|
const port = app.getHttpServer().address().port;
|
|
|
|
// 创建多个连接
|
|
for (let i = 0; i < userCount; i++) {
|
|
const client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
clients.push(client);
|
|
}
|
|
|
|
// 等待连接建立
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
const peakMemory = process.memoryUsage();
|
|
const memoryIncrease = peakMemory.heapUsed - initialMemory.heapUsed;
|
|
const memoryPerUser = memoryIncrease / userCount;
|
|
|
|
console.log(`初始内存使用: ${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
|
console.log(`峰值内存使用: ${(peakMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
|
console.log(`内存增长: ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB`);
|
|
console.log(`每用户内存: ${(memoryPerUser / 1024).toFixed(2)} KB`);
|
|
|
|
// 清理连接
|
|
clients.forEach(c => c.disconnect());
|
|
|
|
// 等待清理完成
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
const finalMemory = process.memoryUsage();
|
|
console.log(`清理后内存: ${(finalMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
|
|
|
// 每个用户的内存使用应该小于100KB
|
|
expect(memoryPerUser).toBeLessThan(100 * 1024);
|
|
});
|
|
|
|
it('应该正确清理断开连接的用户', (done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
const sessionId = 'cleanup-test-session';
|
|
|
|
const client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
client.on('connect', () => {
|
|
client.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
|
});
|
|
});
|
|
|
|
client.on('session_joined', () => {
|
|
// 突然断开连接
|
|
client.disconnect();
|
|
|
|
// 等待系统清理
|
|
setTimeout(() => {
|
|
// 创建新连接验证清理是否成功
|
|
const newClient = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
newClient.on('connect', () => {
|
|
newClient.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
|
});
|
|
});
|
|
|
|
newClient.on('session_joined', (response) => {
|
|
// 如果能成功加入,说明之前的用户已被清理
|
|
expect(response.success).toBe(true);
|
|
newClient.disconnect();
|
|
done();
|
|
});
|
|
|
|
newClient.on('error', (error) => {
|
|
newClient.disconnect();
|
|
done(error);
|
|
});
|
|
}, 2000);
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
done(error);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('压力测试', () => {
|
|
it('应该在高频率更新下保持稳定', (done) => {
|
|
const port = app.getHttpServer().address().port;
|
|
const sessionId = 'stress-test-session';
|
|
const updateInterval = 10; // 10ms间隔
|
|
const testDuration = 5000; // 5秒测试
|
|
|
|
let updateCount = 0;
|
|
let responseCount = 0;
|
|
let errorCount = 0;
|
|
let updateTimer: NodeJS.Timeout | null = null;
|
|
let timeoutTimer: NodeJS.Timeout | null = null;
|
|
|
|
const client = io(`http://localhost:${port}/location-broadcast`, {
|
|
auth: { token: authToken },
|
|
transports: ['websocket'],
|
|
});
|
|
|
|
const cleanup = () => {
|
|
if (updateTimer) {
|
|
clearInterval(updateTimer);
|
|
updateTimer = null;
|
|
}
|
|
if (timeoutTimer) {
|
|
clearTimeout(timeoutTimer);
|
|
timeoutTimer = null;
|
|
}
|
|
if (client && client.connected) {
|
|
client.disconnect();
|
|
}
|
|
};
|
|
|
|
client.on('connect', () => {
|
|
client.emit('join_session', {
|
|
type: 'join_session',
|
|
sessionId,
|
|
initialPosition: { x: 100, y: 200, mapId: 'plaza' },
|
|
});
|
|
});
|
|
|
|
client.on('session_joined', () => {
|
|
const startTime = Date.now();
|
|
|
|
updateTimer = setInterval(() => {
|
|
if (Date.now() - startTime >= testDuration) {
|
|
clearInterval(updateTimer!);
|
|
updateTimer = null;
|
|
|
|
// 等待最后的响应
|
|
setTimeout(() => {
|
|
const successRate = (responseCount / updateCount) * 100;
|
|
const errorRate = (errorCount / updateCount) * 100;
|
|
|
|
console.log(`压力测试结果:`);
|
|
console.log(`- 发送更新: ${updateCount}`);
|
|
console.log(`- 成功响应: ${responseCount}`);
|
|
console.log(`- 错误数量: ${errorCount}`);
|
|
console.log(`- 成功率: ${successRate.toFixed(2)}%`);
|
|
console.log(`- 错误率: ${errorRate.toFixed(2)}%`);
|
|
|
|
cleanup();
|
|
|
|
// 成功率应该大于95%
|
|
expect(successRate).toBeGreaterThan(95);
|
|
done();
|
|
}, 1000);
|
|
return;
|
|
}
|
|
|
|
updateCount++;
|
|
client.emit('position_update', {
|
|
type: 'position_update',
|
|
x: 100 + (updateCount % 100),
|
|
y: 200 + (updateCount % 100),
|
|
mapId: 'plaza',
|
|
});
|
|
}, updateInterval);
|
|
});
|
|
|
|
client.on('position_update_success', () => {
|
|
responseCount++;
|
|
});
|
|
|
|
client.on('error', () => {
|
|
errorCount++;
|
|
});
|
|
|
|
// 超时保护
|
|
timeoutTimer = setTimeout(() => {
|
|
cleanup();
|
|
done(new Error('压力测试超时'));
|
|
}, testDuration + 5000);
|
|
});
|
|
});
|
|
}); |