github.com/jgbaldwinbrown/perf@v0.1.1/benchstat/text.go (about)

     1  // Copyright 2017 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package benchstat
     6  
     7  import (
     8  	"encoding/csv"
     9  	"fmt"
    10  	"io"
    11  	"path/filepath"
    12  	"strings"
    13  	"unicode/utf8"
    14  )
    15  
    16  // FormatText appends a fixed-width text formatting of the tables to w.
    17  func FormatText(w io.Writer, tables []*Table) {
    18  	var textTables [][]*textRow
    19  	for _, t := range tables {
    20  		textTables = append(textTables, toText(t))
    21  	}
    22  
    23  	var max []int
    24  	for _, table := range textTables {
    25  		for _, row := range table {
    26  			if len(row.cols) == 1 {
    27  				// Header row
    28  				continue
    29  			}
    30  			for len(max) < len(row.cols) {
    31  				max = append(max, 0)
    32  			}
    33  			for i, s := range row.cols {
    34  				n := utf8.RuneCountInString(s)
    35  				if max[i] < n {
    36  					max[i] = n
    37  				}
    38  			}
    39  		}
    40  	}
    41  
    42  	for i, table := range textTables {
    43  		if i > 0 {
    44  			fmt.Fprintf(w, "\n")
    45  		}
    46  
    47  		// headings
    48  		row := table[0]
    49  		for i, s := range row.cols {
    50  			switch i {
    51  			case 0:
    52  				fmt.Fprintf(w, "%-*s", max[i], s)
    53  			default:
    54  				fmt.Fprintf(w, "  %-*s", max[i], s)
    55  			case len(row.cols) - 1:
    56  				fmt.Fprintf(w, "  %s\n", s)
    57  			}
    58  		}
    59  
    60  		// data
    61  		for _, row := range table[1:] {
    62  			for i, s := range row.cols {
    63  				switch {
    64  				case len(row.cols) == 1:
    65  					// Header row
    66  					fmt.Fprint(w, s)
    67  				case i == 0:
    68  					fmt.Fprintf(w, "%-*s", max[i], s)
    69  				default:
    70  					if i == len(row.cols)-1 && len(s) > 0 && s[0] == '(' {
    71  						// Left-align p value.
    72  						fmt.Fprintf(w, "  %s", s)
    73  						break
    74  					}
    75  					fmt.Fprintf(w, "  %*s", max[i], s)
    76  				}
    77  			}
    78  			fmt.Fprintf(w, "\n")
    79  		}
    80  	}
    81  }
    82  
    83  func must(e error) {
    84  	if e != nil {
    85  		panic(e)
    86  	}
    87  }
    88  
    89  // FormatCSV appends a CSV formatting of the tables to w.
    90  // norange suppresses the range columns.
    91  func FormatCSV(w io.Writer, tables []*Table, norange bool) {
    92  	var textTables [][]*textRow
    93  	for _, t := range tables {
    94  		textTables = append(textTables, toCSV(t, norange))
    95  	}
    96  
    97  	for i, table := range textTables {
    98  		if i > 0 {
    99  			fmt.Fprintf(w, "\n")
   100  		}
   101  		csvw := csv.NewWriter(w)
   102  
   103  		// headings
   104  		row := table[0]
   105  		must(csvw.Write(row.cols))
   106  
   107  		// data
   108  		for _, row := range table[1:] {
   109  			must(csvw.Write(row.cols))
   110  		}
   111  		csvw.Flush()
   112  	}
   113  }
   114  
   115  // trimCommonPathPrefix returns a string slice with common
   116  // path-separator-terminated prefixes removed.  Empty strings
   117  // are ignored and a singleton non-empty string is left unchanged
   118  func trimCommonPathPrefix(ss []string) []string {
   119  	commonPrefixLen := -1
   120  	commonPrefix := ""
   121  	trimCommonPrefix := false // true if more than one data row w/ non-empty title
   122  
   123  	// begin finding common byte prefix (could end on partial rune)
   124  	for _, s := range ss {
   125  		if s == "" {
   126  			continue
   127  		}
   128  		if commonPrefixLen == -1 {
   129  			commonPrefix = s
   130  			commonPrefixLen = len(s)
   131  			continue
   132  		}
   133  		trimCommonPrefix = true // More than one not-empty
   134  		if commonPrefixLen > len(s) {
   135  			commonPrefixLen = len(s)
   136  			commonPrefix = commonPrefix[0:commonPrefixLen]
   137  		}
   138  		for j := 0; j < commonPrefixLen; j++ {
   139  			if commonPrefix[j] != s[j] {
   140  				commonPrefixLen = j
   141  				commonPrefix = commonPrefix[0:commonPrefixLen]
   142  				break
   143  			}
   144  		}
   145  	}
   146  	if !trimCommonPrefix || commonPrefixLen == 0 {
   147  		return ss
   148  	}
   149  	// trim to include last path separator.
   150  	commonPrefixLen = 1 + strings.LastIndex(commonPrefix, string(filepath.Separator))
   151  	if commonPrefixLen == 0 {
   152  		// No separator found means commonPrefixLen = 1 + (-1) == 0
   153  		return ss
   154  	}
   155  
   156  	rs := []string{}
   157  	for _, s := range ss {
   158  		if s == "" {
   159  			rs = append(rs, "")
   160  		} else {
   161  			rs = append(rs, s[commonPrefixLen:])
   162  		}
   163  	}
   164  	return rs
   165  }
   166  
   167  // A textRow is a row of printed text columns.
   168  type textRow struct {
   169  	cols []string
   170  }
   171  
   172  func newTextRow(cols ...string) *textRow {
   173  	return &textRow{cols: cols}
   174  }
   175  
   176  // newTextRowDelta returns a labeled row of text, with "±" inserted after
   177  // each member of "cols" unless norange is true.
   178  func newTextRowDelta(norange bool, label string, cols ...string) *textRow {
   179  	newcols := []string{}
   180  	newcols = append(newcols, label)
   181  	for _, s := range cols {
   182  		newcols = append(newcols, s)
   183  		if !norange {
   184  			newcols = append(newcols, "±")
   185  		}
   186  	}
   187  	return &textRow{cols: newcols}
   188  }
   189  
   190  func (r *textRow) add(col string) {
   191  	r.cols = append(r.cols, col)
   192  }
   193  
   194  func (r *textRow) trim() {
   195  	for len(r.cols) > 0 && r.cols[len(r.cols)-1] == "" {
   196  		r.cols = r.cols[:len(r.cols)-1]
   197  	}
   198  }
   199  
   200  // toText converts the Table to a textual grid of cells,
   201  // which can then be printed in fixed-width output.
   202  func toText(t *Table) []*textRow {
   203  	var textRows []*textRow
   204  	switch len(t.Configs) {
   205  	case 1:
   206  		textRows = append(textRows, newTextRow("name", t.Metric))
   207  	case 2:
   208  		textRows = append(textRows, newTextRow("name", "old "+t.Metric, "new "+t.Metric, "delta"))
   209  	default:
   210  		row := newTextRow("name \\ " + t.Metric)
   211  		row.cols = append(row.cols, t.Configs...) // TODO Should this also trim common path prefix? (see toCSV)
   212  		textRows = append(textRows, row)
   213  	}
   214  
   215  	var group string
   216  
   217  	for _, row := range t.Rows {
   218  		if row.Group != group {
   219  			group = row.Group
   220  			textRows = append(textRows, newTextRow(group))
   221  		}
   222  		text := newTextRow(row.Benchmark)
   223  		for _, m := range row.Metrics {
   224  			text.cols = append(text.cols, m.Format(row.Scaler))
   225  		}
   226  		if len(t.Configs) == 2 {
   227  			delta := row.Delta
   228  			if delta == "~" {
   229  				delta = "~   "
   230  			}
   231  			text.cols = append(text.cols, delta)
   232  			text.cols = append(text.cols, row.Note)
   233  		}
   234  		textRows = append(textRows, text)
   235  	}
   236  	for _, r := range textRows {
   237  		r.trim()
   238  	}
   239  	return textRows
   240  }
   241  
   242  // toCSV converts the Table to a slice of rows containing CSV-separated data
   243  // If norange is true, suppress the range information for each data item.
   244  // If norange is false, insert a "±" in the appropriate columns of the header row.
   245  func toCSV(t *Table, norange bool) []*textRow {
   246  	var textRows []*textRow
   247  	units := ""
   248  	if len(t.Rows) > 0 && len(t.Rows[0].Metrics) > 0 {
   249  		units = " (" + t.Rows[0].Metrics[0].Unit + ")"
   250  	}
   251  	switch len(t.Configs) {
   252  	case 1:
   253  		textRows = append(textRows, newTextRowDelta(norange, "name", t.Metric+units))
   254  	case 2:
   255  		textRows = append(textRows, newTextRowDelta(norange, "name", "old "+t.Metric+units, "new "+t.Metric+units, "delta"))
   256  	default:
   257  		rowname := "name \\ " + t.Metric
   258  		row := newTextRowDelta(norange, rowname+units, trimCommonPathPrefix(t.Configs)...)
   259  		textRows = append(textRows, row)
   260  	}
   261  
   262  	var group string
   263  
   264  	for _, row := range t.Rows {
   265  		if row.Group != group {
   266  			group = row.Group
   267  			textRows = append(textRows, newTextRow(group))
   268  		}
   269  		text := newTextRow(row.Benchmark)
   270  		for _, m := range row.Metrics {
   271  			mean := fmt.Sprintf("%.5E", m.Mean)
   272  			diff := m.FormatDiff()
   273  			if m.Unit == "" {
   274  				mean = ""
   275  				diff = ""
   276  			}
   277  			text.cols = append(text.cols, mean)
   278  			if !norange {
   279  				text.cols = append(text.cols, diff)
   280  			}
   281  		}
   282  		if len(t.Configs) == 2 {
   283  			delta := row.Delta
   284  			text.cols = append(text.cols, delta)
   285  			text.cols = append(text.cols, row.Note)
   286  		}
   287  		textRows = append(textRows, text)
   288  	}
   289  	for _, r := range textRows {
   290  		r.trim()
   291  	}
   292  	return textRows
   293  }