├── list.go ├── format.go ├── main_test.go ├── parse.go ├── main.go └── README.md /list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type stringList []string 9 | 10 | func (list *stringList) String() string { 11 | return fmt.Sprint(*list) 12 | } 13 | 14 | func (list *stringList) Set(value string) error { 15 | for _, elem := range strings.Split(value, ",") { 16 | list.Add(elem) 17 | } 18 | return nil 19 | } 20 | 21 | func (list *stringList) Add(value string) error { 22 | *list = append(*list, value) 23 | return nil 24 | } 25 | 26 | func (list *stringList) Len() int { 27 | return len(*list) 28 | } 29 | 30 | func (list *stringList) stringInList(a string) bool { 31 | for _, b := range *list { 32 | if b == a { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | // graphData translate bench results to Google graph JSON structure 9 | func graphData(benchResults BenchNameSet, oBenchNames, oBenchArgs stringList) []byte { 10 | data := new(bytes.Buffer) 11 | 12 | data.WriteString("[") 13 | sep := "" 14 | data.WriteString("[\"Argument\"") 15 | for _, oName := range oBenchNames { 16 | sep = "," 17 | data.WriteString(fmt.Sprintf("%s\"%s\"", sep, oName)) 18 | } 19 | data.WriteString("]") 20 | 21 | lsep := "" 22 | for _, oArg := range oBenchArgs { 23 | lsep = "," 24 | data.WriteString(fmt.Sprintf("%s[\"%s\"", lsep, oArg)) 25 | sep := "" 26 | for _, oName := range oBenchNames { 27 | sep = "," 28 | data.WriteString(fmt.Sprintf("%s%.2f", sep, benchResults[oName][oArg])) 29 | } 30 | data.WriteString("]") 31 | } 32 | data.WriteString("]") 33 | 34 | return data.Bytes() 35 | } 36 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/tools/benchmark/parse" 7 | ) 8 | 9 | var bTests = []struct { 10 | line string // input 11 | name string // expected result 12 | arg string 13 | nsperop float64 14 | }{ 15 | {"BenchmarkF2_F0000000-4 50000000 29.4 ns/op", "F2", "F0000000", 29.4}, 16 | {"BenchmarkF0_FF-2 10000000 37.4 ns/op", "F0", "FF", 37.4}, 17 | {"BenchmarkF_0-2 40000000 11.2 ns/op", "F", "0", 11.2}, 18 | } 19 | 20 | func TestParser(t *testing.T) { 21 | for _, tt := range bTests { 22 | b, _ := parse.ParseLine(tt.line) 23 | name, arg, _, _ := parseNameArgThread(b.Name) 24 | if name != tt.name { 25 | t.Errorf("parseNameArgThread(%s): expected %s, actual %s", b.Name, tt.name, name) 26 | } 27 | if arg != tt.arg { 28 | t.Errorf("parseNameArgThread(%s): expected %s, actual %s", b.Name, tt.arg, arg) 29 | } 30 | if b.NsPerOp != tt.nsperop { 31 | t.Errorf("parseNameArgThread(%s): expected %f, actual %f", b.Name, tt.nsperop, b.NsPerOp) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | // Coder should use following naming convention for Benchmark functions 10 | // Naming convention: Benchmark[Function_name]_[Function_argument](b *testing.B) 11 | var re *regexp.Regexp = regexp.MustCompile(`Benchmark([a-zA-Z0-9]+)_([_a-zA-Z0-9]+)-([0-9]+)$`) 12 | 13 | // Storage for Func(Arg)=Result relations 14 | type BenchArgSet map[string]float64 15 | type BenchNameSet map[string]BenchArgSet 16 | 17 | // parseNameArgThread parses function name, argument and number of threads from benchmark output. 18 | func parseNameArgThread(line string) (name string, arg string, c int, err error) { 19 | 20 | arr := re.FindStringSubmatch(line) 21 | 22 | // we expect 4 columns 23 | if len(arr) != 4 { 24 | return "", "", 0, errors.New("Can't parse benchmark result") 25 | } 26 | 27 | name, arg = arr[1], arr[2] 28 | 29 | c, err = strconv.Atoi(arr[3]) 30 | if err != nil { 31 | return "", "", 0, errors.New("Can't parse benchmark result") 32 | } 33 | 34 | return name, arg, c, nil 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | 13 | "github.com/fatih/color" 14 | "golang.org/x/tools/benchmark/parse" 15 | ) 16 | 17 | // uploadData sends data to server and expects graph url. 18 | func uploadData(apiUrl, data, title string) (string, error) { 19 | 20 | resp, err := http.PostForm(apiUrl, url.Values{"data": {data}, "title": {title}}) 21 | if err != nil { 22 | return "", err 23 | } 24 | defer resp.Body.Close() 25 | 26 | body, err := ioutil.ReadAll(resp.Body) 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | if resp.StatusCode != 200 { 32 | return "", errors.New("Server din't return graph URL") 33 | } 34 | 35 | return string(body), nil 36 | } 37 | 38 | func main() { 39 | 40 | var oBenchNames, oBenchArgs stringList 41 | 42 | // graph elements will be ordered as in benchmark output by default - unless the order was specified here 43 | flag.Var(&oBenchNames, "obn", "comma-separated list of benchmark names") 44 | flag.Var(&oBenchArgs, "oba", "comma-separated list of benchmark arguments") 45 | title := flag.String("title", "Graph: Benchmark results in ns/op", "title of a graph") 46 | apiUrl := flag.String("apiurl", "http://benchgraph.codingberg.com", "url to server api") 47 | flag.Parse() 48 | 49 | var skipBenchNamesParsing, skipBenchArgsParsing bool 50 | 51 | if oBenchNames.Len() > 0 { 52 | skipBenchNamesParsing = true 53 | } 54 | if oBenchArgs.Len() > 0 { 55 | skipBenchArgsParsing = true 56 | } 57 | 58 | benchResults := make(BenchNameSet) 59 | 60 | // parse Golang benchmark results, line by line 61 | scan := bufio.NewScanner(os.Stdin) 62 | green := color.New(color.FgGreen).SprintfFunc() 63 | red := color.New(color.FgRed).SprintFunc() 64 | for scan.Scan() { 65 | line := scan.Text() 66 | 67 | mark := green("√") 68 | 69 | b, err := parse.ParseLine(line) 70 | if err != nil { 71 | mark = red("?") 72 | } 73 | 74 | // read bench name and arguments 75 | if b != nil { 76 | name, arg, _, err := parseNameArgThread(b.Name) 77 | if err != nil { 78 | mark = red("!") 79 | fmt.Printf("%s %s\n", mark, line) 80 | continue 81 | } 82 | 83 | if !skipBenchNamesParsing && !oBenchNames.stringInList(name) { 84 | oBenchNames.Add(name) 85 | } 86 | 87 | if !skipBenchArgsParsing && !oBenchArgs.stringInList(arg) { 88 | oBenchArgs.Add(arg) 89 | } 90 | 91 | if _, ok := benchResults[name]; !ok { 92 | benchResults[name] = make(BenchArgSet) 93 | } 94 | 95 | benchResults[name][arg] = b.NsPerOp 96 | } 97 | 98 | fmt.Printf("%s %s\n", mark, line) 99 | } 100 | 101 | if err := scan.Err(); err != nil { 102 | fmt.Fprintf(os.Stderr, "reading standard input: %v", err) 103 | os.Exit(1) 104 | } 105 | 106 | if len(benchResults) == 0 { 107 | fmt.Fprintf(os.Stderr, "no data to show.\n\n") 108 | os.Exit(1) 109 | } 110 | 111 | fmt.Println() 112 | fmt.Println("Waiting for server response ...") 113 | 114 | data := graphData(benchResults, oBenchNames, oBenchArgs) 115 | 116 | graphUrl, err := uploadData(*apiUrl, string(data), *title) 117 | if err != nil { 118 | fmt.Fprintf(os.Stderr, "uploading data: %v", err) 119 | os.Exit(1) 120 | } 121 | 122 | fmt.Println("=========================================") 123 | fmt.Println() 124 | fmt.Println(graphUrl) 125 | fmt.Println() 126 | fmt.Println("=========================================") 127 | 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # benchgraph 2 | Visualization of Golang benchmark output using Google charts 3 | 4 | ## Introduction 5 | In Golang we can analyze algorithm efficiency by writing benchmark functions and looking at execution time in ns/op. This task might become significantly hindered by increasing number of benchmark tests. One way to handle this is to visualize multiple benchmark results and track the function curve on a graph. The `benchgraph` reads benchmark output lines, prepare data for the graph, and upload data to remote server, which enables online view and html embedding. Graph turns out to be very handy in case of many algorithms that are tested against many arguments, especially if you are studing internal algorithm design. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | git clone https://github.com/CodingBerg/benchgraph.git 11 | cd ./benchmark 12 | go get ./ 13 | go install 14 | ``` 15 | 16 | ## Naming convention 17 | In order for `benchgraph` to work a coder is required to follow the **naming convention** when coding benchmark functions: 18 | ```go 19 | // Naming convention 20 | func Benchmark[Function_name]_[Function_argument](b *testing.B){ 21 | ... 22 | } 23 | ``` 24 | For example, if we take one line from the benchmark output, 25 | ```bash 26 | BenchmarkF1_F-4 30000000 53.7 ns/op 27 | ``` 28 | it will be parsed and plotted on graph as function `F1(F)=53.7`, taking `F` as an argument and `53.7` as function result. 29 | In short, X-axis shows function arguments, while Y-axis shows function execution time in ns/op. 30 | 31 | ## Usage 32 | The output of benchmark is piped through `benchgraph`: 33 | 34 | ```bash 35 | go test -bench .|benchgraph -title="Graph: F(x) in ns/op" 36 | testing: warning: no tests to run 37 | ? PASS 38 | √ BenchmarkF1_F-4 30000000 53.7 ns/op 39 | √ BenchmarkF1_FF-4 20000000 62.9 ns/op 40 | √ BenchmarkF1_FFF-4 20000000 70.0 ns/op 41 | √ BenchmarkF1_FFFF-4 20000000 80.3 ns/op 42 | √ BenchmarkF1_FFFFF-4 20000000 90.8 ns/op 43 | √ BenchmarkF1_FFFFFF-4 20000000 99.5 ns/op 44 | ... 45 | Waiting for server response ... 46 | ========================================= 47 | 48 | http://benchgraph.codingberg.com/1 49 | 50 | ========================================= 51 | ``` 52 | 53 | In front of every line `benchgraph` places indicator whether line is parsed correctly, or not. 54 | When you see red marks `-` or `?`, it means, either you do not follow the **naming convention** from above, or the line doesn't contain benchmark test at all. At the end, `benchgraph` returns URL to the graph. From there, follow instructions how to embed graph into custom HTML page. Also, you can just share the graph link. 55 | 56 | ## Help 57 | 58 | ```bash 59 | benchgraph -help 60 | Usage of benchgraph: 61 | -apiurl string 62 | url to server api (default "http://benchgraph.codingberg.com") 63 | -oba value 64 | comma-separated list of benchmark arguments (default []) 65 | -obn value 66 | comma-separated list of benchmark names (default []) 67 | -title string 68 | title of a graph (default "Graph: Benchmark results in ns/op") 69 | ``` 70 | 71 | You can filter out which functions and against which arguments you want to display on graph by passing `-obn` and `-oba` arguments. This can be very handy in case when performing many benchmark tests. 72 | 73 | ```bash 74 | go test -bench .|benchgraph -title="Graph1: Benchmark F(x) in ns/op" -obn="F2,F3,F4" -oba="F,FF,FFF,FFFF,FFFFF,FFFFFF,FFFFFFF,FFFFFFFF" 75 | ``` 76 | 77 | ## Hints on productivity 78 | 79 | You can first save benchmark output and then use it later for drawing graphs. This is very handy if your benchmark tests take some time to complete. 80 | 81 | ```bash 82 | go test -bench . > out 83 | 84 | cat out|benchgraph -title="Graph1: Benchmark F(x) in ns/op" -obn="F2,F3,F4" -oba="F,FF,FFF,FFFF,FFFFF,FFFFFF,FFFFFFF,FFFFFFFF" 85 | cat out|benchgraph -title="Graph2: Benchmark F(x) in ns/op" -obn="F2,F3,F4" -oba="0F,F0,F00,F000,F0000,F00000,F000000,F0000000" 86 | ``` 87 | 88 | ## Online Demo 89 | 90 | Here we analyze efficiency of different algorithms for computing parity of uint64 numbers: 91 | 92 | http://codingberg.com/golang/interview/compute_parity_of_64_bit_unsigned_integer 93 | 94 | There are two graphs embedded into page behind above link: 95 | 96 | http://benchgraph.codingberg.com/1 97 | 98 | http://benchgraph.codingberg.com/2 99 | 100 | *Both above links can be also shared without emebeding into HTML page.* 101 | 102 | --------------------------------------------------------------------------------