/** * Zulip聊天性能测试 * * 功能描述: * - 测试优化后聊天架构的性能表现 * - 验证游戏内实时广播 + Zulip异步同步的效果 * - 测试高并发场景下的系统稳定性 * * 测试场景: * - 单用户消息发送性能 * - 多用户并发聊天性能 * - 大量消息批量处理性能 * - 内存使用和资源清理 * * @author moyin * @version 1.0.0 * @since 2026-01-10 */ import { Test, TestingModule } from '@nestjs/testing'; import { ZulipService } from '../../../src/business/zulip/zulip.service'; import { ZulipClientPoolService } from '../../../src/core/zulip_core/services/zulip_client_pool.service'; import { SessionManagerService } from '../../../src/business/zulip/services/session_manager.service'; import { MessageFilterService } from '../../../src/business/zulip/services/message_filter.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 zulipService: ZulipService; let sessionManager: SessionManagerService; 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: [ ZulipService, { provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockZulipClientPool, }, { provide: SessionManagerService, useValue: mockSessionManager, }, { provide: MessageFilterService, 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(); zulipService = module.get(ZulipService); sessionManager = module.get(SessionManagerService); // 设置WebSocket网关 mockWebSocketGateway = new MockWebSocketGateway(); zulipService.setWebSocketGateway(mockWebSocketGateway as any); }); beforeEach(() => { jest.clearAllMocks(); mockWebSocketGateway.clearMessages(); }); describe('单用户消息发送性能', () => { it('应该在50ms内完成游戏内广播', async () => { const startTime = Date.now(); const result = await zulipService.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 zulipService.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) => zulipService.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 zulipService.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) => zulipService.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 zulipService.handlePlayerLogin({ socketId: sessionId, token: 'valid-jwt-token', }); } // 清理所有会话 for (const sessionId of sessionIds) { await zulipService.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) => zulipService.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 zulipService.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 zulipService.sendChatMessage({ socketId: 'test-socket', content: 'Message during Zulip outage', scope: 'local', }); // 游戏内聊天应该仍然成功 expect(result.success).toBe(true); // 验证游戏内广播仍然工作 const broadcastMessages = mockWebSocketGateway.getBroadcastMessages(); expect(broadcastMessages).toHaveLength(1); }); }); });