├── .gitignore ├── README.md ├── client.go ├── client_test.go ├── config.json ├── deploy.sh ├── main.go ├── master_node.go ├── node.txt ├── response.go ├── single_node.go ├── slave_node.go ├── stats.go └── timer.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-wrk 0.1 2 | 3 | this is a small http benchmark utility similar to https://github.com/wg/wrk but written in go. 4 | it has a couple of features absent from wrk 5 | 6 | - https support (quite expensive on the client side with disabled keep alives) 7 | - http POST support 8 | - more statistics 9 | - leaner codebase 10 | 11 | ## status 12 | 13 | this tool is in early stage development but stable enough to run larger benchmark sets. 14 | missing features will be added as needed, pull requests are welcome ;) 15 | 16 | ## building 17 | 18 | you need go 1.0+ (1.1 is suggested for performance) 19 | 20 | ``` 21 | git clone git://github.com/adeven/go-wrk.git 22 | cd go-wrk 23 | go build 24 | ``` 25 | 26 | ## usage 27 | 28 | basic usage is quite simple: 29 | ``` 30 | go-wrk [flags] url 31 | ``` 32 | 33 | with the flags being 34 | ``` 35 | -H="User-Agent: go-wrk 0.1 bechmark\nContent-Type: text/html;": the http headers sent separated by '\n' 36 | -c=100: the max numbers of connections used 37 | -k=true: if keep-alives are disabled 38 | -i=false: if TLS security checks are disabled 39 | -m="GET": the http request method 40 | -n=1000: the total number of calls processed 41 | -t=1: the numbers of threads used 42 | -b="" the http request body 43 | -s="" if specified, it counts how often the searched string s is contained in the responses 44 | ``` 45 | for example 46 | ``` 47 | go-wrk -c=400 -t=8 -n=100000 http://localhost:8080/index.html 48 | ``` 49 | 50 | 51 | ## example output 52 | 53 | ``` 54 | ==========================BENCHMARK========================== 55 | URL: http://localhost:8509/startup?app_id=479516143&mac=123456789 56 | 57 | Used Connections: 100 58 | Used Threads: 1 59 | Total number of calls: 100000 60 | 61 | ============================TIMES============================ 62 | Total time passed: 19.47s 63 | Avg time per request: 19.45ms 64 | Requests per second: 5135.02 65 | Median time per request: 11.30ms 66 | 99th percentile time: 65.23ms 67 | Slowest time for request: 1698.00ms 68 | 69 | ==========================RESPONSES========================== 70 | 20X responses: 100000 (100%) 71 | 30X responses: 0 (0%) 72 | 40X responses: 0 (0%) 73 | 50X responses: 0 (0%) 74 | matchResponses: 100000 (100.00%) 75 | ``` 76 | 77 | ## License 78 | 79 | This Software is licensed under the MIT License. 80 | 81 | Copyright (c) 2013 adeven GmbH, 82 | http://www.adeven.com 83 | 84 | Permission is hereby granted, free of charge, to any person obtaining 85 | a copy of this software and associated documentation files (the 86 | "Software"), to deal in the Software without restriction, including 87 | without limitation the rights to use, copy, modify, merge, publish, 88 | distribute, sublicense, and/or sell copies of the Software, and to 89 | permit persons to whom the Software is furnished to do so, subject to 90 | the following conditions: 91 | 92 | The above copyright notice and this permission notice shall be 93 | included in all copies or substantial portions of the Software. 94 | 95 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 96 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 97 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 98 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 99 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 100 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 101 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 102 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | func StartClient(url_, heads, requestBody string, meth string, dka bool, responseChan chan *Response, waitGroup *sync.WaitGroup, tc int) { 15 | defer waitGroup.Done() 16 | 17 | var tr *http.Transport 18 | 19 | u, err := url.Parse(url_) 20 | 21 | if err == nil && u.Scheme == "https" { 22 | var tlsConfig *tls.Config 23 | if *insecure { 24 | tlsConfig = &tls.Config{ 25 | InsecureSkipVerify: true, 26 | } 27 | } else { 28 | // Load client cert 29 | cert, err := tls.LoadX509KeyPair(*certFile, *keyFile) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | // Load CA cert 35 | caCert, err := ioutil.ReadFile(*caFile) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | caCertPool := x509.NewCertPool() 40 | caCertPool.AppendCertsFromPEM(caCert) 41 | 42 | // Setup HTTPS client 43 | tlsConfig = &tls.Config{ 44 | Certificates: []tls.Certificate{cert}, 45 | RootCAs: caCertPool, 46 | } 47 | tlsConfig.BuildNameToCertificate() 48 | } 49 | 50 | tr = &http.Transport{TLSClientConfig: tlsConfig, DisableKeepAlives: dka} 51 | } else { 52 | tr = &http.Transport{DisableKeepAlives: dka} 53 | } 54 | 55 | timer := NewTimer() 56 | 57 | hdrs, _ := buildHeaders(heads) 58 | 59 | for { 60 | requestBodyReader := strings.NewReader(requestBody) 61 | req, _ := http.NewRequest(meth, url_, requestBodyReader) 62 | 63 | for key, vals := range hdrs { 64 | for _, val := range vals { 65 | req.Header.Set(key, val) 66 | } 67 | } 68 | 69 | timer.Reset() 70 | 71 | resp, err := tr.RoundTrip(req) 72 | 73 | respObj := &Response{} 74 | 75 | if err != nil { 76 | respObj.Error = true 77 | } else { 78 | if resp.ContentLength < 0 { // -1 if the length is unknown 79 | data, err := ioutil.ReadAll(resp.Body) 80 | if err == nil { 81 | respObj.Size = int64(len(data)) 82 | } 83 | } else { 84 | respObj.Size = resp.ContentLength 85 | if *respContains != "" { 86 | data, err := ioutil.ReadAll(resp.Body) 87 | if err == nil { 88 | respObj.Body = string(data) 89 | } 90 | } 91 | } 92 | respObj.StatusCode = resp.StatusCode 93 | 94 | resp.Body.Close() 95 | } 96 | 97 | respObj.Duration = timer.Duration() 98 | 99 | if len(responseChan) >= tc { 100 | break 101 | } 102 | responseChan <- respObj 103 | } 104 | } 105 | 106 | // buildHeaders build the HTTP Request headers from the parsed flag -H or 107 | // from the default header set. 108 | // The headers are "set" (not added), thus same key values get replaced. 109 | // Note: if a key has no value, it is not added into the Headers, by original 110 | // package design. 111 | func buildHeaders(heads string) (http.Header, error) { 112 | 113 | heads = strings.Replace(heads, `\n`, "\n", -1) 114 | h := http.Header{} 115 | 116 | sets := strings.Split(heads, "\n") 117 | for i := range sets { 118 | split := strings.SplitN(sets[i], ":", 2) 119 | if len(split) == 2 { 120 | h.Set(split[0], split[1]) 121 | } 122 | } 123 | 124 | return h, nil 125 | } 126 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "sort" 7 | "testing" 8 | ) 9 | 10 | func TestBuildHeaders(t *testing.T) { 11 | 12 | var testSet = []struct { 13 | H string // headers passed via flag 14 | Res map[string][]string 15 | }{ 16 | { 17 | "User-Agent:go-wrk 0.1 bechmark\nContent-Type:text/html;", 18 | map[string][]string{ 19 | "User-Agent": []string{"go-wrk 0.1 bechmark"}, 20 | "Content-Type": []string{"text/html;"}, 21 | }, 22 | }, 23 | { 24 | "Key:Value", 25 | map[string][]string{ 26 | "Key": []string{"Value"}, 27 | }, 28 | }, 29 | { 30 | "Key1:Value1\nKey2:Value2", 31 | map[string][]string{ 32 | "Key1": []string{"Value1"}, 33 | "Key2": []string{"Value2"}, 34 | }, 35 | }, 36 | { 37 | // the headers are set (not added) thus same key values 38 | // are replaced. 39 | "Key1:Value1A\nKey1:Value1B", 40 | map[string][]string{ 41 | "Key1": []string{"Value1B"}, 42 | }, 43 | }, 44 | { 45 | // a key with no value gets removed by design of the package. 46 | "Key1", 47 | map[string][]string{}, 48 | }, 49 | } 50 | 51 | for _, set := range testSet { 52 | 53 | tmpHeaders := http.Header{} 54 | for k, v := range set.Res { 55 | tmpHeaders[k] = append(tmpHeaders[k], v...) 56 | sort.Strings(tmpHeaders[k]) 57 | } 58 | 59 | headers, _ := buildHeaders(set.H) 60 | for _, v := range headers { 61 | sort.Strings(v) 62 | } 63 | 64 | // comparison; using the not very efficient reflect.DeepEqual 65 | // because its a small test suite. 66 | if !reflect.DeepEqual(tmpHeaders, headers) { 67 | t.Errorf("Different results") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Port" : "7777", 3 | "Nodes" : [ 4 | "192.168.178.121:7777" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go build -o go-wrk *.go 4 | 5 | while read node 6 | do 7 | scp go-wrk root@$node: 8 | scp config.json root@$node: 9 | ssh root@$node '/root/go-wrk -d s -f /root/config.json /var/log/root-backup.log 2>&1 &' 10 | done < node.txt 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "runtime" 10 | ) 11 | 12 | type Config struct { 13 | Port string 14 | Nodes []string 15 | } 16 | 17 | var ( 18 | numThreads = flag.Int("t", 1, "the numbers of threads used") 19 | method = flag.String("m", "GET", "the http request method") 20 | requestBody = flag.String("b", "", "the http request body") 21 | requestBodyFile = flag.String("p", "", "the http request body data file") 22 | numConnections = flag.Int("c", 100, "the max numbers of connections used") 23 | totalCalls = flag.Int("n", 1000, "the total number of calls processed") 24 | disableKeepAlives = flag.Bool("k", true, "if keep-alives are disabled") 25 | dist = flag.String("d", "", "dist mode") 26 | configFile = flag.String("f", "", "json config file") 27 | config Config 28 | target string 29 | headers = flag.String("H", "User-Agent: go-wrk 0.1 benchmark\nContent-Type: text/html;", "the http headers sent separated by '\\n'") 30 | certFile = flag.String("cert", "someCertFile", "A PEM eoncoded certificate file.") 31 | keyFile = flag.String("key", "someKeyFile", "A PEM encoded private key file.") 32 | caFile = flag.String("CA", "someCertCAFile", "A PEM eoncoded CA's certificate file.") 33 | insecure = flag.Bool("i", false, "TLS checks are disabled") 34 | respContains = flag.String("s", "", "if specified, it counts how often the searched string s is contained in the responses") 35 | readAll = flag.Bool("r", false, "in the case of having stream or file in the response,\n it reads all response body to calculate the response size") 36 | ) 37 | 38 | func init() { 39 | flag.Parse() 40 | target = os.Args[len(os.Args)-1] 41 | if *configFile != "" { 42 | readConfig() 43 | } 44 | runtime.GOMAXPROCS(*numThreads) 45 | } 46 | 47 | func readConfig() { 48 | configData, err := ioutil.ReadFile(*configFile) 49 | if err != nil { 50 | fmt.Println(err) 51 | panic(err) 52 | } 53 | err = json.Unmarshal(configData, &config) 54 | if err != nil { 55 | fmt.Println(err) 56 | panic(err) 57 | } 58 | } 59 | 60 | func setRequestBody() { 61 | // requestBody has been setup and it has highest priority 62 | if *requestBody != "" { 63 | return 64 | } 65 | 66 | if *requestBodyFile == "" { 67 | return 68 | } 69 | 70 | // requestBodyFile has been setup 71 | data, err := ioutil.ReadFile(*requestBodyFile) 72 | if err != nil { 73 | fmt.Println(err) 74 | panic(err) 75 | } 76 | body := string(data) 77 | requestBody = &body 78 | } 79 | 80 | func main() { 81 | if len(os.Args) == 1 { //If no argument specified 82 | flag.Usage() //Print usage 83 | os.Exit(1) 84 | } 85 | setRequestBody() 86 | switch *dist { 87 | case "m": 88 | MasterNode() 89 | case "s": 90 | SlaveNode() 91 | default: 92 | SingleNode(target) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /master_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "sync" 9 | ) 10 | 11 | func MasterNode() { 12 | distChannel := make(chan string, len(config.Nodes)*2) 13 | wg := &sync.WaitGroup{} 14 | for _, node := range config.Nodes { 15 | go runChild(distChannel, wg, node) 16 | wg.Add(1) 17 | } 18 | wg.Wait() 19 | CalcDistStats(distChannel) 20 | } 21 | 22 | func runChild(distChan chan string, wg *sync.WaitGroup, node string) { 23 | defer wg.Done() 24 | toCall := fmt.Sprintf( 25 | "http://%s/t=%d&m=%s&c=%d&n=%d&k=%t&url=%s", 26 | node, 27 | *numThreads, 28 | *method, 29 | *numConnections, 30 | *totalCalls, 31 | *disableKeepAlives, 32 | url.QueryEscape(url.QueryEscape(target)), 33 | ) 34 | fmt.Println(toCall) 35 | resp, err := http.Get(toCall) 36 | if err != nil { 37 | fmt.Println(err) 38 | return 39 | } 40 | defer resp.Body.Close() 41 | body, err := ioutil.ReadAll(resp.Body) 42 | if err != nil { 43 | fmt.Println(err) 44 | } 45 | distChan <- string(body) 46 | } 47 | -------------------------------------------------------------------------------- /node.txt: -------------------------------------------------------------------------------- 1 | 192.168.178.121 2 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Response struct { 4 | Size int64 5 | Duration int64 6 | StatusCode int 7 | Error bool 8 | Body string 9 | } 10 | -------------------------------------------------------------------------------- /single_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | func SingleNode(toCall string) []byte { 8 | responseChannel := make(chan *Response, *totalCalls*2) 9 | 10 | benchTime := NewTimer() 11 | benchTime.Reset() 12 | //TODO check ulimit 13 | wg := &sync.WaitGroup{} 14 | 15 | for i := 0; i < *numConnections; i++ { 16 | go StartClient( 17 | toCall, 18 | *headers, 19 | *requestBody, 20 | *method, 21 | *disableKeepAlives, 22 | responseChannel, 23 | wg, 24 | *totalCalls, 25 | ) 26 | wg.Add(1) 27 | } 28 | 29 | wg.Wait() 30 | 31 | result := CalcStats( 32 | responseChannel, 33 | benchTime.Duration(), 34 | ) 35 | return result 36 | } 37 | -------------------------------------------------------------------------------- /slave_node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strconv" 8 | ) 9 | 10 | func SlaveNode() { 11 | http.HandleFunc("/", rootHandler) 12 | err := http.ListenAndServe( 13 | fmt.Sprintf(":%s", config.Port), 14 | nil, 15 | ) 16 | if err != nil { 17 | fmt.Println(err) 18 | } 19 | select {} 20 | } 21 | 22 | func rootHandler(w http.ResponseWriter, req *http.Request) { 23 | values, err := url.ParseQuery(req.URL.String()[1:]) 24 | if err != nil { 25 | fmt.Fprintf(w, err.Error()) 26 | } 27 | *numThreads, _ = strconv.Atoi(values.Get("t")) 28 | *method = values.Get("m") 29 | *numConnections, _ = strconv.Atoi(values.Get("c")) 30 | *totalCalls, _ = strconv.Atoi(values.Get("n")) 31 | *disableKeepAlives, _ = strconv.ParseBool(values.Get("k")) 32 | toCall, _ := url.QueryUnescape(values.Get("url")) 33 | fmt.Fprintf(w, string(SingleNode(toCall))) 34 | } 35 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | type Stats struct { 12 | Url string 13 | Connections int 14 | Threads int 15 | AvgDuration float64 16 | Duration float64 17 | Sum float64 18 | Times []int 19 | Transferred int64 20 | Resp200 int64 21 | Resp300 int64 22 | Resp400 int64 23 | Resp500 int64 24 | Errors int64 25 | Contains int64 26 | } 27 | 28 | func CalcStats(responseChannel chan *Response, duration int64) []byte { 29 | 30 | stats := &Stats{ 31 | Url: target, 32 | Connections: *numConnections, 33 | Threads: *numThreads, 34 | Times: make([]int, len(responseChannel)), 35 | Duration: float64(duration), 36 | AvgDuration: float64(duration), 37 | } 38 | 39 | if *respContains != "" { 40 | log.Printf("search in response for: %v", *respContains) 41 | } 42 | 43 | i := 0 44 | for res := range responseChannel { 45 | switch { 46 | case res.StatusCode < 200: 47 | // error 48 | case res.StatusCode < 300: 49 | stats.Resp200++ 50 | case res.StatusCode < 400: 51 | stats.Resp300++ 52 | case res.StatusCode < 500: 53 | stats.Resp400++ 54 | case res.StatusCode < 600: 55 | stats.Resp500++ 56 | } 57 | 58 | if *respContains != "" && strings.Contains(res.Body, *respContains) { 59 | stats.Contains++ 60 | } 61 | 62 | stats.Sum += float64(res.Duration) 63 | stats.Times[i] = int(res.Duration) 64 | i++ 65 | 66 | stats.Transferred += res.Size 67 | 68 | if res.Error { 69 | stats.Errors++ 70 | } 71 | 72 | if len(responseChannel) == 0 { 73 | break 74 | } 75 | } 76 | 77 | sort.Ints(stats.Times) 78 | 79 | PrintStats(stats) 80 | b, err := json.Marshal(&stats) 81 | if err != nil { 82 | fmt.Println(err) 83 | } 84 | return b 85 | } 86 | 87 | func CalcDistStats(distChan chan string) { 88 | if len(distChan) == 0 { 89 | return 90 | } 91 | allStats := &Stats{ 92 | Url: target, 93 | Connections: *numConnections, 94 | Threads: *numThreads, 95 | } 96 | statCount := len(distChan) 97 | for res := range distChan { 98 | var stats Stats 99 | err := json.Unmarshal([]byte(res), &stats) 100 | if err != nil { 101 | fmt.Println(err) 102 | } 103 | allStats.Duration += stats.Duration 104 | allStats.Sum += stats.Sum 105 | allStats.Times = append(allStats.Times, stats.Times...) 106 | allStats.Resp200 += stats.Resp200 107 | allStats.Resp300 += stats.Resp300 108 | allStats.Resp400 += stats.Resp400 109 | allStats.Resp500 += stats.Resp500 110 | allStats.Errors += stats.Errors 111 | allStats.Contains += stats.Contains 112 | 113 | if len(distChan) == 0 { 114 | break 115 | } 116 | } 117 | allStats.AvgDuration = allStats.Duration / float64(statCount) 118 | PrintStats(allStats) 119 | } 120 | 121 | func PrintStats(allStats *Stats) { 122 | sort.Ints(allStats.Times) 123 | total := float64(len(allStats.Times)) 124 | totalInt := int64(total) 125 | fmt.Println("==========================BENCHMARK==========================") 126 | fmt.Printf("URL:\t\t\t\t%s\n\n", allStats.Url) 127 | fmt.Printf("Used Connections:\t\t%d\n", allStats.Connections) 128 | fmt.Printf("Used Threads:\t\t\t%d\n", allStats.Threads) 129 | fmt.Printf("Total number of calls:\t\t%d\n\n", totalInt) 130 | fmt.Println("===========================TIMINGS===========================") 131 | fmt.Printf("Total time passed:\t\t%.2fs\n", allStats.AvgDuration/1E6) 132 | fmt.Printf("Avg time per request:\t\t%.2fms\n", allStats.Sum/total/1000) 133 | fmt.Printf("Requests per second:\t\t%.2f\n", total/(allStats.AvgDuration/1E6)) 134 | fmt.Printf("Median time per request:\t%.2fms\n", float64(allStats.Times[(totalInt-1)/2])/1000) 135 | fmt.Printf("99th percentile time:\t\t%.2fms\n", float64(allStats.Times[(totalInt/100*99)])/1000) 136 | fmt.Printf("Slowest time for request:\t%.2fms\n\n", float64(allStats.Times[totalInt-1]/1000)) 137 | fmt.Println("=============================DATA=============================") 138 | fmt.Printf("Total response body sizes:\t\t%d\n", allStats.Transferred) 139 | fmt.Printf("Avg response body per request:\t\t%.2f Byte\n", float64(allStats.Transferred)/total) 140 | tr := float64(allStats.Transferred) / (allStats.AvgDuration / 1E6) 141 | fmt.Printf("Transfer rate per second:\t\t%.2f Byte/s (%.2f MByte/s)\n", tr, tr/1E6) 142 | fmt.Println("==========================RESPONSES==========================") 143 | fmt.Printf("20X Responses:\t\t%d\t(%.2f%%)\n", allStats.Resp200, float64(allStats.Resp200)/total*1e2) 144 | fmt.Printf("30X Responses:\t\t%d\t(%.2f%%)\n", allStats.Resp300, float64(allStats.Resp300)/total*1e2) 145 | fmt.Printf("40X Responses:\t\t%d\t(%.2f%%)\n", allStats.Resp400, float64(allStats.Resp400)/total*1e2) 146 | fmt.Printf("50X Responses:\t\t%d\t(%.2f%%)\n", allStats.Resp500, float64(allStats.Resp500)/total*1e2) 147 | if *respContains != "" { 148 | fmt.Printf("matchResponses:\t\t%d\t(%.2f%%)\n", allStats.Contains, float64(allStats.Contains)/total*1e2) 149 | } 150 | fmt.Printf("Errors:\t\t\t%d\t(%.2f%%)\n", allStats.Errors, float64(allStats.Errors)/total*1e2) 151 | } 152 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Timer struct { 8 | start time.Time 9 | } 10 | 11 | func NewTimer() *Timer { 12 | timer := &Timer{} 13 | return timer 14 | } 15 | 16 | func (timer *Timer) Reset() { 17 | timer.start = time.Now().UTC() 18 | } 19 | 20 | func (timer *Timer) Duration() int64 { 21 | now := time.Now().UTC() 22 | nanos := now.Sub(timer.start).Nanoseconds() 23 | micros := nanos / 1000 24 | return micros 25 | } 26 | --------------------------------------------------------------------------------