package main import ( "flag" "fmt" "log" "math/rand" "os" "sync" "time" "gopkg.in/ini.v1" "gordenko.dev/dima/diploma/client" ) const ( listCumulativeMeasures = 0 listCumulativePeriods = 1 listInstantMeasures = 2 listInstantPeriods = 3 listCurrentValues = 4 methodsQty = 5 timeRangesQty = 4 ) var ( methodCodeToName = []string{ "listCumulativeMeasures", "listCumulativePeriods", "listInstantMeasures", "listInstantPeriods", "listCurrentValues", } rangeCodeToName = []string{ "last day", "last week", "last month", "random time range", } ) type WorkerStat struct { Queries int ElapsedTime time.Duration MethodCalls []int ElapsedTimeByMethods []time.Duration TimeRangeCalls []int ElapsedTimeByTimeRanges []time.Duration } func main() { var ( iniFileName string ) flag.Usage = func() { fmt.Fprint(flag.CommandLine.Output(), helpMessage) fmt.Fprint(flag.CommandLine.Output(), configExample) fmt.Fprintf(flag.CommandLine.Output(), mainUsage, os.Args[0]) flag.PrintDefaults() } flag.StringVar(&iniFileName, "c", "loadtest.ini", "path to *.ini config file") flag.Parse() config, err := loadConfig(iniFileName) if err != nil { log.Fatalln(err) } rand.Seed(time.Now().UnixNano()) metrics, err := readMetricInfo(config.MetricsInfo) if err != nil { log.Fatalln(err) } var ( wg = new(sync.WaitGroup) stats = make([]*WorkerStat, config.Connections) queryGenerator = NewRandomQueryGenerator(RandomQueryGeneratorOptions{ Metrics: metrics, // call method probabilitites ListCurrentValuesProbability: 50, // current values / others ListPeriodsProbability: 80, // periods / measures // time range probabilities TimeRangeProbabilities: []int{ 82, // last day 12, // last week 3, // last month 3, // any range }, }) ) for i := range stats { stats[i] = &WorkerStat{ MethodCalls: make([]int, methodsQty), ElapsedTimeByMethods: make([]time.Duration, methodsQty), TimeRangeCalls: make([]int, timeRangesQty), ElapsedTimeByTimeRanges: make([]time.Duration, timeRangesQty), } } t1 := time.Now() for i := range config.Connections { wg.Add(1) go func(stat *WorkerStat) { defer wg.Done() conn, err := client.Connect(config.DatabaseAddr) if err != nil { log.Fatalln(err) } defer conn.Close() for range config.RequestsPerConn { err := execQuery(conn, queryGenerator, stat) if err != nil { log.Println(err) } } }(stats[i]) } wg.Wait() testingTime := time.Since(t1) var ( methodCalls = make([]int, methodsQty) elapsedTimeByMethods = make([]time.Duration, methodsQty) timeRangeCalls = make([]int, timeRangesQty) elapsedTimeByTimeRanges = make([]time.Duration, timeRangesQty) totalElapsedTime time.Duration totalQueries int avgTimePerQuery time.Duration rps float64 ) for _, stat := range stats { totalElapsedTime += stat.ElapsedTime totalQueries += stat.Queries for i, elapsedTime := range stat.ElapsedTimeByMethods { elapsedTimeByMethods[i] += elapsedTime } for i, qty := range stat.MethodCalls { methodCalls[i] += qty } for i, elapsedTime := range stat.ElapsedTimeByTimeRanges { elapsedTimeByTimeRanges[i] += elapsedTime } for i, qty := range stat.TimeRangeCalls { timeRangeCalls[i] += qty } } avgTimePerQuery = totalElapsedTime / time.Duration(totalQueries) rps = float64(config.Connections*config.RequestsPerConn) / testingTime.Seconds() fmt.Printf(`TEST RESULTS: Time: %.0f seconds Connections: %d Requests per conn: %d Total requests: %d AVG request time: %v RPS: %d `, testingTime.Seconds(), config.Connections, config.RequestsPerConn, totalQueries, avgTimePerQuery, int(rps)) for i, calls := range methodCalls { totalElapsedTimeByMethod := elapsedTimeByMethods[i] methodPercent := float64(calls*100) / float64(totalQueries) fmt.Printf("%s: %d (%.1f%%), AVG request time: %v\n", methodCodeToName[i], calls, methodPercent, totalElapsedTimeByMethod/time.Duration(calls)) } fmt.Println() for i, calls := range timeRangeCalls { totalElapsedTimeByTimeRange := elapsedTimeByTimeRanges[i] timeRangePercent := float64(calls*100) / float64(totalQueries-methodCalls[listCurrentValues]) fmt.Printf("%s: %d (%.1f%%), AVG request time: %v\n", rangeCodeToName[i], calls, timeRangePercent, totalElapsedTimeByTimeRange/time.Duration(calls)) } } // CONFIG FILE const mainUsage = `Usage: %s -c path/to/config.ini ` const helpMessage = `Diploma project. Load test. Version: 1.0 created by Dmytro Gordenko, 1.e4.kc6@gmail.com ` const configExample = ` loadtest.ini example: databaseAddr = :12345 metricsInfo = ../../datadir/metrics.info connections = 1000 requestsPerConn = 500 ` type Config struct { DatabaseAddr string MetricsInfo string Connections int RequestsPerConn int } func (s Config) String() string { return fmt.Sprintf(`starting options: databaseAddr = %s metricsInfo = %s connections = %d requestsPerConn = %d `, s.DatabaseAddr, s.MetricsInfo, s.Connections, s.RequestsPerConn) } func loadConfig(iniFileName string) (_ Config, err error) { file, err := ini.Load(iniFileName) if err != nil { return } conf := Config{} top := file.Section("") conf.DatabaseAddr = top.Key("databaseAddr").String() conf.MetricsInfo = top.Key("metricsInfo").String() conf.Connections, err = top.Key("connections").Int() if err != nil { err = fmt.Errorf("'connections' option is required in config file") return } conf.RequestsPerConn, err = top.Key("requestsPerConn").Int() if err != nil { err = fmt.Errorf("'requestsPerConn' option is required in config file") return } return conf, nil }