Brak opisu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

performance_benchmark_test.go 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "testing"
  9. "time"
  10. "git.x2erp.com/qdy/go-svc-code/internal/opencode"
  11. )
  12. // TestMessageResponseTimeComparison 性能对比测试:比较8787端口和8020端口的历史消息加载性能
  13. func TestMessageResponseTimeComparison(t *testing.T) {
  14. // 测试会话ID - 使用现有会话
  15. sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
  16. t.Logf("🚀 开始性能对比测试")
  17. t.Logf("测试会话ID: %s", sessionID)
  18. // 1. 测试8787端口(直接opencode服务)
  19. t.Run("DirectOpenCode_8787", func(t *testing.T) {
  20. directURL := "http://localhost:8787"
  21. // 检查服务是否可用
  22. if !isServiceRunningForMessages(t, directURL) {
  23. t.Skipf("8787端口服务不可用,跳过测试")
  24. return
  25. }
  26. t.Logf("📊 测试8787端口直接访问...")
  27. // 测试不同消息数量的加载性能
  28. testCases := []struct {
  29. name string
  30. limit int
  31. repeat int // 重复次数
  32. }{
  33. {"limit_10", 10, 5},
  34. {"limit_50", 50, 5},
  35. {"limit_100", 100, 5},
  36. {"no_limit", 0, 3},
  37. }
  38. for _, tc := range testCases {
  39. t.Run(tc.name, func(t *testing.T) {
  40. var totalDuration time.Duration
  41. var successes int
  42. var firstByteTimes []time.Duration
  43. for i := 0; i < tc.repeat; i++ {
  44. duration, firstByte, err := measureDirectRequest(directURL, sessionID, tc.limit)
  45. if err != nil {
  46. t.Logf("⚠️ 第%d次请求失败: %v", i+1, err)
  47. continue
  48. }
  49. totalDuration += duration
  50. firstByteTimes = append(firstByteTimes, firstByte)
  51. successes++
  52. // 请求间短暂延迟
  53. time.Sleep(100 * time.Millisecond)
  54. }
  55. if successes > 0 {
  56. avgDuration := totalDuration / time.Duration(successes)
  57. var avgFirstByte time.Duration
  58. for _, fb := range firstByteTimes {
  59. avgFirstByte += fb
  60. }
  61. avgFirstByte /= time.Duration(len(firstByteTimes))
  62. t.Logf("✅ %s: 成功%d/%d次, 平均总时间: %v, 平均首字节: %v",
  63. tc.name, successes, tc.repeat, avgDuration, avgFirstByte)
  64. } else {
  65. t.Errorf("❌ %s: 所有请求都失败", tc.name)
  66. }
  67. })
  68. }
  69. })
  70. // 2. 测试8020端口(svc-code代理)
  71. t.Run("SvcCodeProxy_8020", func(t *testing.T) {
  72. proxyURL := "http://localhost:8020"
  73. // 检查服务是否可用
  74. if !isServiceRunningForMessages(t, proxyURL) {
  75. t.Skipf("8020端口服务不可用,跳过测试")
  76. return
  77. }
  78. // 获取认证token
  79. token, err := loginAndGetTokenForMessagesTest(t, proxyURL)
  80. if err != nil {
  81. t.Skipf("获取认证token失败: %v,跳过8020端口测试", err)
  82. return
  83. }
  84. t.Logf("📊 测试8020端口代理访问...")
  85. t.Logf("Token获取成功(前20位): %s...", token[:minForMessagesTest(20, len(token))])
  86. // 测试不同消息数量的加载性能
  87. testCases := []struct {
  88. name string
  89. limit int
  90. repeat int // 重复次数
  91. }{
  92. {"limit_10", 10, 5},
  93. {"limit_50", 50, 5},
  94. {"limit_100", 100, 5},
  95. {"no_limit", 0, 3},
  96. }
  97. for _, tc := range testCases {
  98. t.Run(tc.name, func(t *testing.T) {
  99. var totalDuration time.Duration
  100. var successes int
  101. var firstByteTimes []time.Duration
  102. for i := 0; i < tc.repeat; i++ {
  103. duration, firstByte, err := measureProxyRequest(proxyURL, sessionID, token, tc.limit)
  104. if err != nil {
  105. t.Logf("⚠️ 第%d次请求失败: %v", i+1, err)
  106. continue
  107. }
  108. totalDuration += duration
  109. firstByteTimes = append(firstByteTimes, firstByte)
  110. successes++
  111. // 请求间短暂延迟
  112. time.Sleep(100 * time.Millisecond)
  113. }
  114. if successes > 0 {
  115. avgDuration := totalDuration / time.Duration(successes)
  116. var avgFirstByte time.Duration
  117. for _, fb := range firstByteTimes {
  118. avgFirstByte += fb
  119. }
  120. avgFirstByte /= time.Duration(len(firstByteTimes))
  121. t.Logf("✅ %s: 成功%d/%d次, 平均总时间: %v, 平均首字节: %v",
  122. tc.name, successes, tc.repeat, avgDuration, avgFirstByte)
  123. } else {
  124. t.Errorf("❌ %s: 所有请求都失败", tc.name)
  125. }
  126. })
  127. }
  128. })
  129. }
  130. // TestPerformanceSummary 性能对比总结
  131. func TestPerformanceSummary(t *testing.T) {
  132. sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
  133. // 只测试limit=50的情况,进行更详细的对比
  134. t.Logf("📈 性能对比总结测试 (limit=50)")
  135. // 测试8787端口
  136. directTimes := []time.Duration{}
  137. directFirstBytes := []time.Duration{}
  138. directURL := "http://localhost:8787"
  139. if isServiceRunningForMessages(t, directURL) {
  140. t.Log("🔍 测试8787端口...")
  141. for i := 0; i < 10; i++ {
  142. duration, firstByte, err := measureDirectRequest(directURL, sessionID, 50)
  143. if err != nil {
  144. t.Logf("⚠️ 8787端口第%d次请求失败: %v", i+1, err)
  145. continue
  146. }
  147. directTimes = append(directTimes, duration)
  148. directFirstBytes = append(directFirstBytes, firstByte)
  149. time.Sleep(50 * time.Millisecond)
  150. }
  151. }
  152. // 测试8020端口
  153. proxyTimes := []time.Duration{}
  154. proxyFirstBytes := []time.Duration{}
  155. proxyURL := "http://localhost:8020"
  156. token := ""
  157. if isServiceRunningForMessages(t, proxyURL) {
  158. t.Log("🔍 测试8020端口...")
  159. token, _ = loginAndGetTokenForMessagesTest(t, proxyURL)
  160. if token != "" {
  161. for i := 0; i < 10; i++ {
  162. duration, firstByte, err := measureProxyRequest(proxyURL, sessionID, token, 50)
  163. if err != nil {
  164. t.Logf("⚠️ 8020端口第%d次请求失败: %v", i+1, err)
  165. continue
  166. }
  167. proxyTimes = append(proxyTimes, duration)
  168. proxyFirstBytes = append(proxyFirstBytes, firstByte)
  169. time.Sleep(50 * time.Millisecond)
  170. }
  171. }
  172. }
  173. // 分析结果
  174. if len(directTimes) > 0 && len(proxyTimes) > 0 {
  175. directAvg := averageDuration(directTimes)
  176. directFBAvg := averageDuration(directFirstBytes)
  177. proxyAvg := averageDuration(proxyTimes)
  178. proxyFBAvg := averageDuration(proxyFirstBytes)
  179. diff := proxyAvg - directAvg
  180. diffPercent := float64(diff) / float64(directAvg) * 100
  181. t.Logf("\n📊 性能对比结果:")
  182. t.Logf(" 8787端口(直接): 平均总时间=%v, 平均首字节=%v", directAvg, directFBAvg)
  183. t.Logf(" 8020端口(代理): 平均总时间=%v, 平均首字节=%v", proxyAvg, proxyFBAvg)
  184. t.Logf(" 差异: %v (%.1f%%)", diff, diffPercent)
  185. if diffPercent > 20 {
  186. t.Logf("⚠️ 8020端口比8787端口慢%.1f%%,可能存在代理开销", diffPercent)
  187. } else if diffPercent > 50 {
  188. t.Logf("❌ 8020端口性能显著下降,建议优化代理逻辑")
  189. } else if diffPercent < -10 {
  190. t.Logf("✅ 8020端口性能更好,可能包含缓存优化")
  191. } else {
  192. t.Logf("✅ 两端性能接近,差异在可接受范围内")
  193. }
  194. } else {
  195. t.Logf("⚠️ 无法完成完整对比,部分端口数据缺失")
  196. if len(directTimes) == 0 {
  197. t.Logf(" - 8787端口无有效数据")
  198. }
  199. if len(proxyTimes) == 0 {
  200. t.Logf(" - 8020端口无有效数据")
  201. }
  202. }
  203. }
  204. // measureDirectRequest 测量直接访问8787端口的请求性能
  205. func measureDirectRequest(baseURL, sessionID string, limit int) (totalDuration, firstByteDuration time.Duration, err error) {
  206. // 构造URL
  207. url := fmt.Sprintf("%s/session/%s/message", baseURL, sessionID)
  208. if limit > 0 {
  209. url = fmt.Sprintf("%s?limit=%d", url, limit)
  210. }
  211. client := &http.Client{
  212. Timeout: 30 * time.Second,
  213. }
  214. req, err := http.NewRequest("GET", url, nil)
  215. if err != nil {
  216. return 0, 0, fmt.Errorf("创建请求失败: %w", err)
  217. }
  218. req.Header.Set("Accept", "application/json")
  219. startTime := time.Now()
  220. // 监听响应首字节
  221. req.Header.Set("Accept-Encoding", "identity") // 禁用压缩以便准确测量
  222. resp, err := client.Do(req)
  223. if err != nil {
  224. return 0, 0, fmt.Errorf("HTTP请求失败: %w", err)
  225. }
  226. defer resp.Body.Close()
  227. // 记录首字节到达时间
  228. firstByteTime := time.Now()
  229. firstByteDuration = firstByteTime.Sub(startTime)
  230. // 读取完整响应体
  231. body, err := io.ReadAll(resp.Body)
  232. if err != nil {
  233. return 0, 0, fmt.Errorf("读取响应体失败: %w", err)
  234. }
  235. totalDuration = time.Since(startTime)
  236. // 验证响应状态
  237. if resp.StatusCode != http.StatusOK {
  238. return 0, 0, fmt.Errorf("状态码错误: %d, 响应: %s", resp.StatusCode, string(body[:minForMessagesTest(200, len(body))]))
  239. }
  240. // 验证响应格式
  241. var messages []opencode.SessionMessage
  242. if err := json.Unmarshal(body, &messages); err != nil {
  243. return 0, 0, fmt.Errorf("解析响应失败: %w", err)
  244. }
  245. return totalDuration, firstByteDuration, nil
  246. }
  247. // measureProxyRequest 测量通过8020端口代理的请求性能
  248. func measureProxyRequest(baseURL, sessionID, token string, limit int) (totalDuration, firstByteDuration time.Duration, err error) {
  249. // 构造URL
  250. url := fmt.Sprintf("%s/api/session/messages", baseURL)
  251. // 创建请求体
  252. requestBody := map[string]interface{}{
  253. "sessionID": sessionID,
  254. }
  255. if limit > 0 {
  256. requestBody["limit"] = limit
  257. }
  258. jsonBody, err := json.Marshal(requestBody)
  259. if err != nil {
  260. return 0, 0, fmt.Errorf("编码请求体失败: %w", err)
  261. }
  262. client := &http.Client{
  263. Timeout: 30 * time.Second,
  264. }
  265. req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
  266. if err != nil {
  267. return 0, 0, fmt.Errorf("创建请求失败: %w", err)
  268. }
  269. // 添加认证头
  270. req.Header.Set("Authorization", "Bearer "+token)
  271. req.Header.Set("Accept", "application/json")
  272. req.Header.Set("Content-Type", "application/json")
  273. req.Header.Set("Accept-Encoding", "identity")
  274. startTime := time.Now()
  275. resp, err := client.Do(req)
  276. if err != nil {
  277. return 0, 0, fmt.Errorf("HTTP请求失败: %w", err)
  278. }
  279. defer resp.Body.Close()
  280. // 记录首字节到达时间
  281. firstByteTime := time.Now()
  282. firstByteDuration = firstByteTime.Sub(startTime)
  283. // 读取完整响应体
  284. body, err := io.ReadAll(resp.Body)
  285. if err != nil {
  286. return 0, 0, fmt.Errorf("读取响应体失败: %w", err)
  287. }
  288. totalDuration = time.Since(startTime)
  289. // 验证响应状态
  290. if resp.StatusCode != http.StatusOK {
  291. return 0, 0, fmt.Errorf("状态码错误: %d, 响应: %s", resp.StatusCode, string(body[:minForMessagesTest(200, len(body))]))
  292. }
  293. // 验证响应格式
  294. var result struct {
  295. Success bool `json:"success"`
  296. Message string `json:"message"`
  297. Data struct {
  298. Messages []opencode.SessionMessage `json:"messages"`
  299. Count int `json:"count"`
  300. } `json:"data"`
  301. }
  302. if err := json.Unmarshal(body, &result); err != nil {
  303. return 0, 0, fmt.Errorf("解析响应失败: %w", err)
  304. }
  305. if !result.Success {
  306. return 0, 0, fmt.Errorf("API调用失败: %s", result.Message)
  307. }
  308. return totalDuration, firstByteDuration, nil
  309. }
  310. // averageDuration 计算平均时长
  311. func averageDuration(durations []time.Duration) time.Duration {
  312. if len(durations) == 0 {
  313. return 0
  314. }
  315. var total time.Duration
  316. for _, d := range durations {
  317. total += d
  318. }
  319. return total / time.Duration(len(durations))
  320. }