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  }