github.com/weaviate/weaviate@v1.24.6/test/benchmark/benchmark.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 // Package implements performance tracking examples 13 14 package main 15 16 import ( 17 "bytes" 18 "encoding/json" 19 "flag" 20 "fmt" 21 "io" 22 "net" 23 "net/http" 24 "os" 25 "os/exec" 26 "time" 27 28 "github.com/pkg/errors" 29 30 "github.com/weaviate/weaviate/entities/models" 31 ) 32 33 type batch struct { 34 Objects []*models.Object 35 } 36 37 type benchmarkResult map[string]map[string]int64 38 39 func main() { 40 var benchmarkName string 41 var numBatches, failPercentage, maxEntries int 42 43 flag.StringVar(&benchmarkName, "name", "SIFT", "Which benchmark should be run. Currently only SIFT is available.") 44 flag.IntVar(&maxEntries, "numberEntries", 100000, "Maximum number of entries read from the dataset") 45 flag.IntVar(&numBatches, "numBatches", 1, "With how many parallel batches objects should be added") 46 flag.IntVar(&failPercentage, "fail", -1, "Fail if regression is larger") 47 flag.Parse() 48 49 t := &http.Transport{ 50 Proxy: http.ProxyFromEnvironment, 51 DialContext: (&net.Dialer{ 52 Timeout: 30 * time.Second, 53 KeepAlive: 120 * time.Second, 54 }).DialContext, 55 MaxIdleConnsPerHost: 100, 56 MaxIdleConns: 100, 57 IdleConnTimeout: 90 * time.Second, 58 TLSHandshakeTimeout: 10 * time.Second, 59 ExpectContinueTimeout: 1 * time.Second, 60 } 61 c := &http.Client{Transport: t} 62 url := "http://localhost:8080/v1/" 63 64 alreadyRunning := startWeaviate(c, url) 65 66 var newRuntime map[string]int64 67 var err error 68 switch benchmarkName { 69 case "SIFT": 70 newRuntime, err = benchmarkSift(c, url, maxEntries, numBatches) 71 default: 72 panic("Unknown benchmark " + benchmarkName) 73 } 74 75 if err != nil { 76 clearExistingObjects(c, url) 77 } 78 79 if !alreadyRunning { 80 tearDownWeaviate() 81 } 82 83 if err != nil { 84 panic(errors.Wrap(err, "Error occurred during benchmarking")) 85 } 86 87 FullBenchmarkName := benchmarkName + "-" + fmt.Sprint(maxEntries) + "_Entries-" + fmt.Sprint(numBatches) + "_Batch(es)" 88 89 // Write results to file, keeping existing entries 90 oldBenchmarkRunTimes := readCurrentBenchmarkResults() 91 oldRuntime := oldBenchmarkRunTimes[FullBenchmarkName] 92 oldBenchmarkRunTimes[FullBenchmarkName] = newRuntime 93 benchmarkJSON, _ := json.MarshalIndent(oldBenchmarkRunTimes, "", "\t") 94 if err := os.WriteFile("benchmark_results.json", benchmarkJSON, 0o666); err != nil { 95 panic(err) 96 } 97 98 totalNewRuntime := int64(0) 99 for _, runtime := range newRuntime { 100 totalNewRuntime += runtime 101 } 102 totalOldRuntime := int64(0) 103 for _, runtime := range oldRuntime { 104 totalOldRuntime += runtime 105 } 106 107 fmt.Fprint( 108 os.Stdout, 109 "Runtime for benchmark "+FullBenchmarkName+ 110 ": old total runtime: "+fmt.Sprint(totalOldRuntime)+"ms, new total runtime:"+fmt.Sprint(totalNewRuntime)+"ms.\n"+ 111 "This is a change of "+fmt.Sprintf("%.2f", 100*float32(totalNewRuntime-totalOldRuntime)/float32(totalNewRuntime))+"%.\n"+ 112 "Please update the benchmark results if necessary.\n\n", 113 ) 114 fmt.Fprint(os.Stdout, "Runtime for individual steps:.\n") 115 for name, time := range newRuntime { 116 fmt.Fprint(os.Stdout, "Runtime for "+name+" is "+fmt.Sprint(time)+"ms.\n") 117 } 118 119 // Return with error code if runtime regressed and corresponding flag was set 120 if failPercentage >= 0 && 121 totalOldRuntime > 0 && // don't report regression if no old entry exists 122 float64(totalOldRuntime)*(1.0+0.01*float64(failPercentage)) < float64(totalNewRuntime) { 123 fmt.Fprint( 124 os.Stderr, "Failed due to performance regressions.\n", 125 ) 126 os.Exit(1) 127 } 128 } 129 130 // If there is already a schema present, clear it out 131 func clearExistingObjects(c *http.Client, url string) { 132 checkSchemaRequest := createRequest(url+"schema", "GET", nil) 133 checkSchemaResponseCode, body, _, err := performRequest(c, checkSchemaRequest) 134 if err != nil { 135 panic(errors.Wrap(err, "perform request")) 136 } 137 if checkSchemaResponseCode != 200 { 138 return 139 } 140 141 var dump models.Schema 142 if err := json.Unmarshal(body, &dump); err != nil { 143 panic(errors.Wrap(err, "Could not unmarshal read response")) 144 } 145 for _, classObj := range dump.Classes { 146 requestDelete := createRequest(url+"schema/"+classObj.Class, "DELETE", nil) 147 responseDeleteCode, _, _, err := performRequest(c, requestDelete) 148 if err != nil { 149 panic(errors.Wrap(err, "Could delete schema")) 150 } 151 if responseDeleteCode != 200 { 152 panic(fmt.Sprintf("Could not delete schema, code: %v", responseDeleteCode)) 153 } 154 } 155 } 156 157 func command(app string, arguments []string, waitForCompletion bool) error { 158 mydir, err := os.Getwd() 159 if err != nil { 160 return err 161 } 162 163 cmd := exec.Command(app, arguments...) 164 execDir := mydir + "/../../" 165 cmd.Dir = execDir 166 cmd.Stdout = os.Stdout 167 cmd.Stderr = os.Stderr 168 if waitForCompletion { 169 err = cmd.Run() 170 } else { 171 err = cmd.Start() 172 } 173 174 return err 175 } 176 177 func readCurrentBenchmarkResults() benchmarkResult { 178 benchmarkFile, err := os.Open("benchmark_results.json") 179 if err != nil { 180 fmt.Print("No benchmark file present.") 181 return make(benchmarkResult) 182 } 183 defer benchmarkFile.Close() 184 185 var result benchmarkResult 186 jsonParser := json.NewDecoder(benchmarkFile) 187 if err = jsonParser.Decode(&result); err != nil { 188 panic("Could not parse existing benchmark file.") 189 } 190 return result 191 } 192 193 func tearDownWeaviate() error { 194 fmt.Print("Shutting down weaviate.\n") 195 app := "docker-compose" 196 arguments := []string{ 197 "down", 198 "--remove-orphans", 199 } 200 return command(app, arguments, true) 201 } 202 203 // start weaviate in case it was not already started 204 // 205 // We want to benchmark the current state and therefore need to rebuild and then start a docker container 206 func startWeaviate(c *http.Client, url string) bool { 207 requestReady := createRequest(url+".well-known/ready", "GET", nil) 208 209 responseStartedCode, _, _, err := performRequest(c, requestReady) 210 alreadyRunning := err == nil && responseStartedCode == 200 211 212 if alreadyRunning { 213 fmt.Print("Weaviate instance already running.\n") 214 return alreadyRunning 215 } 216 217 fmt.Print("(Re-) build and start weaviate.\n") 218 cmd := "./tools/test/run_ci_server.sh" 219 if err := command(cmd, []string{}, true); err != nil { 220 panic(errors.Wrap(err, "Command to (re-) build and start weaviate failed")) 221 } 222 return false 223 } 224 225 // createRequest creates requests 226 func createRequest(url string, method string, payload interface{}) *http.Request { 227 var body io.Reader = nil 228 if payload != nil { 229 jsonBody, err := json.Marshal(payload) 230 if err != nil { 231 panic(errors.Wrap(err, "Could not marshal request")) 232 } 233 body = bytes.NewBuffer(jsonBody) 234 } 235 request, err := http.NewRequest(method, url, body) 236 if err != nil { 237 panic(errors.Wrap(err, "Could not create request")) 238 } 239 request.Header.Add("Content-Type", "application/json") 240 request.Header.Add("Accept", "application/json") 241 242 return request 243 } 244 245 // performRequest runs requests 246 func performRequest(c *http.Client, request *http.Request) (int, []byte, int64, error) { 247 timeStart := time.Now() 248 response, err := c.Do(request) 249 requestTime := time.Since(timeStart).Milliseconds() 250 251 if err != nil { 252 return 0, nil, requestTime, err 253 } 254 255 body, err := io.ReadAll(response.Body) 256 response.Body.Close() 257 if err != nil { 258 return 0, nil, requestTime, err 259 } 260 261 return response.StatusCode, body, requestTime, nil 262 }