github.com/wI2L/jettison@v0.7.5-0.20230106001914-c70014c6417a/tools/benchparse/benchparse.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"flag"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  )
    13  
    14  var (
    15  	omitMem       = flag.Bool("omit-mem", false, "omit B/op stat")
    16  	omitAllocs    = flag.Bool("omit-allocs", false, "omit allocs/op stat")
    17  	omitBandwidth = flag.Bool("omit-bandwidth", false, "omit MB/s stat")
    18  	flagInput     = flag.String("in", "benchstats.csv", "csv-formatted benchstat file")
    19  	flagOutput    = flag.String("out", "benchstats.json", "json-formatted data table file")
    20  )
    21  
    22  func usage() {
    23  	fmt.Fprintf(os.Stderr, "usage: benchparse [options]\n")
    24  	fmt.Fprintf(os.Stderr, "options:\n")
    25  	flag.PrintDefaults()
    26  	os.Exit(2)
    27  }
    28  
    29  type set map[string][]benchStat
    30  
    31  type benchStat struct {
    32  	Name  string
    33  	Value float64
    34  	Unit  string
    35  }
    36  
    37  type gcDataTables map[string][][]interface{}
    38  
    39  func main() {
    40  	log.SetPrefix("benchparse: ")
    41  	log.SetFlags(0)
    42  	flag.Usage = usage
    43  	flag.Parse()
    44  
    45  	file, err := os.Open(*flagInput)
    46  	if err != nil {
    47  		log.Fatal(err)
    48  	}
    49  	defer file.Close()
    50  
    51  	stats, err := parse(bufio.NewScanner(file))
    52  	if err != nil {
    53  		log.Fatal(err)
    54  	}
    55  	out, err := os.OpenFile(*flagOutput, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
    56  	if err != nil {
    57  		log.Fatal(err)
    58  	}
    59  	defer out.Close()
    60  
    61  	data := transform(stats)
    62  
    63  	enc := json.NewEncoder(out)
    64  	if err := enc.Encode(data); err != nil {
    65  		log.Fatal(err)
    66  	}
    67  	if err := out.Sync(); err != nil {
    68  		log.Fatal(err)
    69  	}
    70  	os.Exit(0)
    71  }
    72  
    73  func parse(scanr *bufio.Scanner) (set, error) {
    74  	var (
    75  		i     int
    76  		unit  string
    77  		stats = make(set)
    78  	)
    79  	// The first line is always the header of the
    80  	// first section. All the next section headers
    81  	// will be detected thanks to the empty line
    82  	// that precede them.
    83  	nextIsHdr := true
    84  
    85  	for ; scanr.Scan(); i++ {
    86  		line := scanr.Text()
    87  
    88  		if len(line) == 0 {
    89  			nextIsHdr = true
    90  			continue
    91  		}
    92  		r := strings.Split(line, ",")
    93  
    94  		if nextIsHdr {
    95  			if len(r) != 2 {
    96  				return nil, fmt.Errorf("invalid header at line %d", i)
    97  			}
    98  			s := strings.Split(r[1], " ")[1]
    99  			unit = strings.Trim(s, "()")
   100  			nextIsHdr = false
   101  			continue
   102  		}
   103  		idx := strings.Index(r[0], "/")
   104  		if idx == -1 {
   105  			return nil, fmt.Errorf("invalid run name at line %d: no separator", i)
   106  		}
   107  		bname, rname := r[0][:idx], r[0][idx+1:]
   108  		if idx := strings.LastIndexByte(rname, '-'); idx != -1 {
   109  			rname = rname[:idx]
   110  		}
   111  		if _, ok := stats[bname]; !ok {
   112  			stats[bname] = nil
   113  		}
   114  		if len(r) <= 1 {
   115  			continue
   116  		}
   117  		f, err := strconv.ParseFloat(r[1], 64)
   118  		if err != nil {
   119  			return nil, fmt.Errorf("invalid float value at line %d: %s", i, err)
   120  		}
   121  		stats[bname] = append(stats[bname], benchStat{
   122  			Name:  rname,
   123  			Value: f,
   124  			Unit:  unit,
   125  		})
   126  	}
   127  	if err := scanr.Err(); err != nil {
   128  		return nil, err
   129  	}
   130  	return stats, nil
   131  }
   132  
   133  // transform converts a stats set to a 2D data-table
   134  // interpretable by the Google Charts API.
   135  func transform(stats set) gcDataTables {
   136  	data := make(gcDataTables)
   137  
   138  	for bname, stats := range stats {
   139  		values := make(map[string][]interface{})
   140  	L:
   141  		for _, s := range stats {
   142  			if _, ok := values[s.Name]; !ok {
   143  				values[s.Name] = append(values[s.Name], s.Name)
   144  			}
   145  			switch s.Unit {
   146  			case "MB/s":
   147  				if *omitBandwidth {
   148  					continue L
   149  				}
   150  			case "B/op": // total memory allocated
   151  				if *omitMem {
   152  					continue L
   153  				}
   154  			case "allocs/op": // number of allocs
   155  				if *omitAllocs {
   156  					continue L
   157  				}
   158  			}
   159  			values[s.Name] = append(values[s.Name], s.Value)
   160  		}
   161  		data[bname] = append(data[bname], []interface{}{"Name", "ns/op"})
   162  		if !*omitBandwidth {
   163  			data[bname][0] = append(data[bname][0], "MB/s")
   164  		}
   165  		if !*omitMem {
   166  			data[bname][0] = append(data[bname][0], "B/op")
   167  		}
   168  		if !*omitAllocs {
   169  			data[bname][0] = append(data[bname][0], "allocs/op")
   170  		}
   171  		for _, v := range values {
   172  			data[bname] = append(data[bname], v)
   173  		}
   174  	}
   175  	return data
   176  }