Files
whale-town-end/test/zulip_integration/performance/chat_performance.spec.ts
moyin ed04b8c92d docs(zulip): 完善Zulip业务模块功能文档
范围: src/business/zulip/README.md
- 补充对外提供的接口章节(14个公共方法)
- 添加使用的项目内部依赖说明(7个依赖)
- 完善核心特性描述(5个特性)
- 添加潜在风险评估(4个风险及缓解措施)
- 优化文档结构和内容完整性
2026-01-15 10:53:04 +08:00

366 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});
});
});