benchstat: handle user-ns/op units

compilebench emits both ns/op and user-ns/op measurements.
This CL improves benchstat's handling of such measurements.
Migrated from https://github.com/rsc/benchstat/pull/5.

Sample output, before:

name       old user-ns/op  new user-ns/op  delta
Template        279M ± 7%       273M ± 4%     ~     (p=0.329 n=6+5)
Unicode         148M ±11%       131M ± 7%  -11.49%  (p=0.030 n=6+5)
GoTypes         830M ± 2%       835M ± 3%     ~     (p=0.792 n=6+5)
SSA            9.60G ± 3%      9.64G ± 2%     ~     (p=0.792 n=6+5)
Flate           158M ± 3%       155M ± 1%     ~     (p=0.329 n=6+5)
GoParser        204M ± 6%       202M ± 8%     ~     (p=0.792 n=6+5)
Reflect         511M ± 9%       531M ± 6%     ~     (p=0.177 n=6+5)
Tar             151M ± 3%       151M ± 5%     ~     (p=0.931 n=6+5)
XML             271M ± 3%       268M ± 3%     ~     (p=0.429 n=6+5)

Sample output, after:

name       old user-time/op  new user-time/op  delta
Template         279ms ± 7%        273ms ± 4%     ~     (p=0.329 n=6+5)
Unicode          148ms ±11%        131ms ± 7%  -11.49%  (p=0.030 n=6+5)
GoTypes          830ms ± 2%        835ms ± 3%     ~     (p=0.792 n=6+5)
SSA              9.60s ± 3%        9.64s ± 2%     ~     (p=0.792 n=6+5)
Flate            158ms ± 3%        155ms ± 1%     ~     (p=0.329 n=6+5)
GoParser         204ms ± 6%        202ms ± 8%     ~     (p=0.792 n=6+5)
Reflect          511ms ± 9%        531ms ± 6%     ~     (p=0.177 n=6+5)
Tar              151ms ± 3%        151ms ± 5%     ~     (p=0.931 n=6+5)
XML              271ms ± 3%        268ms ± 3%     ~     (p=0.429 n=6+5)

Change-Id: I716b4e89a28adc72e9135b8c580434f748a9c0a9
Reviewed-on: https://go-review.googlesource.com/39794
Run-TryBot: Josh Bleecher Snyder <josharian@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Quentin Smith <quentin@golang.org>
diff --git a/benchstat/scaler.go b/benchstat/scaler.go
index c775caa..7b652f1 100644
--- a/benchstat/scaler.go
+++ b/benchstat/scaler.go
@@ -4,7 +4,10 @@
 
 package benchstat
 
-import "fmt"
+import (
+	"fmt"
+	"strings"
+)
 
 // A Scaler is a function that scales and formats a measurement.
 // All measurements within a given table row are formatted
@@ -15,7 +18,7 @@
 // NewScaler returns a Scaler appropriate for formatting
 // the measurement val, which has the given unit.
 func NewScaler(val float64, unit string) Scaler {
-	if unit == "ns/op" {
+	if hasBaseUnit(unit, "ns/op") || hasBaseUnit(unit, "ns/GC") {
 		return timeScaler(val)
 	}
 
@@ -24,7 +27,7 @@
 	var suffix string
 
 	prescale := 1.0
-	if unit == "MB/s" {
+	if hasBaseUnit(unit, "MB/s") {
 		prescale = 1e6
 	}
 
@@ -61,10 +64,10 @@
 		format, scale, suffix = "%.2f", 1, ""
 	}
 
-	if unit == "B/op" {
+	if hasBaseUnit(unit, "B/op") || hasBaseUnit(unit, "bytes/op") || hasBaseUnit(unit, "bytes") {
 		suffix += "B"
 	}
-	if unit == "MB/s" {
+	if hasBaseUnit(unit, "MB/s") {
 		suffix += "B/s"
 	}
 	scale /= prescale
@@ -107,3 +110,9 @@
 		return fmt.Sprintf(format, ns/1e9*scale)
 	}
 }
+
+// hasBaseUnit reports whether s has unit unit.
+// For now, it reports whether s == unit or s ends in -unit.
+func hasBaseUnit(s, unit string) bool {
+	return s == unit || strings.HasSuffix(s, "-"+unit)
+}
diff --git a/benchstat/table.go b/benchstat/table.go
index 9693389..ef938d1 100644
--- a/benchstat/table.go
+++ b/benchstat/table.go
@@ -6,6 +6,7 @@
 
 import (
 	"fmt"
+	"strings"
 
 	"golang.org/x/perf/internal/stats"
 )
@@ -126,18 +127,25 @@
 	return tables
 }
 
+var metricSuffix = map[string]string{
+	"ns/op": "time/op",
+	"ns/GC": "time/GC",
+	"B/op":  "alloc/op",
+	"MB/s":  "speed",
+}
+
 // metricOf returns the name of the metric with the given unit.
 func metricOf(unit string) string {
-	switch unit {
-	case "ns/op":
-		return "time/op"
-	case "B/op":
-		return "alloc/op"
-	case "MB/s":
-		return "speed"
-	default:
-		return unit
+	if s := metricSuffix[unit]; s != "" {
+		return s
 	}
+	for s, suff := range metricSuffix {
+		if dashs := "-" + s; strings.HasSuffix(unit, dashs) {
+			prefix := strings.TrimSuffix(unit, dashs)
+			return prefix + "-" + suff
+		}
+	}
+	return unit
 }
 
 // addGeomean adds a "geomean" row to the table,
diff --git a/cmd/benchstat/main_test.go b/cmd/benchstat/main_test.go
index 26f5433..4653ae0 100644
--- a/cmd/benchstat/main_test.go
+++ b/cmd/benchstat/main_test.go
@@ -38,6 +38,7 @@
 	check(t, "oldnewttest", "-delta-test=ttest", "old.txt", "new.txt")
 	check(t, "packagesold", "packagesold.txt")
 	check(t, "packages", "packagesold.txt", "packagesnew.txt")
+	check(t, "units", "units-old.txt", "units-new.txt")
 }
 
 func check(t *testing.T, name string, files ...string) {
diff --git a/cmd/benchstat/testdata/units-new.txt b/cmd/benchstat/testdata/units-new.txt
new file mode 100644
index 0000000..397088d
--- /dev/null
+++ b/cmd/benchstat/testdata/units-new.txt
@@ -0,0 +1,8 @@
+pkg: synthetic
+note: test benchstat printing of units
+
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14100000000000 user-ns/op 5 ns/GC 12 quick-bytes
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14700000000000 user-ns/op 5 ns/GC 16 quick-bytes
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14800000000000 user-ns/op 5 ns/GC 12 quick-bytes
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14300000000000 user-ns/op 5 ns/GC 16 quick-bytes
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14000000000000 user-ns/op 5 ns/GC 12 quick-bytes
diff --git a/cmd/benchstat/testdata/units-old.txt b/cmd/benchstat/testdata/units-old.txt
new file mode 100644
index 0000000..6e87b07
--- /dev/null
+++ b/cmd/benchstat/testdata/units-old.txt
@@ -0,0 +1,8 @@
+pkg: synthetic
+note: test benchstat printing of units
+
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14400000000000 user-ns/op 5 ns/GC 12 quick-bytes
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14500000000000 user-ns/op 5 ns/GC 16 quick-bytes
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14600000000000 user-ns/op 5 ns/GC 12 quick-bytes
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14200000000000 user-ns/op 5 ns/GC 16 quick-bytes
+BenchmarkTwoHourMarathon 1 7200000000000 ns/op 14300000000000 user-ns/op 5 ns/GC 12 quick-bytes
diff --git a/cmd/benchstat/testdata/units.golden b/cmd/benchstat/testdata/units.golden
new file mode 100644
index 0000000..3a5ca67
--- /dev/null
+++ b/cmd/benchstat/testdata/units.golden
@@ -0,0 +1,11 @@
+name             old time/op       new time/op       delta
+TwoHourMarathon        7200s ± 0%        7200s ± 0%   ~     (all equal)
+
+name             old user-time/op  new user-time/op  delta
+TwoHourMarathon       14400s ± 1%       14380s ± 3%   ~     (p=0.881 n=5+5)
+
+name             old time/GC       new time/GC       delta
+TwoHourMarathon       5.00ns ± 0%       5.00ns ± 0%   ~     (all equal)
+
+name             old quick-bytes   new quick-bytes   delta
+TwoHourMarathon        13.6B ±18%        13.6B ±18%   ~     (p=1.000 n=5+5)