package main import ( "fmt" "math" "math/rand" "net" "os" "strconv" "strings" "sync/atomic" "time" "github.com/tidwall/redbench" "github.com/tidwall/tile38/core" ) var ( hostname = "127.0.0.1" port = 9851 clients = 50 requests = 100000 quiet = false pipeline = 1 csv = false json = false tests = "PING,SET,GET,INTERSECTS,WITHIN,NEARBY,EVAL" redis = false ) var addr string func showHelp() bool { gitsha := "" if core.GitSHA == "" || core.GitSHA == "0000000" { gitsha = "" } else { gitsha = " (git:" + core.GitSHA + ")" } fmt.Fprintf(os.Stdout, "tile38-benchmark %s%s\n\n", core.Version, gitsha) fmt.Fprintf(os.Stdout, "Usage: tile38-benchmark [-h ] [-p ] [-c ] [-n ]\n") fmt.Fprintf(os.Stdout, " -h Server hostname (default: %s)\n", hostname) fmt.Fprintf(os.Stdout, " -p Server port (default: %d)\n", port) fmt.Fprintf(os.Stdout, " -c Number of parallel connections (default %d)\n", clients) fmt.Fprintf(os.Stdout, " -n Total number or requests (default %d)\n", requests) fmt.Fprintf(os.Stdout, " -q Quiet. Just show query/sec values\n") fmt.Fprintf(os.Stdout, " -P Pipeline requests. Default 1 (no pipeline).\n") fmt.Fprintf(os.Stdout, " -t Only run the comma separated list of tests. The test\n") fmt.Fprintf(os.Stdout, " names are the same as the ones produced as output.\n") fmt.Fprintf(os.Stdout, " --csv Output in CSV format.\n") fmt.Fprintf(os.Stdout, " --json Request JSON responses (default is RESP output)\n") fmt.Fprintf(os.Stdout, " --redis Runs against a Redis server\n") fmt.Fprintf(os.Stdout, "\n") return false } func parseArgs() bool { defer func() { if v := recover(); v != nil { if v, ok := v.(string); ok && v == "bad arg" { showHelp() } } }() args := os.Args[1:] readArg := func(arg string) string { if len(args) == 0 { panic("bad arg") } var narg = args[0] args = args[1:] return narg } readIntArg := func(arg string) int { n, err := strconv.ParseUint(readArg(arg), 10, 64) if err != nil { panic("bad arg") } return int(n) } badArg := func(arg string) bool { fmt.Fprintf(os.Stderr, "Unrecognized option or bad number of args for: '%s'\n", arg) return false } for len(args) > 0 { arg := readArg("") if arg == "--help" || arg == "-?" { return showHelp() } if !strings.HasPrefix(arg, "-") { args = append([]string{arg}, args...) break } switch arg { default: return badArg(arg) case "-h": hostname = readArg(arg) case "-p": port = readIntArg(arg) case "-c": clients = readIntArg(arg) if clients <= 0 { clients = 1 } case "-n": requests = readIntArg(arg) if requests <= 0 { requests = 0 } case "-q": quiet = true case "-P": pipeline = readIntArg(arg) if pipeline <= 0 { pipeline = 1 } case "-t": tests = readArg(arg) case "--csv": csv = true case "--json": json = true case "--redis": redis = true } } return true } func fillOpts() *redbench.Options { opts := *redbench.DefaultOptions opts.CSV = csv opts.Clients = clients opts.Pipeline = pipeline opts.Quiet = quiet opts.Requests = requests opts.Stderr = os.Stderr opts.Stdout = os.Stdout return &opts } func randPoint() (lat, lon float64) { return rand.Float64()*180 - 90, rand.Float64()*360 - 180 } func isValidRect(minlat, minlon, maxlat, maxlon float64) bool { return minlat > -90 && maxlat < 90 && minlon > -180 && maxlon < 180 } func randRect(meters float64) (minlat, minlon, maxlat, maxlon float64) { for { lat, lon := randPoint() maxlat, _ = destinationPoint(lat, lon, meters, 0) _, maxlon = destinationPoint(lat, lon, meters, 90) minlat, _ = destinationPoint(lat, lon, meters, 180) _, minlon = destinationPoint(lat, lon, meters, 270) if isValidRect(minlat, minlon, maxlat, maxlon) { return } } } func prepFn(conn net.Conn) bool { if json { conn.Write([]byte("output json\r\n")) resp := make([]byte, 100) conn.Read(resp) } return true } func main() { rand.Seed(time.Now().UnixNano()) if !parseArgs() { return } opts := fillOpts() addr = fmt.Sprintf("%s:%d", hostname, port) for _, test := range strings.Split(tests, ",") { switch strings.ToUpper(strings.TrimSpace(test)) { case "PING": redbench.Bench("PING", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "PING") }, ) case "GEOADD": //GEOADD key longitude latitude member if redis { var i int64 redbench.Bench("GEOADD", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) lat, lon := randPoint() return redbench.AppendCommand(buf, "GEOADD", "key:bench", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "id:"+strconv.FormatInt(i, 10), ) }, ) } case "SET", "SET-POINT", "SET-RECT", "SET-STRING": if redis { redbench.Bench("SET", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "SET", "key:__rand_int__", "xxx") }, ) } else { var i int64 switch strings.ToUpper(strings.TrimSpace(test)) { case "SET", "SET-POINT": redbench.Bench("SET (point)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) lat, lon := randPoint() return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), ) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "SET", "SET-RECT": redbench.Bench("SET (rect)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) minlat, minlon, maxlat, maxlon := randRect(10000) return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS", strconv.FormatFloat(minlat, 'f', 5, 64), strconv.FormatFloat(minlon, 'f', 5, 64), strconv.FormatFloat(maxlat, 'f', 5, 64), strconv.FormatFloat(maxlon, 'f', 5, 64), ) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "SET", "SET-STRING": redbench.Bench("SET (string)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "STRING", "xxx") }, ) } } case "GET": if redis { redbench.Bench("GET", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "GET", "key:__rand_int__") }, ) } else { var i int64 redbench.Bench("GET (point)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT") }, ) redbench.Bench("GET (rect)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS") }, ) redbench.Bench("GET (string)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "OBJECT") }, ) } case "INTERSECTS", "INTERSECTS-RECT", "INTERSECTS-RECT-1000", "INTERSECTS-RECT-10000", "INTERSECTS-RECT-100000", "INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-1000", "INTERSECTS-CIRCLE-10000", "INTERSECTS-CIRCLE-100000": if redis { break } switch strings.ToUpper(strings.TrimSpace(test)) { case "INTERSECTS", "INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-1000": redbench.Bench("INTERSECTS (intersects-circle 1km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "INTERSECTS", "key:bench", "COUNT", "CIRCLE", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "1000") }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "INTERSECTS", "INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-10000": redbench.Bench("INTERSECTS (intersects-circle 10km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "INTERSECTS", "key:bench", "COUNT", "CIRCLE", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "10000") }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "INTERSECTS", "INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-100000": redbench.Bench("INTERSECTS (intersects-circle 100km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "INTERSECTS", "key:bench", "COUNT", "CIRCLE", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "100000") }, ) } // INTERSECTS-BOUNDS switch strings.ToUpper(strings.TrimSpace(test)) { case "INTERSECTS", "INTERSECTS-BOUNDS", "INTERSECTS-BOUNDS-1000": minlat, minlon, maxlat, maxlon := randRect(1000) redbench.Bench("INTERSECTS (intersects-bounds 1km)", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "INTERSECTS", "key:bench", "COUNT", "BOUNDS", strconv.FormatFloat(minlat, 'f', 5, 64), strconv.FormatFloat(minlon, 'f', 5, 64), strconv.FormatFloat(maxlat, 'f', 5, 64), strconv.FormatFloat(maxlon, 'f', 5, 64)) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "INTERSECTS", "INTERSECTS-BOUNDS", "INTERSECTS-BOUNDS-10000": minlat, minlon, maxlat, maxlon := randRect(10000) redbench.Bench("INTERSECTS (intersects-bounds 10km)", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "INTERSECTS", "key:bench", "COUNT", "BOUNDS", strconv.FormatFloat(minlat, 'f', 5, 64), strconv.FormatFloat(minlon, 'f', 5, 64), strconv.FormatFloat(maxlat, 'f', 5, 64), strconv.FormatFloat(maxlon, 'f', 5, 64)) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "INTERSECTS", "INTERSECTS-BOUNDS", "INTERSECTS-BOUNDS-100000": minlat, minlon, maxlat, maxlon := randRect(10000) redbench.Bench("INTERSECTS (intersects-bounds 100km)", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "INTERSECTS", "key:bench", "COUNT", "BOUNDS", strconv.FormatFloat(minlat, 'f', 5, 64), strconv.FormatFloat(minlon, 'f', 5, 64), strconv.FormatFloat(maxlat, 'f', 5, 64), strconv.FormatFloat(maxlon, 'f', 5, 64)) }, ) } case "WITHIN", "WITHIN-RECT", "WITHIN-RECT-1000", "WITHIN-RECT-10000", "WITHIN-RECT-100000", "WITHIN-CIRCLE", "WITHIN-CIRCLE-1000", "WITHIN-CIRCLE-10000", "WITHIN-CIRCLE-100000": if redis { break } switch strings.ToUpper(strings.TrimSpace(test)) { case "WITHIN", "WITHIN-CIRCLE", "WITHIN-CIRCLE-1000": redbench.Bench("WITHIN (within-circle 1km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "WITHIN", "key:bench", "COUNT", "CIRCLE", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "1000") }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "WITHIN", "WITHIN-CIRCLE", "WITHIN-CIRCLE-10000": redbench.Bench("WITHIN (within-circle 10km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "WITHIN", "key:bench", "COUNT", "CIRCLE", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "10000") }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "WITHIN", "WITHIN-CIRCLE", "WITHIN-CIRCLE-100000": redbench.Bench("WITHIN (within-circle 100km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "WITHIN", "key:bench", "COUNT", "CIRCLE", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "100000") }, ) } // WITHIN-BOUNDS switch strings.ToUpper(strings.TrimSpace(test)) { case "WITHIN", "WITHIN-BOUNDS", "WITHIN-BOUNDS-1000": minlat, minlon, maxlat, maxlon := randRect(1000) redbench.Bench("WITHIN (within-bounds 1km)", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "WITHIN", "key:bench", "COUNT", "BOUNDS", strconv.FormatFloat(minlat, 'f', 5, 64), strconv.FormatFloat(minlon, 'f', 5, 64), strconv.FormatFloat(maxlat, 'f', 5, 64), strconv.FormatFloat(maxlon, 'f', 5, 64)) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "WITHIN", "WITHIN-BOUNDS", "WITHIN-BOUNDS-10000": minlat, minlon, maxlat, maxlon := randRect(10000) redbench.Bench("WITHIN (within-bounds 10km)", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "WITHIN", "key:bench", "COUNT", "BOUNDS", strconv.FormatFloat(minlat, 'f', 5, 64), strconv.FormatFloat(minlon, 'f', 5, 64), strconv.FormatFloat(maxlat, 'f', 5, 64), strconv.FormatFloat(maxlon, 'f', 5, 64)) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "WITHIN", "WITHIN-BOUNDS", "WITHIN-BOUNDS-100000": minlat, minlon, maxlat, maxlon := randRect(10000) redbench.Bench("WITHIN (within-bounds 100km)", addr, opts, prepFn, func(buf []byte) []byte { return redbench.AppendCommand(buf, "WITHIN", "key:bench", "COUNT", "BOUNDS", strconv.FormatFloat(minlat, 'f', 5, 64), strconv.FormatFloat(minlon, 'f', 5, 64), strconv.FormatFloat(maxlat, 'f', 5, 64), strconv.FormatFloat(maxlon, 'f', 5, 64)) }, ) } case "NEARBY", "NEARBY-KNN", "NEARBY-KNN-1", "NEARBY-KNN-10", "NEARBY-KNN-100", "NEARBY-POINT", "NEARBY-POINT-1000", "NEARBY-POINT-10000", "NEARBY-POINT-100000": if redis { break } switch strings.ToUpper(strings.TrimSpace(test)) { case "NEARBY", "NEARBY-KNN", "NEARBY-KNN-1": redbench.Bench("NEARBY (limit 1)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "NEARBY", "key:bench", "LIMIT", "1", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), ) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "NEARBY", "NEARBY-KNN", "NEARBY-KNN-10": redbench.Bench("NEARBY (limit 10)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "NEARBY", "key:bench", "LIMIT", "10", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), ) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "NEARBY", "NEARBY-KNN", "NEARBY-KNN-100": redbench.Bench("NEARBY (limit 100)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "NEARBY", "key:bench", "LIMIT", "100", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), ) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "NEARBY", "NEARBY-POINT", "NEARBY-POINT-1000": redbench.Bench("NEARBY (point 1km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "1000", ) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "NEARBY", "NEARBY-POINT", "NEARBY-POINT-10000": redbench.Bench("NEARBY (point 10km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "10000", ) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "NEARBY", "NEARBY-POINT", "NEARBY-POINT-100000": redbench.Bench("NEARBY (point 100km)", addr, opts, prepFn, func(buf []byte) []byte { lat, lon := randPoint() return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT", strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), "100000", ) }, ) } case "EVAL": if redis { break } var i int64 getScript := "return tile38.call('GET', KEYS[1], ARGV[1], 'point')" get4Script := "local a = tile38.call('GET', KEYS[1], ARGV[1], 'point');" + "local b = tile38.call('GET', KEYS[1], ARGV[2], 'point');" + "local c = tile38.call('GET', KEYS[1], ARGV[3], 'point');" + "local d = tile38.call('GET', KEYS[1], ARGV[4], 'point');" + "return d" setScript := "return tile38.call('SET', KEYS[1], ARGV[1], 'point', ARGV[2], ARGV[3])" if !opts.Quiet { fmt.Println("Scripts to run:") fmt.Println("GET SCRIPT: " + getScript) fmt.Println("GET FOUR SCRIPT: " + get4Script) fmt.Println("SET SCRIPT: " + setScript) } redbench.Bench("EVAL (set point)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) lat, lon := randPoint() return redbench.AppendCommand(buf, "EVAL", setScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10), strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), ) }, ) redbench.Bench("EVALNA (set point)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) lat, lon := randPoint() return redbench.AppendCommand(buf, "EVALNA", setScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10), strconv.FormatFloat(lat, 'f', 5, 64), strconv.FormatFloat(lon, 'f', 5, 64), ) }, ) redbench.Bench("EVALRO (get point)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) args := []string{"EVALRO", getScript, "1", "key:bench", "id:" + strconv.FormatInt(i, 10)} return redbench.AppendCommand(buf, args...) }, ) redbench.Bench("EVALRO (get 4 points)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) args := []string{ "EVALRO", get4Script, "1", "key:bench", "id:" + strconv.FormatInt(i, 10), "id:" + strconv.FormatInt(i+1, 10), "id:" + strconv.FormatInt(i+2, 10), "id:" + strconv.FormatInt(i+3, 10), } return redbench.AppendCommand(buf, args...) }, ) redbench.Bench("EVALNA (get point)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) return redbench.AppendCommand(buf, "EVALNA", getScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10)) }, ) } } } const earthRadius = 6371e3 func toRadians(deg float64) float64 { return deg * math.Pi / 180 } func toDegrees(rad float64) float64 { return rad * 180 / math.Pi } // destinationPoint return the destination from a point based on a distance and bearing. func destinationPoint(lat, lon, meters, bearingDegrees float64) (destLat, destLon float64) { // see http://williams.best.vwh.net/avform.htm#LL δ := meters / earthRadius // angular distance in radians θ := toRadians(bearingDegrees) φ1 := toRadians(lat) λ1 := toRadians(lon) φ2 := math.Asin(math.Sin(φ1)*math.Cos(δ) + math.Cos(φ1)*math.Sin(δ)*math.Cos(θ)) λ2 := λ1 + math.Atan2(math.Sin(θ)*math.Sin(δ)*math.Cos(φ1), math.Cos(δ)-math.Sin(φ1)*math.Sin(φ2)) λ2 = math.Mod(λ2+3*math.Pi, 2*math.Pi) - math.Pi // normalise to -180..+180° return toDegrees(φ2), toDegrees(λ2) }