package main

import (
	"database/sql"
	"flag"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"os"
	"strings"
	"time"
	"math/rand"
	"sync"
	"strconv"
)

type (
	subMap map[int64]int64
)

const (
	CHARSET = "utf8mb4"
)

var (
	db         *sql.DB
	dbErr      error
	errCounter int
	// params
	help         bool
	output       bool
	host         string
	port         int
	databaseName string
	username     string
	passwd       string
	concurrency  int
	runTime      int
	sbtable_num  int
	sbtable_size int
	dbTimeout    int
)

func usage() {
	fmt.Fprintf(os.Stderr, `sd-benchmark version: 1.0.1
Desc : MySQL bench mark tools 
Usage: sd-benchmark [-h Connect to host.] [-P Port number to use for connection] [-d database name] [-u User for login if not current user.] [-p Password to use when connecting to server] [-r running time] [-f SQL file] [-t timeout] [-o true | false]

Options:
`)
	flag.PrintDefaults()
}

var tableNameMap = struct{
    sync.Mutex
    m map[int]string
}{m: make(map[int]string)}

func init() {
	//version = flag.Bool("V", false, "show version and exit")
	flag.BoolVar(&help, "H", false, "this help")
	flag.StringVar(&host, "h", "127.0.0.1", "Connect to host.")
	flag.IntVar(&port, "P", 3306, "Port number to use for connection.")
	flag.StringVar(&databaseName, "d", "information_schema", "Database to use.")
	flag.StringVar(&username, "u", "root", "User for login if not current user.")
	flag.StringVar(&passwd, "p", "", "Password to use when connecting to server. (default \"\")")
	flag.IntVar(&concurrency, "c", 64, "Number of threads.")
	flag.IntVar(&runTime, "r", 600, "running time.")
	flag.IntVar(&sbtable_num, "f", 10, "Number of sysbench tables. (default 10")
	flag.IntVar(&sbtable_size, "g", 100, "Number of keys in sysbench tables. (default 100")
	flag.IntVar(&dbTimeout, "t", 60, "connect MySQL timeout.")
	flag.BoolVar(&output, "o", false, "output error info.")
	flag.Usage = usage
}

func RandInt(rd *rand.Rand, start int, end int ) int {
	return rd.Intn(end - start + 1) + start
}

func main() {

	flag.Parse()
	if help {
		flag.Usage()
		return
	}

	// initialize db
	getDB(host, port, databaseName, username, passwd)
	defer db.Close()

	queueMap := make(map[int]subMap)
	for i := 1; i <= concurrency; i++ {
		queueMap[i] = make(subMap)
	}
	startTime := time.Now()
	fmt.Printf("\nINFO: %s start performance testing (%d threads) ... \n", startTime.Format("2006-01-02 15:04:05"), concurrency)
	for i := 1; i <= concurrency; i++ {
		go subProcess(int64(i), queueMap[i])
	}

	select {
	case <-time.After(time.Duration(runTime) * time.Second):
		fmt.Printf("INFO: %s DONE.\n", time.Now().Format("2006-01-02 15:04:05"))
		endTime := time.Now()
		elapsed := (endTime.UnixNano() - startTime.UnixNano()) / int64(time.Millisecond)
		analyzePerformanceResult(queueMap, float64(elapsed)/1000)
	}
}

func getDB(host string, port int, dbName string, user string, passwd string) {
	dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&timeout=%ds&readTimeout=%ds", user, passwd, host, port, dbName, CHARSET, dbTimeout, dbTimeout)

	db, dbErr = sql.Open("mysql", dbDSN)
	//defer MysqlDb.Close();
	if dbErr != nil {
		if output {
			fmt.Println("dbDSN: " + dbDSN)
			panic("ERROR: Data source configuration is incorrect, err: " + dbErr.Error())
		}
	}

	db.SetMaxOpenConns(concurrency * 2)
	db.SetMaxIdleConns(100)
	db.SetConnMaxLifetime(600 * time.Second)

	if dbErr = db.Ping(); nil != dbErr {
		if output {
			fmt.Println("db conn=", dbDSN)
			panic("connect database failed ,err: " + dbErr.Error())
		}
	}
}

func getCurrentTableName(tableId int) string {
	tableNameMap.Lock();
	defer tableNameMap.Unlock();
	if tableName, ok := tableNameMap.m[tableId]; ok {
		return tableName
	} else {
		tableNameMap.m[tableId] = "sbtest" + strconv.Itoa(tableId)
		return  "sbtest" + strconv.Itoa(tableId)
	}
}

/* return previous name and current name */
func alterCurrentTableName(tableId int) (string, string) {
	tableNameMap.Lock();
	defer tableNameMap.Unlock();
	if oldTableName, ok := tableNameMap.m[tableId]; ok {
		newTableName := ""
		if strings.HasSuffix(oldTableName, "tmp") {
			newTableName = strings.TrimSuffix(oldTableName, "tmp")			
		} else {
			newTableName = oldTableName + "tmp"
		}
		tableNameMap.m[tableId] = newTableName
		return oldTableName, newTableName
	} else {
		tableNameMap.m[tableId] = "sbtest" + strconv.Itoa(tableId)
		return  "", "sbtest" + strconv.Itoa(tableId)
	}
}

func subProcess(id int64, costMap subMap) {
	seed := rand.NewSource(id)
	rd := rand.New(seed)
	for {
		dice := rd.Intn(1000)
		sql := ""
		tableId := RandInt(rd, 1, sbtable_num)
		if dice == 0 {
			oldTableName, newTableName := alterCurrentTableName(tableId)
			if len(oldTableName) == 0 {
				sql = "select 1"
			} else {
				sql = "ALTER TABLE " + oldTableName + " RENAME " + newTableName
			}
		} else {
			keyId := RandInt(rd, 1, sbtable_size)
			tableName := getCurrentTableName(tableId)
			sql = "UPDATE " + tableName + " SET k=k+1 WHERE id=" + strconv.Itoa(keyId)
		}
		start := time.Now().UnixNano()
		_, err := db.Exec(sql)
		end := time.Now().UnixNano()
		if err != nil {
			if output {
				fmt.Printf("ERROR: query  exception sql: %s, err: %s\n", sql, err)
			}
			errCounter++
			continue
		}
		costTime := (end - start) / int64(time.Millisecond)
		if _, ok := costMap[costTime]; ok {
			costMap[costTime]++
		} else {
			costMap[costTime] = 1
		}
	}
}

func analyzePerformanceResult(queueMap map[int]subMap, elapsed float64) {

	fmt.Printf("INFO: %s start analysis report ... \n", time.Now().Format("2006-01-02 15:04:05"))
	var QPS, costTime, minTime, maxTime int64
	minTime = 20
	for _, item := range queueMap {
		//fmt.Printf("\nqueue %d:\n", queue)
		var tmpQPS, tmpCostTime, tmpMinTime, tmpMaxTime int64
		tmpMinTime = 20
		for k, v := range item {
			tmpQPS += v
			tmpCostTime += k * v
			if k > tmpMaxTime {
				tmpMaxTime = k
			}
			if k < tmpMinTime {
				tmpMinTime = k
			}
		}
		QPS += tmpQPS
		costTime += tmpCostTime
		if tmpMaxTime > maxTime {
			maxTime = tmpMaxTime
		}
		if tmpMinTime < minTime {
			minTime = tmpMinTime
		}
		//fmt.Println("qps=", tmpQPS)
		//fmt.Println("costTime=", tmpCostTime)
		//fmt.Println("minResponseTime=", tmpMinTime)
		//fmt.Println("maxResponseTime=", tmpMaxTime)
	}
	fmt.Printf("INFO: %s DONE.\n", time.Now().Format("2006-01-02 15:04:05"))

	fmt.Printf(`
SQL statistics:
    queries performed:
         read :                          - (nonsupport)
         write:                          - (nonsupport)
         other:                          - (nonsupport)
         total:                          %d
       queries:                          %d (%f per sec.)
ignored errors:                          %d (%f per sec.)


General statistics:
    number of threads:                   %d
    conn db timeout:                     %ds
    read db timeout:                     %ds
    total time:                          %fs


Latency (ms):
         min:                            %d
         avg:                            %f
         max:                            %d
         sum:                            %d
`, QPS+int64(errCounter), QPS, float64(QPS)/elapsed, errCounter, float64(errCounter)/elapsed, concurrency, dbTimeout, dbTimeout, elapsed, minTime, float32(costTime)/float32(QPS), maxTime, costTime)
}
