package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "testing" "time" "git.x2erp.com/qdy/go-svc-code/internal/opencode" ) // TestMessageResponseTimeComparison 性能对比测试:比较8787端口和8020端口的历史消息加载性能 func TestMessageResponseTimeComparison(t *testing.T) { // 测试会话ID - 使用现有会话 sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z" t.Logf("🚀 开始性能对比测试") t.Logf("测试会话ID: %s", sessionID) // 1. 测试8787端口(直接opencode服务) t.Run("DirectOpenCode_8787", func(t *testing.T) { directURL := "http://localhost:8787" // 检查服务是否可用 if !isServiceRunningForMessages(t, directURL) { t.Skipf("8787端口服务不可用,跳过测试") return } t.Logf("📊 测试8787端口直接访问...") // 测试不同消息数量的加载性能 testCases := []struct { name string limit int repeat int // 重复次数 }{ {"limit_10", 10, 5}, {"limit_50", 50, 5}, {"limit_100", 100, 5}, {"no_limit", 0, 3}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var totalDuration time.Duration var successes int var firstByteTimes []time.Duration for i := 0; i < tc.repeat; i++ { duration, firstByte, err := measureDirectRequest(directURL, sessionID, tc.limit) if err != nil { t.Logf("⚠️ 第%d次请求失败: %v", i+1, err) continue } totalDuration += duration firstByteTimes = append(firstByteTimes, firstByte) successes++ // 请求间短暂延迟 time.Sleep(100 * time.Millisecond) } if successes > 0 { avgDuration := totalDuration / time.Duration(successes) var avgFirstByte time.Duration for _, fb := range firstByteTimes { avgFirstByte += fb } avgFirstByte /= time.Duration(len(firstByteTimes)) t.Logf("✅ %s: 成功%d/%d次, 平均总时间: %v, 平均首字节: %v", tc.name, successes, tc.repeat, avgDuration, avgFirstByte) } else { t.Errorf("❌ %s: 所有请求都失败", tc.name) } }) } }) // 2. 测试8020端口(svc-code代理) t.Run("SvcCodeProxy_8020", func(t *testing.T) { proxyURL := "http://localhost:8020" // 检查服务是否可用 if !isServiceRunningForMessages(t, proxyURL) { t.Skipf("8020端口服务不可用,跳过测试") return } // 获取认证token token, err := loginAndGetTokenForMessagesTest(t, proxyURL) if err != nil { t.Skipf("获取认证token失败: %v,跳过8020端口测试", err) return } t.Logf("📊 测试8020端口代理访问...") t.Logf("Token获取成功(前20位): %s...", token[:minForMessagesTest(20, len(token))]) // 测试不同消息数量的加载性能 testCases := []struct { name string limit int repeat int // 重复次数 }{ {"limit_10", 10, 5}, {"limit_50", 50, 5}, {"limit_100", 100, 5}, {"no_limit", 0, 3}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var totalDuration time.Duration var successes int var firstByteTimes []time.Duration for i := 0; i < tc.repeat; i++ { duration, firstByte, err := measureProxyRequest(proxyURL, sessionID, token, tc.limit) if err != nil { t.Logf("⚠️ 第%d次请求失败: %v", i+1, err) continue } totalDuration += duration firstByteTimes = append(firstByteTimes, firstByte) successes++ // 请求间短暂延迟 time.Sleep(100 * time.Millisecond) } if successes > 0 { avgDuration := totalDuration / time.Duration(successes) var avgFirstByte time.Duration for _, fb := range firstByteTimes { avgFirstByte += fb } avgFirstByte /= time.Duration(len(firstByteTimes)) t.Logf("✅ %s: 成功%d/%d次, 平均总时间: %v, 平均首字节: %v", tc.name, successes, tc.repeat, avgDuration, avgFirstByte) } else { t.Errorf("❌ %s: 所有请求都失败", tc.name) } }) } }) } // TestPerformanceSummary 性能对比总结 func TestPerformanceSummary(t *testing.T) { sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z" // 只测试limit=50的情况,进行更详细的对比 t.Logf("📈 性能对比总结测试 (limit=50)") // 测试8787端口 directTimes := []time.Duration{} directFirstBytes := []time.Duration{} directURL := "http://localhost:8787" if isServiceRunningForMessages(t, directURL) { t.Log("🔍 测试8787端口...") for i := 0; i < 10; i++ { duration, firstByte, err := measureDirectRequest(directURL, sessionID, 50) if err != nil { t.Logf("⚠️ 8787端口第%d次请求失败: %v", i+1, err) continue } directTimes = append(directTimes, duration) directFirstBytes = append(directFirstBytes, firstByte) time.Sleep(50 * time.Millisecond) } } // 测试8020端口 proxyTimes := []time.Duration{} proxyFirstBytes := []time.Duration{} proxyURL := "http://localhost:8020" token := "" if isServiceRunningForMessages(t, proxyURL) { t.Log("🔍 测试8020端口...") token, _ = loginAndGetTokenForMessagesTest(t, proxyURL) if token != "" { for i := 0; i < 10; i++ { duration, firstByte, err := measureProxyRequest(proxyURL, sessionID, token, 50) if err != nil { t.Logf("⚠️ 8020端口第%d次请求失败: %v", i+1, err) continue } proxyTimes = append(proxyTimes, duration) proxyFirstBytes = append(proxyFirstBytes, firstByte) time.Sleep(50 * time.Millisecond) } } } // 分析结果 if len(directTimes) > 0 && len(proxyTimes) > 0 { directAvg := averageDuration(directTimes) directFBAvg := averageDuration(directFirstBytes) proxyAvg := averageDuration(proxyTimes) proxyFBAvg := averageDuration(proxyFirstBytes) diff := proxyAvg - directAvg diffPercent := float64(diff) / float64(directAvg) * 100 t.Logf("\n📊 性能对比结果:") t.Logf(" 8787端口(直接): 平均总时间=%v, 平均首字节=%v", directAvg, directFBAvg) t.Logf(" 8020端口(代理): 平均总时间=%v, 平均首字节=%v", proxyAvg, proxyFBAvg) t.Logf(" 差异: %v (%.1f%%)", diff, diffPercent) if diffPercent > 20 { t.Logf("⚠️ 8020端口比8787端口慢%.1f%%,可能存在代理开销", diffPercent) } else if diffPercent > 50 { t.Logf("❌ 8020端口性能显著下降,建议优化代理逻辑") } else if diffPercent < -10 { t.Logf("✅ 8020端口性能更好,可能包含缓存优化") } else { t.Logf("✅ 两端性能接近,差异在可接受范围内") } } else { t.Logf("⚠️ 无法完成完整对比,部分端口数据缺失") if len(directTimes) == 0 { t.Logf(" - 8787端口无有效数据") } if len(proxyTimes) == 0 { t.Logf(" - 8020端口无有效数据") } } } // measureDirectRequest 测量直接访问8787端口的请求性能 func measureDirectRequest(baseURL, sessionID string, limit int) (totalDuration, firstByteDuration time.Duration, err error) { // 构造URL url := fmt.Sprintf("%s/session/%s/message", baseURL, sessionID) if limit > 0 { url = fmt.Sprintf("%s?limit=%d", url, limit) } client := &http.Client{ Timeout: 30 * time.Second, } req, err := http.NewRequest("GET", url, nil) if err != nil { return 0, 0, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("Accept", "application/json") startTime := time.Now() // 监听响应首字节 req.Header.Set("Accept-Encoding", "identity") // 禁用压缩以便准确测量 resp, err := client.Do(req) if err != nil { return 0, 0, fmt.Errorf("HTTP请求失败: %w", err) } defer resp.Body.Close() // 记录首字节到达时间 firstByteTime := time.Now() firstByteDuration = firstByteTime.Sub(startTime) // 读取完整响应体 body, err := io.ReadAll(resp.Body) if err != nil { return 0, 0, fmt.Errorf("读取响应体失败: %w", err) } totalDuration = time.Since(startTime) // 验证响应状态 if resp.StatusCode != http.StatusOK { return 0, 0, fmt.Errorf("状态码错误: %d, 响应: %s", resp.StatusCode, string(body[:minForMessagesTest(200, len(body))])) } // 验证响应格式 var messages []opencode.SessionMessage if err := json.Unmarshal(body, &messages); err != nil { return 0, 0, fmt.Errorf("解析响应失败: %w", err) } return totalDuration, firstByteDuration, nil } // measureProxyRequest 测量通过8020端口代理的请求性能 func measureProxyRequest(baseURL, sessionID, token string, limit int) (totalDuration, firstByteDuration time.Duration, err error) { // 构造URL url := fmt.Sprintf("%s/api/session/messages", baseURL) // 创建请求体 requestBody := map[string]interface{}{ "sessionID": sessionID, } if limit > 0 { requestBody["limit"] = limit } jsonBody, err := json.Marshal(requestBody) if err != nil { return 0, 0, fmt.Errorf("编码请求体失败: %w", err) } client := &http.Client{ Timeout: 30 * time.Second, } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) if err != nil { return 0, 0, fmt.Errorf("创建请求失败: %w", err) } // 添加认证头 req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept-Encoding", "identity") startTime := time.Now() resp, err := client.Do(req) if err != nil { return 0, 0, fmt.Errorf("HTTP请求失败: %w", err) } defer resp.Body.Close() // 记录首字节到达时间 firstByteTime := time.Now() firstByteDuration = firstByteTime.Sub(startTime) // 读取完整响应体 body, err := io.ReadAll(resp.Body) if err != nil { return 0, 0, fmt.Errorf("读取响应体失败: %w", err) } totalDuration = time.Since(startTime) // 验证响应状态 if resp.StatusCode != http.StatusOK { return 0, 0, fmt.Errorf("状态码错误: %d, 响应: %s", resp.StatusCode, string(body[:minForMessagesTest(200, len(body))])) } // 验证响应格式 var result struct { Success bool `json:"success"` Message string `json:"message"` Data struct { Messages []opencode.SessionMessage `json:"messages"` Count int `json:"count"` } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { return 0, 0, fmt.Errorf("解析响应失败: %w", err) } if !result.Success { return 0, 0, fmt.Errorf("API调用失败: %s", result.Message) } return totalDuration, firstByteDuration, nil } // averageDuration 计算平均时长 func averageDuration(durations []time.Duration) time.Duration { if len(durations) == 0 { return 0 } var total time.Duration for _, d := range durations { total += d } return total / time.Duration(len(durations)) }