package main import ( "fmt" "log" "math" "math/rand" "net" "os" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/tidwall/redbench" "github.com/tidwall/redcon" "github.com/tidwall/tile38/cmd/tile38-benchmark/az" "github.com/tidwall/tile38/core" ) var ( hostname = "127.0.0.1" port = 9851 auth = "" clients = 50 requests = 100000 quiet = false pipeline = 1 csv = false json = false allTests = "PING,SET,GET,INTERSECTS,WITHIN,NEARBY,EVAL" tests = allTests 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, " -a Password for Tile38 Auth\n") 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 "-a": auth = readArg(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 { var resp [64]byte conn.Write([]byte("CONFIG GET requirepass\r\n")) n, err := conn.Read(resp[:]) if err != nil { log.Fatal(err) } if string(resp[:n]) == "-ERR authentication required\r\n" { if auth == "" { log.Fatal("invalid auth") } else { cmd := redcon.AppendArray(nil, 2) cmd = redcon.AppendBulkString(cmd, "AUTH") cmd = redcon.AppendBulkString(cmd, auth) conn.Write(cmd) n, err := conn.Read(resp[:]) if err != nil || string(resp[:n]) != "+OK\r\n" { log.Fatal("invalid auth") } } } else if auth != "" { log.Fatal("invalid auth") } if json { conn.Write([]byte("output json\r\n")) conn.Read(make([]byte, 64)) } return true } func main() { rand.Seed(time.Now().UnixNano()) if !parseArgs() { return } opts := fillOpts() addr = fmt.Sprintf("%s:%d", hostname, port) testsArr := strings.Split(allTests, ",") var subtract bool var add bool for _, test := range strings.Split(tests, ",") { if strings.HasPrefix(test, "-") { if add { os.Stderr.Write([]byte("test flag cannot mix add and subtract\n")) os.Exit(1) } subtract = true for i := range testsArr { if strings.EqualFold(testsArr[i], test[1:]) { testsArr = append(testsArr[:i], testsArr[i+1:]...) break } } } else if subtract { add = true os.Stderr.Write([]byte("test flag cannot mix add and subtract\n")) os.Exit(1) } } if !subtract { testsArr = strings.Split(tests, ",") } for _, test := range testsArr { 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-LINE", "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-LINE": redbench.Bench("SET (line)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) alat, alon, blat, blon := randRect(10000) if rand.Int()%2 == 0 { alat, alon, blat, blon = blat, blon, alat, alon } linestring := fmt.Sprintf(`{"type":"LineString","coordinates":[[%f,%f],[%f,%f]]}`, alon, alat, blon, blat) return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "OBJECT", linestring, ) }, ) } 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-BOUNDS", "INTERSECTS-BOUNDS-1000", "INTERSECTS-BOUNDS-10000", "INTERSECTS-BOUNDS-100000", "INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-1000", "INTERSECTS-CIRCLE-10000", "INTERSECTS-CIRCLE-100000", "INTERSECTS-AZ": 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)) }, ) } switch strings.ToUpper(strings.TrimSpace(test)) { case "INTERSECTS", "INTERSECTS-AZ": var mu sync.Mutex var loaded bool redbench.Bench("INTERSECTS (intersects-az limit 5)", addr, opts, func(conn net.Conn) bool { func() { mu.Lock() defer mu.Unlock() if loaded { return } loaded = true p := make([]byte, 0xFF) conn.Write([]byte("GET keys:bench:geo az point\r\n")) n, err := conn.Read(p) if err != nil { panic(err) } if string(p[:n]) != "$-1\r\n" { return } args := []string{"SET", "key:bench:geo", "az", "object", az.JSON} out := redcon.AppendArray(nil, len(args)) for _, arg := range args { out = redcon.AppendBulkString(out, arg) } conn.Write(out) n, err = conn.Read(p) if err != nil { panic(err) } if string(p[:n]) != "+OK\r\n" { panic("expected OK") } }() return prepFn(conn) }, func(buf []byte) []byte { args := []string{"INTERSECTS", "key:bench", "LIMIT", "5", "COUNT", "GET", "key:bench:geo", "az"} return redbench.AppendCommand(buf, args...) }, ) } 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 { 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) return redbench.AppendCommand(buf, "EVALRO", getScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10)) }, ) redbench.Bench("EVALRO (get 4 points)", addr, opts, prepFn, func(buf []byte) []byte { i := atomic.AddInt64(&i, 1) return redbench.AppendCommand(buf, "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), ) }, ) 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) }