@@ -26,9 +26,14 @@ type Stats struct {
26
26
Total time.Duration
27
27
}
28
28
29
+ type requestResult struct {
30
+ duration time.Duration
31
+ traceID string // X-Trace header value
32
+ }
33
+
29
34
func init () {
30
35
usage := `
31
- 'src gateway benchmark' runs performance benchmarks against Cody Gateway endpoints.
36
+ 'src gateway benchmark' runs performance benchmarks against Cody Gateway and Sourcegraph test endpoints.
32
37
33
38
Usage:
34
39
@@ -39,17 +44,20 @@ Examples:
39
44
$ src gateway benchmark --sgp <token>
40
45
$ src gateway benchmark --requests 50 --sgp <token>
41
46
$ src gateway benchmark --gateway http://localhost:9992 --sourcegraph http://localhost:3082 --sgp <token>
42
- $ src gateway benchmark --requests 50 --csv results.csv --sgp <token>
47
+ $ src gateway benchmark --requests 50 --csv results.csv --request-csv requests.csv --sgp <token>
48
+ $ src gateway benchmark --gateway https://cody-gateway.sourcegraph.com --sourcegraph https://sourcegraph.com --sgp <token> --use-special-header
43
49
`
44
50
45
51
flagSet := flag .NewFlagSet ("benchmark" , flag .ExitOnError )
46
52
47
53
var (
48
- requestCount = flagSet .Int ("requests" , 1000 , "Number of requests to make per endpoint" )
49
- csvOutput = flagSet .String ("csv" , "" , "Export results to CSV file (provide filename)" )
50
- gatewayEndpoint = flagSet .String ("gateway" , "https://cody-gateway.sourcegraph.com" , "Cody Gateway endpoint" )
51
- sgEndpoint = flagSet .String ("sourcegraph" , "https://sourcegraph.com" , "Sourcegraph endpoint" )
52
- sgpToken = flagSet .String ("sgp" , "" , "Sourcegraph personal access token for the called instance" )
54
+ requestCount = flagSet .Int ("requests" , 1000 , "Number of requests to make per endpoint" )
55
+ csvOutput = flagSet .String ("csv" , "" , "Export results to CSV file (provide filename)" )
56
+ requestLevelCsvOutput = flagSet .String ("request-csv" , "" , "Export request results to CSV file (provide filename)" )
57
+ gatewayEndpoint = flagSet .String ("gateway" , "" , "Cody Gateway endpoint" )
58
+ sgEndpoint = flagSet .String ("sourcegraph" , "" , "Sourcegraph endpoint" )
59
+ sgpToken = flagSet .String ("sgp" , "" , "Sourcegraph personal access token for the called instance" )
60
+ useSpecialHeader = flagSet .Bool ("use-special-header" , false , "Use special header to test the gateway" )
53
61
)
54
62
55
63
handler := func (args []string ) error {
@@ -61,15 +69,23 @@ Examples:
61
69
return cmderrors .Usage ("additional arguments not allowed" )
62
70
}
63
71
72
+ if * useSpecialHeader {
73
+ fmt .Println ("Using special header 'cody-core-gc-test'" )
74
+ }
75
+
64
76
var (
65
77
httpClient = & http.Client {}
66
78
endpoints = map [string ]any {} // Values: URL `string`s or `*webSocketClient`s
67
79
)
68
80
if * gatewayEndpoint != "" {
69
81
fmt .Println ("Benchmarking Cody Gateway instance:" , * gatewayEndpoint )
82
+ headers := http.Header {
83
+ "X-Sourcegraph-Should-Trace" : []string {"true" },
84
+ }
70
85
endpoints ["ws(s): gateway" ] = & webSocketClient {
71
- conn : nil ,
72
- URL : strings .Replace (fmt .Sprint (* gatewayEndpoint , "/v2/websocket" ), "http" , "ws" , 1 ),
86
+ conn : nil ,
87
+ URL : strings .Replace (fmt .Sprint (* gatewayEndpoint , "/v2/websocket" ), "http" , "ws" , 1 ),
88
+ reqHeaders : headers ,
73
89
}
74
90
endpoints ["http(s): gateway" ] = fmt .Sprint (* gatewayEndpoint , "/v2/http" )
75
91
} else {
@@ -80,12 +96,18 @@ Examples:
80
96
return cmderrors .Usage ("must specify --sgp <Sourcegraph personal access token>" )
81
97
}
82
98
fmt .Println ("Benchmarking Sourcegraph instance:" , * sgEndpoint )
99
+ headers := http.Header {
100
+ "Authorization" : []string {"token " + * sgpToken },
101
+ "X-Sourcegraph-Should-Trace" : []string {"true" },
102
+ }
103
+ if * useSpecialHeader {
104
+ headers .Set ("cody-core-gc-test" , "M2R{+6VI?1,M3n&<vpw1&AK>" )
105
+ }
106
+
83
107
endpoints ["ws(s): sourcegraph" ] = & webSocketClient {
84
- conn : nil ,
85
- URL : strings .Replace (fmt .Sprint (* sgEndpoint , "/.api/gateway/websocket" ), "http" , "ws" , 1 ),
86
- headers : http.Header {
87
- "Authorization" : []string {"token " + * sgpToken },
88
- },
108
+ conn : nil ,
109
+ URL : strings .Replace (fmt .Sprint (* sgEndpoint , "/.api/gateway/websocket" ), "http" , "ws" , 1 ),
110
+ reqHeaders : headers ,
89
111
}
90
112
endpoints ["http(s): sourcegraph" ] = fmt .Sprint (* sgEndpoint , "/.api/gateway/http" )
91
113
endpoints ["http(s): http-then-ws" ] = fmt .Sprint (* sgEndpoint , "/.api/gateway/http-then-websocket" )
@@ -95,29 +117,33 @@ Examples:
95
117
96
118
fmt .Printf ("Starting benchmark with %d requests per endpoint...\n " , * requestCount )
97
119
98
- var results []endpointResult
120
+ var eResults []endpointResult
121
+ rResults := map [string ][]requestResult {}
99
122
for name , clientOrURL := range endpoints {
100
123
durations := make ([]time.Duration , 0 , * requestCount )
124
+ rResults [name ] = make ([]requestResult , 0 , * requestCount )
101
125
fmt .Printf ("\n Testing %s..." , name )
102
126
103
127
for i := 0 ; i < * requestCount ; i ++ {
104
128
if ws , ok := clientOrURL .(* webSocketClient ); ok {
105
- duration := benchmarkEndpointWebSocket (ws )
106
- if duration > 0 {
107
- durations = append (durations , duration )
129
+ result := benchmarkEndpointWebSocket (ws )
130
+ if result .duration > 0 {
131
+ durations = append (durations , result .duration )
132
+ rResults [name ] = append (rResults [name ], result )
108
133
}
109
134
} else if url , ok := clientOrURL .(string ); ok {
110
- duration := benchmarkEndpointHTTP (httpClient , url , * sgpToken )
111
- if duration > 0 {
112
- durations = append (durations , duration )
135
+ result := benchmarkEndpointHTTP (httpClient , url , * sgpToken , * useSpecialHeader )
136
+ if result .duration > 0 {
137
+ durations = append (durations , result .duration )
138
+ rResults [name ] = append (rResults [name ], result )
113
139
}
114
140
}
115
141
}
116
142
fmt .Println ()
117
143
118
144
stats := calculateStats (durations )
119
145
120
- results = append (results , endpointResult {
146
+ eResults = append (eResults , endpointResult {
121
147
name : name ,
122
148
avg : stats .Avg ,
123
149
median : stats .Median ,
@@ -130,14 +156,20 @@ Examples:
130
156
})
131
157
}
132
158
133
- printResults (results , requestCount )
159
+ printResults (eResults , requestCount )
134
160
135
161
if * csvOutput != "" {
136
- if err := writeResultsToCSV (* csvOutput , results , requestCount ); err != nil {
162
+ if err := writeResultsToCSV (* csvOutput , eResults , requestCount ); err != nil {
137
163
return fmt .Errorf ("failed to export CSV: %v" , err )
138
164
}
139
165
fmt .Printf ("\n Results exported to %s\n " , * csvOutput )
140
166
}
167
+ if * requestLevelCsvOutput != "" {
168
+ if err := writeRequestResultsToCSV (* requestLevelCsvOutput , rResults ); err != nil {
169
+ return fmt .Errorf ("failed to export request-level CSV: %v" , err )
170
+ }
171
+ fmt .Printf ("\n Request-level results exported to %s\n " , * requestLevelCsvOutput )
172
+ }
141
173
142
174
return nil
143
175
}
@@ -158,9 +190,10 @@ Examples:
158
190
}
159
191
160
192
type webSocketClient struct {
161
- conn * websocket.Conn
162
- URL string
163
- headers http.Header
193
+ conn * websocket.Conn
194
+ URL string
195
+ reqHeaders http.Header
196
+ respHeaders http.Header
164
197
}
165
198
166
199
func (c * webSocketClient ) reconnect () error {
@@ -169,11 +202,13 @@ func (c *webSocketClient) reconnect() error {
169
202
}
170
203
fmt .Println ("Connecting to WebSocket.." , c .URL )
171
204
var err error
172
- c .conn , _ , err = websocket .DefaultDialer .Dial (c .URL , c .headers )
205
+ var resp * http.Response
206
+ c .conn , resp , err = websocket .DefaultDialer .Dial (c .URL , c .reqHeaders )
173
207
if err != nil {
174
208
c .conn = nil // retry again later
175
209
return fmt .Errorf ("WebSocket dial(%s): %v" , c .URL , err )
176
210
}
211
+ c .respHeaders = resp .Header
177
212
fmt .Println ("Connected!" )
178
213
return nil
179
214
}
@@ -190,19 +225,23 @@ type endpointResult struct {
190
225
successful int
191
226
}
192
227
193
- func benchmarkEndpointHTTP (client * http.Client , url , accessToken string ) time. Duration {
228
+ func benchmarkEndpointHTTP (client * http.Client , url , accessToken string , useSpecialHeader bool ) requestResult {
194
229
start := time .Now ()
195
230
req , err := http .NewRequest ("POST" , url , strings .NewReader ("ping" ))
196
231
if err != nil {
197
232
fmt .Printf ("Error creating request: %v\n " , err )
198
- return 0
233
+ return requestResult {}
199
234
}
200
235
req .Header .Set ("Content-Type" , "application/json" )
201
236
req .Header .Set ("Authorization" , "token " + accessToken )
237
+ req .Header .Set ("X-Sourcegraph-Should-Trace" , "true" )
238
+ if useSpecialHeader {
239
+ req .Header .Set ("cody-core-gc-test" , "M2R{+6VI?1,M3n&<vpw1&AK>" )
240
+ }
202
241
resp , err := client .Do (req )
203
242
if err != nil {
204
243
fmt .Printf ("Error calling %s: %v\n " , url , err )
205
- return 0
244
+ return requestResult {}
206
245
}
207
246
defer func () {
208
247
err := resp .Body .Close ()
@@ -212,27 +251,30 @@ func benchmarkEndpointHTTP(client *http.Client, url, accessToken string) time.Du
212
251
}()
213
252
if resp .StatusCode != http .StatusOK {
214
253
fmt .Printf ("non-200 response: %v\n " , resp .Status )
215
- return 0
254
+ return requestResult {}
216
255
}
217
256
body , err := io .ReadAll (resp .Body )
218
257
if err != nil {
219
258
fmt .Printf ("Error reading response body: %v\n " , err )
220
- return 0
259
+ return requestResult {}
221
260
}
222
261
if string (body ) != "pong" {
223
262
fmt .Printf ("Expected 'pong' response, got: %q\n " , string (body ))
224
- return 0
263
+ return requestResult {}
225
264
}
226
265
227
- return time .Since (start )
266
+ return requestResult {
267
+ duration : time .Since (start ),
268
+ traceID : resp .Header .Get ("X-Trace" ),
269
+ }
228
270
}
229
271
230
- func benchmarkEndpointWebSocket (client * webSocketClient ) time. Duration {
272
+ func benchmarkEndpointWebSocket (client * webSocketClient ) requestResult {
231
273
// Perform initial websocket connection, if needed.
232
274
if client .conn == nil {
233
275
if err := client .reconnect (); err != nil {
234
276
fmt .Printf ("Error reconnecting: %v\n " , err )
235
- return 0
277
+ return requestResult {}
236
278
}
237
279
}
238
280
@@ -244,7 +286,7 @@ func benchmarkEndpointWebSocket(client *webSocketClient) time.Duration {
244
286
if err := client .reconnect (); err != nil {
245
287
fmt .Printf ("Error reconnecting: %v\n " , err )
246
288
}
247
- return 0
289
+ return requestResult {}
248
290
}
249
291
_ , message , err := client .conn .ReadMessage ()
250
292
@@ -253,16 +295,19 @@ func benchmarkEndpointWebSocket(client *webSocketClient) time.Duration {
253
295
if err := client .reconnect (); err != nil {
254
296
fmt .Printf ("Error reconnecting: %v\n " , err )
255
297
}
256
- return 0
298
+ return requestResult {}
257
299
}
258
300
if string (message ) != "pong" {
259
301
fmt .Printf ("Expected 'pong' response, got: %q\n " , string (message ))
260
302
if err := client .reconnect (); err != nil {
261
303
fmt .Printf ("Error reconnecting: %v\n " , err )
262
304
}
263
- return 0
305
+ return requestResult {}
306
+ }
307
+ return requestResult {
308
+ duration : time .Since (start ),
309
+ traceID : client .respHeaders .Get ("Content-Type" ),
264
310
}
265
- return time .Since (start )
266
311
}
267
312
268
313
func calculateStats (durations []time.Duration ) Stats {
@@ -438,3 +483,40 @@ func writeResultsToCSV(filename string, results []endpointResult, requestCount *
438
483
439
484
return nil
440
485
}
486
+
487
+ func writeRequestResultsToCSV (filename string , results map [string ][]requestResult ) error {
488
+ file , err := os .Create (filename )
489
+ if err != nil {
490
+ return fmt .Errorf ("failed to create CSV file: %v" , err )
491
+ }
492
+ defer func () {
493
+ err := file .Close ()
494
+ if err != nil {
495
+ return
496
+ }
497
+ }()
498
+
499
+ writer := csv .NewWriter (file )
500
+ defer writer .Flush ()
501
+
502
+ // Write header
503
+ header := []string {"Endpoint" , "Duration (ms)" , "Trace ID" }
504
+ if err := writer .Write (header ); err != nil {
505
+ return fmt .Errorf ("failed to write CSV header: %v" , err )
506
+ }
507
+
508
+ for endpoint , requestResults := range results {
509
+ for _ , result := range requestResults {
510
+ row := []string {
511
+ endpoint ,
512
+ fmt .Sprintf ("%.2f" , float64 (result .duration .Microseconds ())/ 1000 ),
513
+ result .traceID ,
514
+ }
515
+ if err := writer .Write (row ); err != nil {
516
+ return fmt .Errorf ("failed to write CSV row: %v" , err )
517
+ }
518
+ }
519
+ }
520
+
521
+ return nil
522
+ }
0 commit comments