范围: src/business/zulip/README.md - 补充对外提供的接口章节(14个公共方法) - 添加使用的项目内部依赖说明(7个依赖) - 完善核心特性描述(5个特性) - 添加潜在风险评估(4个风险及缓解措施) - 优化文档结构和内容完整性
366 lines
11 KiB
TypeScript
366 lines
11 KiB
TypeScript
/**
|
||
* Zulip聊天性能测试
|
||
*
|
||
* 功能描述:
|
||
* - 测试优化后聊天架构的性能表现
|
||
* - 验证游戏内实时广播 + Zulip异步同步的效果
|
||
* - 测试高并发场景下的系统稳定性
|
||
*
|
||
* 测试场景:
|
||
* - 单用户消息发送性能
|
||
* - 多用户并发聊天性能
|
||
* - 大量消息批量处理性能
|
||
* - 内存使用和资源清理
|
||
*
|
||
* 更新记录:
|
||
* - 2026-01-14: 重构后更新 - 使用新的四层架构模块
|
||
* - ChatService 替代 ZulipService
|
||
* - ChatSessionService 替代 SessionManagerService
|
||
* - ChatFilterService 替代 MessageFilterService
|
||
*
|
||
* @author moyin
|
||
* @version 2.0.0
|
||
* @since 2026-01-10
|
||
* @lastModified 2026-01-14
|
||
*/
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { ChatService } from '../../../src/business/chat/chat.service';
|
||
import { ChatSessionService } from '../../../src/business/chat/services/chat_session.service';
|
||
import { ChatFilterService } from '../../../src/business/chat/services/chat_filter.service';
|
||
import { ZulipClientPoolService } from '../../../src/core/zulip_core/services/zulip_client_pool.service';
|
||
|
||
// 模拟WebSocket网关
|
||
class MockWebSocketGateway {
|
||
private sentMessages: Array<{ socketId: string; data: any }> = [];
|
||
private broadcastMessages: Array<{ mapId: string; data: any }> = [];
|
||
|
||
sendToPlayer(socketId: string, data: any): void {
|
||
this.sentMessages.push({ socketId, data });
|
||
}
|
||
|
||
broadcastToMap(mapId: string, data: any, excludeId?: string): void {
|
||
this.broadcastMessages.push({ mapId, data });
|
||
}
|
||
|
||
getSentMessages() { return this.sentMessages; }
|
||
getBroadcastMessages() { return this.broadcastMessages; }
|
||
clearMessages() {
|
||
this.sentMessages = [];
|
||
this.broadcastMessages = [];
|
||
}
|
||
}
|
||
|
||
describe('Zulip聊天性能测试', () => {
|
||
let chatService: ChatService;
|
||
let sessionManager: ChatSessionService;
|
||
let mockWebSocketGateway: MockWebSocketGateway;
|
||
let mockZulipClientPool: any;
|
||
|
||
beforeAll(async () => {
|
||
// 创建模拟服务
|
||
mockZulipClientPool = {
|
||
sendMessage: jest.fn().mockResolvedValue({
|
||
success: true,
|
||
messageId: 'zulip-msg-123',
|
||
}),
|
||
createUserClient: jest.fn(),
|
||
destroyUserClient: jest.fn(),
|
||
};
|
||
|
||
const mockSessionManager = {
|
||
getSession: jest.fn().mockResolvedValue({
|
||
sessionId: 'test-session',
|
||
userId: 'user-123',
|
||
username: 'TestPlayer',
|
||
currentMap: 'whale_port',
|
||
position: { x: 100, y: 200 },
|
||
}),
|
||
injectContext: jest.fn().mockResolvedValue({
|
||
stream: 'Whale Port',
|
||
topic: 'Town Square Chat',
|
||
}),
|
||
getSocketsInMap: jest.fn().mockResolvedValue(['socket-1', 'socket-2', 'socket-3']),
|
||
createSession: jest.fn(),
|
||
destroySession: jest.fn(),
|
||
updatePlayerPosition: jest.fn().mockResolvedValue(true),
|
||
};
|
||
|
||
const mockMessageFilter = {
|
||
validateMessage: jest.fn().mockResolvedValue({
|
||
allowed: true,
|
||
filteredContent: null,
|
||
}),
|
||
};
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [
|
||
ChatService,
|
||
{
|
||
provide: 'ZULIP_CLIENT_POOL_SERVICE',
|
||
useValue: mockZulipClientPool,
|
||
},
|
||
{
|
||
provide: ChatSessionService,
|
||
useValue: mockSessionManager,
|
||
},
|
||
{
|
||
provide: ChatFilterService,
|
||
useValue: mockMessageFilter,
|
||
},
|
||
{
|
||
provide: 'API_KEY_SECURITY_SERVICE',
|
||
useValue: {
|
||
getApiKey: jest.fn().mockResolvedValue({
|
||
success: true,
|
||
apiKey: 'test-api-key',
|
||
}),
|
||
},
|
||
},
|
||
{
|
||
provide: 'LoginCoreService',
|
||
useValue: {
|
||
verifyToken: jest.fn().mockResolvedValue({
|
||
sub: 'user-123',
|
||
username: 'TestPlayer',
|
||
email: 'test@example.com',
|
||
}),
|
||
},
|
||
},
|
||
],
|
||
}).compile();
|
||
|
||
chatService = module.get<ChatService>(ChatService);
|
||
sessionManager = module.get<ChatSessionService>(ChatSessionService);
|
||
|
||
// 设置WebSocket网关
|
||
mockWebSocketGateway = new MockWebSocketGateway();
|
||
chatService.setWebSocketGateway(mockWebSocketGateway as any);
|
||
});
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks();
|
||
mockWebSocketGateway.clearMessages();
|
||
});
|
||
|
||
describe('单用户消息发送性能', () => {
|
||
it('应该在50ms内完成游戏内广播', async () => {
|
||
const startTime = Date.now();
|
||
|
||
const result = await chatService.sendChatMessage({
|
||
socketId: 'test-socket',
|
||
content: 'Performance test message',
|
||
scope: 'local',
|
||
});
|
||
|
||
const duration = Date.now() - startTime;
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(duration).toBeLessThan(50); // 游戏内广播应该在50ms内完成
|
||
|
||
console.log(`游戏内广播耗时: ${duration}ms`);
|
||
});
|
||
|
||
it('应该异步处理Zulip同步,不阻塞游戏聊天', async () => {
|
||
// 模拟Zulip同步延迟
|
||
mockZulipClientPool.sendMessage.mockImplementation(() =>
|
||
new Promise(resolve => setTimeout(() => resolve({
|
||
success: true,
|
||
messageId: 'delayed-msg',
|
||
}), 200))
|
||
);
|
||
|
||
const startTime = Date.now();
|
||
|
||
const result = await chatService.sendChatMessage({
|
||
socketId: 'test-socket',
|
||
content: 'Async test message',
|
||
scope: 'local',
|
||
});
|
||
|
||
const duration = Date.now() - startTime;
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(duration).toBeLessThan(100); // 不应该等待Zulip同步完成
|
||
|
||
console.log(`异步处理耗时: ${duration}ms`);
|
||
});
|
||
});
|
||
|
||
describe('多用户并发聊天性能', () => {
|
||
it('应该处理50个并发消息', async () => {
|
||
const messageCount = 50;
|
||
const startTime = Date.now();
|
||
|
||
const promises = Array.from({ length: messageCount }, (_, i) =>
|
||
chatService.sendChatMessage({
|
||
socketId: `socket-${i}`,
|
||
content: `Concurrent message ${i}`,
|
||
scope: 'local',
|
||
})
|
||
);
|
||
|
||
const results = await Promise.all(promises);
|
||
const duration = Date.now() - startTime;
|
||
|
||
// 验证所有消息都成功处理
|
||
expect(results).toHaveLength(messageCount);
|
||
results.forEach(result => {
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
const avgTimePerMessage = duration / messageCount;
|
||
console.log(`处理${messageCount}条并发消息耗时: ${duration}ms, 平均每条: ${avgTimePerMessage.toFixed(2)}ms`);
|
||
|
||
// 期望平均每条消息处理时间不超过20ms
|
||
expect(avgTimePerMessage).toBeLessThan(20);
|
||
}, 10000);
|
||
|
||
it('应该正确广播给地图内的所有玩家', async () => {
|
||
await chatService.sendChatMessage({
|
||
socketId: 'sender-socket',
|
||
content: 'Broadcast test message',
|
||
scope: 'local',
|
||
});
|
||
|
||
// 验证广播消息
|
||
const broadcastMessages = mockWebSocketGateway.getBroadcastMessages();
|
||
expect(broadcastMessages).toHaveLength(1);
|
||
|
||
const broadcastMessage = broadcastMessages[0];
|
||
expect(broadcastMessage.mapId).toBe('whale_port');
|
||
expect(broadcastMessage.data.t).toBe('chat_render');
|
||
expect(broadcastMessage.data.txt).toBe('Broadcast test message');
|
||
});
|
||
});
|
||
|
||
describe('批量消息处理性能', () => {
|
||
it('应该高效处理大量消息', async () => {
|
||
const batchSize = 100;
|
||
const startTime = Date.now();
|
||
|
||
// 创建批量消息
|
||
const batchPromises = Array.from({ length: batchSize }, (_, i) =>
|
||
chatService.sendChatMessage({
|
||
socketId: 'batch-socket',
|
||
content: `Batch message ${i}`,
|
||
scope: 'local',
|
||
})
|
||
);
|
||
|
||
const results = await Promise.all(batchPromises);
|
||
const duration = Date.now() - startTime;
|
||
|
||
// 验证处理结果
|
||
expect(results).toHaveLength(batchSize);
|
||
results.forEach((result, index) => {
|
||
expect(result.success).toBe(true);
|
||
expect(result.messageId).toBeDefined();
|
||
});
|
||
|
||
const throughput = (batchSize / duration) * 1000; // 每秒处理的消息数
|
||
console.log(`批量处理${batchSize}条消息耗时: ${duration}ms, 吞吐量: ${throughput.toFixed(2)} msg/s`);
|
||
|
||
// 期望吞吐量至少达到500 msg/s
|
||
expect(throughput).toBeGreaterThan(500);
|
||
}, 15000);
|
||
});
|
||
|
||
describe('内存使用和资源清理', () => {
|
||
it('应该正确清理会话资源', async () => {
|
||
// 创建多个会话
|
||
const sessionCount = 10;
|
||
const sessionIds = Array.from({ length: sessionCount }, (_, i) => `session-${i}`);
|
||
|
||
// 模拟会话创建
|
||
for (const sessionId of sessionIds) {
|
||
await chatService.handlePlayerLogin({
|
||
socketId: sessionId,
|
||
token: 'valid-jwt-token',
|
||
});
|
||
}
|
||
|
||
// 清理所有会话
|
||
for (const sessionId of sessionIds) {
|
||
await chatService.handlePlayerLogout(sessionId);
|
||
}
|
||
|
||
// 验证资源清理
|
||
expect(mockZulipClientPool.destroyUserClient).toHaveBeenCalledTimes(sessionCount);
|
||
});
|
||
|
||
it('应该处理内存压力测试', async () => {
|
||
const initialMemory = process.memoryUsage();
|
||
|
||
// 创建大量临时对象
|
||
const largeDataSet = Array.from({ length: 1000 }, (_, i) => ({
|
||
id: i,
|
||
data: 'x'.repeat(1000), // 1KB per object
|
||
timestamp: new Date(),
|
||
}));
|
||
|
||
// 处理大量消息
|
||
const promises = largeDataSet.map((item, i) =>
|
||
chatService.sendChatMessage({
|
||
socketId: `memory-test-${i}`,
|
||
content: `Memory test ${item.id}: ${item.data.substring(0, 50)}...`,
|
||
scope: 'local',
|
||
})
|
||
);
|
||
|
||
await Promise.all(promises);
|
||
|
||
// 强制垃圾回收(如果可用)
|
||
if (global.gc) {
|
||
global.gc();
|
||
}
|
||
|
||
const finalMemory = process.memoryUsage();
|
||
const memoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed;
|
||
|
||
console.log(`内存使用增加: ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB`);
|
||
|
||
// 期望内存增加不超过50MB
|
||
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024);
|
||
}, 20000);
|
||
});
|
||
|
||
describe('错误处理性能', () => {
|
||
it('应该快速处理无效会话', async () => {
|
||
const startTime = Date.now();
|
||
|
||
const result = await chatService.sendChatMessage({
|
||
socketId: 'invalid-socket',
|
||
content: 'This should fail quickly',
|
||
scope: 'local',
|
||
});
|
||
|
||
const duration = Date.now() - startTime;
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toContain('会话不存在');
|
||
expect(duration).toBeLessThan(10); // 错误处理应该很快
|
||
|
||
console.log(`错误处理耗时: ${duration}ms`);
|
||
});
|
||
|
||
it('应该处理Zulip服务异常而不影响游戏聊天', async () => {
|
||
// 模拟Zulip服务异常
|
||
mockZulipClientPool.sendMessage.mockRejectedValue(new Error('Zulip service unavailable'));
|
||
|
||
const result = await chatService.sendChatMessage({
|
||
socketId: 'test-socket',
|
||
content: 'Message during Zulip outage',
|
||
scope: 'local',
|
||
});
|
||
|
||
// 游戏内聊天应该仍然成功
|
||
expect(result.success).toBe(true);
|
||
|
||
// 验证游戏内广播仍然工作
|
||
const broadcastMessages = mockWebSocketGateway.getBroadcastMessages();
|
||
expect(broadcastMessages).toHaveLength(1);
|
||
});
|
||
});
|
||
});
|