github.com/jgbaldwinbrown/perf@v0.1.1/benchunit/scale.go (about)

     1  // Copyright 2022 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 benchunit
     6  
     7  import (
     8  	"fmt"
     9  	"math"
    10  	"strconv"
    11  )
    12  
    13  // A Scaler represents a scaling factor for a number and
    14  // its scientific representation.
    15  type Scaler struct {
    16  	Prec   int     // Digits after the decimal point
    17  	Factor float64 // Unscaled value of 1 Prefix (e.g., 1 k => 1000)
    18  	Prefix string  // Unit prefix ("k", "M", "Ki", etc)
    19  }
    20  
    21  // Format formats val and appends the unit prefix according to the given scale.
    22  // For example, if the Scaler has class Decimal, Format(123456789)
    23  // returns "123.4M".
    24  //
    25  // If the value has units, be sure to tidy it first (see Tidy).
    26  // Otherwise, this can result in nonsense units when the result is
    27  // displayed with its units; for example, representing 123456789 ns as
    28  // 123.4M ns ("megananoseconds", aka milliseconds).
    29  func (s Scaler) Format(val float64) string {
    30  	buf := make([]byte, 0, 20)
    31  	buf = strconv.AppendFloat(buf, val/s.Factor, 'f', s.Prec, 64)
    32  	buf = append(buf, s.Prefix...)
    33  	return string(buf)
    34  }
    35  
    36  // NoOpScaler is a Scaler that formats numbers with the smallest
    37  // number of digits necessary to capture the exact value, and no
    38  // prefix. This is intended for when the output will be consumed by
    39  // another program, such as when producing CSV format.
    40  var NoOpScaler = Scaler{-1, 1, ""}
    41  
    42  type factor struct {
    43  	factor float64
    44  	prefix string
    45  	// Thresholds for 100.0, 10.00, 1.000.
    46  	t100, t10, t1 float64
    47  }
    48  
    49  var siFactors = mkSIFactors()
    50  var iecFactors = mkIECFactors()
    51  var sigfigs, sigfigsBase = mkSigfigs()
    52  
    53  func mkSIFactors() []factor {
    54  	// To ensure that the thresholds for printing values with
    55  	// various factors exactly match how printing itself will
    56  	// round, we construct the thresholds by parsing the printed
    57  	// representation.
    58  	var factors []factor
    59  	exp := 12
    60  	for _, p := range []string{"T", "G", "M", "k", "", "m", "ยต", "n"} {
    61  		t100, _ := strconv.ParseFloat(fmt.Sprintf("99.995e%d", exp), 64)
    62  		t10, _ := strconv.ParseFloat(fmt.Sprintf("9.9995e%d", exp), 64)
    63  		t1, _ := strconv.ParseFloat(fmt.Sprintf(".99995e%d", exp), 64)
    64  		factors = append(factors, factor{math.Pow(10, float64(exp)), p, t100, t10, t1})
    65  		exp -= 3
    66  	}
    67  	return factors
    68  }
    69  
    70  func mkIECFactors() []factor {
    71  	var factors []factor
    72  	exp := 40
    73  	// ISO/IEC 80000 doesn't specify fractional prefixes. They're
    74  	// meaningful for things like "B/sec", but "1 /KiB/s" looks a
    75  	// lot more confusing than bottoming out at "B" and printing
    76  	// with more precision, such as "0.001 B/s".
    77  	//
    78  	// Because t1 of one factor represents 1024 of the next
    79  	// smaller factor, we'll render values in the range [1000,
    80  	// 1024) using the smaller factor. E.g., 1020 * 1024 will be
    81  	// rendered as 1020 KiB, not 0.996 MiB. This is intentional as
    82  	// it achieves higher precision, avoids rendering scaled
    83  	// values below 1, and seems generally less confusing for
    84  	// binary factors.
    85  	for _, p := range []string{"Ti", "Gi", "Mi", "Ki", ""} {
    86  		t100, _ := strconv.ParseFloat(fmt.Sprintf("0x1.8ffae147ae148p%d", 6+exp), 64) // 99.995
    87  		t10, _ := strconv.ParseFloat(fmt.Sprintf("0x1.3ffbe76c8b439p%d", 3+exp), 64)  // 9.9995
    88  		t1, _ := strconv.ParseFloat(fmt.Sprintf("0x1.fff972474538fp%d", -1+exp), 64)  // .99995
    89  		factors = append(factors, factor{math.Pow(2, float64(exp)), p, t100, t10, t1})
    90  		exp -= 10
    91  	}
    92  	return factors
    93  }
    94  
    95  func mkSigfigs() ([]float64, int) {
    96  	var sigfigs []float64
    97  	// Print up to 10 digits after the decimal place.
    98  	for exp := -1; exp > -9; exp-- {
    99  		thresh, _ := strconv.ParseFloat(fmt.Sprintf("9.9995e%d", exp), 64)
   100  		sigfigs = append(sigfigs, thresh)
   101  	}
   102  	// sigfigs[0] is the threshold for 3 digits after the decimal.
   103  	return sigfigs, 3
   104  }
   105  
   106  // Scale formats val using at least three significant digits,
   107  // appending an SI or binary prefix. See Scaler.Format for details.
   108  func Scale(val float64, cls Class) string {
   109  	return CommonScale([]float64{val}, cls).Format(val)
   110  }
   111  
   112  // CommonScale returns a common Scaler to apply to all values in vals.
   113  // This scale will show at least three significant digits for every
   114  // value.
   115  func CommonScale(vals []float64, cls Class) Scaler {
   116  	// The common scale is determined by the non-zero value
   117  	// closest to zero.
   118  	var min float64
   119  	for _, v := range vals {
   120  		v = math.Abs(v)
   121  		if v != 0 && (min == 0 || v < min) {
   122  			min = v
   123  		}
   124  	}
   125  	if min == 0 {
   126  		return Scaler{3, 1, ""}
   127  	}
   128  
   129  	var factors []factor
   130  	switch cls {
   131  	default:
   132  		panic(fmt.Sprintf("bad Class %v", cls))
   133  	case Decimal:
   134  		factors = siFactors
   135  	case Binary:
   136  		factors = iecFactors
   137  	}
   138  
   139  	for _, factor := range factors {
   140  		switch {
   141  		case min >= factor.t100:
   142  			return Scaler{1, factor.factor, factor.prefix}
   143  		case min >= factor.t10:
   144  			return Scaler{2, factor.factor, factor.prefix}
   145  		case min >= factor.t1:
   146  			return Scaler{3, factor.factor, factor.prefix}
   147  		}
   148  	}
   149  
   150  	// The value is less than the smallest factor. Print it using
   151  	// the smallest factor and more precision to achieve the
   152  	// desired sigfigs.
   153  	factor := factors[len(factors)-1]
   154  	val := min / factor.factor
   155  	for i, thresh := range sigfigs {
   156  		if val >= thresh || i == len(sigfigs)-1 {
   157  			return Scaler{i + sigfigsBase, factor.factor, factor.prefix}
   158  		}
   159  	}
   160  
   161  	panic("not reachable")
   162  }