| // Copyright 2022 The Tint Authors | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //     https://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 |  | 
 | // Package bench provides types and methods for parsing Google benchmark results. | 
 | package bench | 
 |  | 
 | import ( | 
 | 	"encoding/json" | 
 | 	"errors" | 
 | 	"fmt" | 
 | 	"regexp" | 
 | 	"sort" | 
 | 	"strconv" | 
 | 	"strings" | 
 | 	"time" | 
 | 	"unicode/utf8" | 
 | ) | 
 |  | 
 | // Run holds all the benchmark results for a run, along with the context | 
 | // information for the run. | 
 | type Run struct { | 
 | 	Benchmarks []Benchmark | 
 | 	Context    *Context | 
 | } | 
 |  | 
 | // Context provides information about the environment used to perform the | 
 | // benchmark. | 
 | type Context struct { | 
 | 	Date              time.Time | 
 | 	HostName          string | 
 | 	Executable        string | 
 | 	NumCPUs           int | 
 | 	MhzPerCPU         int | 
 | 	CPUScalingEnabled bool | 
 | 	Caches            []ContextCache | 
 | 	LoadAvg           []float32 | 
 | 	LibraryBuildType  string | 
 | } | 
 |  | 
 | // ContextCache holds information about one of the system caches. | 
 | type ContextCache struct { | 
 | 	Type       string | 
 | 	Level      int | 
 | 	Size       int | 
 | 	NumSharing int | 
 | } | 
 |  | 
 | // Benchmark holds the results of a single benchmark test. | 
 | type Benchmark struct { | 
 | 	Name          string | 
 | 	Duration      time.Duration | 
 | 	AggregateType AggregateType | 
 | } | 
 |  | 
 | // AggregateType is an enumerator of benchmark aggregate types. | 
 | type AggregateType string | 
 |  | 
 | // Enumerator values of AggregateType | 
 | const ( | 
 | 	NonAggregate AggregateType = "NonAggregate" | 
 | 	Mean         AggregateType = "mean" | 
 | 	Median       AggregateType = "median" | 
 | 	Stddev       AggregateType = "stddev" | 
 | ) | 
 |  | 
 | // Parse parses the benchmark results from the string s. | 
 | // Parse will handle the json and 'console' formats. | 
 | func Parse(s string) (Run, error) { | 
 | 	type Parser = func(s string) (Run, error) | 
 | 	for _, parser := range []Parser{parseConsole, parseJSON} { | 
 | 		r, err := parser(s) | 
 | 		switch err { | 
 | 		case nil: | 
 | 			return r, nil | 
 | 		case errWrongFormat: | 
 | 		default: | 
 | 			return Run{}, err | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return Run{}, errors.New("Unrecognised file format") | 
 | } | 
 |  | 
 | var errWrongFormat = errors.New("Wrong format") | 
 | var consoleLineRE = regexp.MustCompile(`([\w/:]+)\s+([0-9]+(?:.[0-9]+)?) ns\s+[0-9]+(?:.[0-9]+) ns\s+([0-9]+)`) | 
 |  | 
 | func parseConsole(s string) (Run, error) { | 
 | 	blocks := strings.Split(s, "------------------------------------------------------------------------------------------") | 
 | 	if len(blocks) != 3 { | 
 | 		return Run{}, errWrongFormat | 
 | 	} | 
 |  | 
 | 	lines := strings.Split(blocks[2], "\n") | 
 | 	b := make([]Benchmark, 0, len(lines)) | 
 |  | 
 | 	for _, line := range lines { | 
 | 		if len(line) == 0 { | 
 | 			continue | 
 | 		} | 
 | 		matches := consoleLineRE.FindStringSubmatch(line) | 
 | 		if len(matches) != 4 { | 
 | 			return Run{}, fmt.Errorf("Unable to parse the line:\n" + line) | 
 | 		} | 
 | 		ns, err := strconv.ParseFloat(matches[2], 64) | 
 | 		if err != nil { | 
 | 			return Run{}, fmt.Errorf("Unable to parse the duration: " + matches[2]) | 
 | 		} | 
 |  | 
 | 		b = append(b, Benchmark{ | 
 | 			Name:     trimAggregateSuffix(matches[1]), | 
 | 			Duration: time.Nanosecond * time.Duration(ns), | 
 | 		}) | 
 | 	} | 
 | 	return Run{Benchmarks: b}, nil | 
 | } | 
 |  | 
 | func parseJSON(s string) (Run, error) { | 
 | 	type Data struct { | 
 | 		Context struct { | 
 | 			Date              time.Time `json:"date"` | 
 | 			HostName          string    `json:"host_name"` | 
 | 			Executable        string    `json:"executable"` | 
 | 			NumCPUs           int       `json:"num_cpus"` | 
 | 			MhzPerCPU         int       `json:"mhz_per_cpu"` | 
 | 			CPUScalingEnabled bool      `json:"cpu_scaling_enabled"` | 
 | 			LoadAvg           []float32 `json:"load_avg"` | 
 | 			LibraryBuildType  string    `json:"library_build_type"` | 
 | 			Caches            []struct { | 
 | 				Type       string `json:"type"` | 
 | 				Level      int    `json:"level"` | 
 | 				Size       int    `json:"size"` | 
 | 				NumSharing int    `json:"num_sharing"` | 
 | 			} `json:"caches"` | 
 | 		} `json:"context"` | 
 | 		Benchmarks []struct { | 
 | 			Name          string        `json:"name"` | 
 | 			Time          float64       `json:"real_time"` | 
 | 			AggregateType AggregateType `json:"aggregate_name"` | 
 | 		} `json:"benchmarks"` | 
 | 	} | 
 | 	data := Data{} | 
 | 	d := json.NewDecoder(strings.NewReader(s)) | 
 | 	if err := d.Decode(&data); err != nil { | 
 | 		return Run{}, err | 
 | 	} | 
 |  | 
 | 	out := Run{ | 
 | 		Benchmarks: make([]Benchmark, len(data.Benchmarks)), | 
 | 		Context: &Context{ | 
 | 			Date:              data.Context.Date, | 
 | 			HostName:          data.Context.HostName, | 
 | 			Executable:        data.Context.Executable, | 
 | 			NumCPUs:           data.Context.NumCPUs, | 
 | 			MhzPerCPU:         data.Context.MhzPerCPU, | 
 | 			CPUScalingEnabled: data.Context.CPUScalingEnabled, | 
 | 			LoadAvg:           data.Context.LoadAvg, | 
 | 			LibraryBuildType:  data.Context.LibraryBuildType, | 
 | 			Caches:            make([]ContextCache, len(data.Context.Caches)), | 
 | 		}, | 
 | 	} | 
 | 	for i, c := range data.Context.Caches { | 
 | 		out.Context.Caches[i] = ContextCache{ | 
 | 			Type:       c.Type, | 
 | 			Level:      c.Level, | 
 | 			Size:       c.Size, | 
 | 			NumSharing: c.NumSharing, | 
 | 		} | 
 | 	} | 
 | 	for i, b := range data.Benchmarks { | 
 | 		out.Benchmarks[i] = Benchmark{ | 
 | 			Name:          trimAggregateSuffix(b.Name), | 
 | 			Duration:      time.Nanosecond * time.Duration(int64(b.Time)), | 
 | 			AggregateType: b.AggregateType, | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return out, nil | 
 | } | 
 |  | 
 | // Diff describes the difference between two benchmarks | 
 | type Diff struct { | 
 | 	TestName           string | 
 | 	Delta              time.Duration // Δ (A → B) | 
 | 	PercentChangeAB    float64       // % (A → B) | 
 | 	PercentChangeBA    float64       // % (A → B) | 
 | 	MultiplierChangeAB float64       // × (A → B) | 
 | 	MultiplierChangeBA float64       // × (A → B) | 
 | 	TimeA              time.Duration // A | 
 | 	TimeB              time.Duration // B | 
 | } | 
 |  | 
 | // Diffs is a list of Diff | 
 | type Diffs []Diff | 
 |  | 
 | // DiffFormat describes how a list of diffs should be formatted | 
 | type DiffFormat struct { | 
 | 	TestName           bool | 
 | 	Delta              bool | 
 | 	PercentChangeAB    bool | 
 | 	PercentChangeBA    bool | 
 | 	MultiplierChangeAB bool | 
 | 	MultiplierChangeBA bool | 
 | 	TimeA              bool | 
 | 	TimeB              bool | 
 | } | 
 |  | 
 | func (diffs Diffs) Format(f DiffFormat) string { | 
 | 	if len(diffs) == 0 { | 
 | 		return "<no changes>" | 
 | 	} | 
 |  | 
 | 	type row []string | 
 |  | 
 | 	header := row{} | 
 | 	if f.TestName { | 
 | 		header = append(header, "Test name") | 
 | 	} | 
 | 	if f.Delta { | 
 | 		header = append(header, "Δ (A → B)") | 
 | 	} | 
 | 	if f.PercentChangeAB { | 
 | 		header = append(header, "% (A → B)") | 
 | 	} | 
 | 	if f.PercentChangeBA { | 
 | 		header = append(header, "% (B → A)") | 
 | 	} | 
 | 	if f.MultiplierChangeAB { | 
 | 		header = append(header, "× (A → B)") | 
 | 	} | 
 | 	if f.MultiplierChangeBA { | 
 | 		header = append(header, "× (B → A)") | 
 | 	} | 
 | 	if f.TimeA { | 
 | 		header = append(header, "A") | 
 | 	} | 
 | 	if f.TimeB { | 
 | 		header = append(header, "B") | 
 | 	} | 
 | 	if len(header) == 0 { | 
 | 		return "" | 
 | 	} | 
 |  | 
 | 	columns := []row{} | 
 | 	for _, d := range diffs { | 
 | 		r := make(row, 0, len(header)) | 
 | 		if f.TestName { | 
 | 			r = append(r, d.TestName) | 
 | 		} | 
 | 		if f.Delta { | 
 | 			r = append(r, fmt.Sprintf("%v", d.Delta)) | 
 | 		} | 
 | 		if f.PercentChangeAB { | 
 | 			r = append(r, fmt.Sprintf("%+2.1f%%", d.PercentChangeAB)) | 
 | 		} | 
 | 		if f.PercentChangeBA { | 
 | 			r = append(r, fmt.Sprintf("%+2.1f%%", d.PercentChangeBA)) | 
 | 		} | 
 | 		if f.MultiplierChangeAB { | 
 | 			r = append(r, fmt.Sprintf("%+.4f", d.MultiplierChangeAB)) | 
 | 		} | 
 | 		if f.MultiplierChangeBA { | 
 | 			r = append(r, fmt.Sprintf("%+.4f", d.MultiplierChangeBA)) | 
 | 		} | 
 | 		if f.TimeA { | 
 | 			r = append(r, fmt.Sprintf("%v", d.TimeA)) | 
 | 		} | 
 | 		if f.TimeB { | 
 | 			r = append(r, fmt.Sprintf("%v", d.TimeB)) | 
 | 		} | 
 | 		columns = append(columns, r) | 
 | 	} | 
 |  | 
 | 	// measure | 
 | 	widths := make([]int, len(header)) | 
 | 	for i, h := range header { | 
 | 		widths[i] = utf8.RuneCountInString(h) | 
 | 	} | 
 | 	for _, row := range columns { | 
 | 		for i, cell := range row { | 
 | 			l := utf8.RuneCountInString(cell) | 
 | 			if widths[i] < l { | 
 | 				widths[i] = l | 
 | 			} | 
 | 		} | 
 | 	} | 
 |  | 
 | 	pad := func(s string, i int) string { | 
 | 		if n := i - utf8.RuneCountInString(s); n > 0 { | 
 | 			return s + strings.Repeat(" ", n) | 
 | 		} | 
 | 		return s | 
 | 	} | 
 |  | 
 | 	// Draw table | 
 | 	b := &strings.Builder{} | 
 |  | 
 | 	horizontal_bar := func() { | 
 | 		for i := range header { | 
 | 			fmt.Fprintf(b, "+%v", strings.Repeat("-", 2+widths[i])) | 
 | 		} | 
 | 		fmt.Fprintln(b, "+") | 
 | 	} | 
 |  | 
 | 	horizontal_bar() | 
 |  | 
 | 	for i, h := range header { | 
 | 		fmt.Fprintf(b, "| %v ", pad(h, widths[i])) | 
 | 	} | 
 | 	fmt.Fprintln(b, "|") | 
 |  | 
 | 	horizontal_bar() | 
 |  | 
 | 	for _, row := range columns { | 
 | 		for i, cell := range row { | 
 | 			fmt.Fprintf(b, "| %v ", pad(cell, widths[i])) | 
 | 		} | 
 | 		fmt.Fprintln(b, "|") | 
 | 	} | 
 |  | 
 | 	horizontal_bar() | 
 |  | 
 | 	return b.String() | 
 | } | 
 |  | 
 | // Compare returns a string describing differences in the two benchmarks | 
 | // Absolute benchmark differences less than minDiff are omitted | 
 | // Absolute relative differences between [1, 1+x] are omitted | 
 | func Compare(a, b []Benchmark, minDiff time.Duration, minRelDiff float64) Diffs { | 
 | 	type times struct { | 
 | 		a time.Duration | 
 | 		b time.Duration | 
 | 	} | 
 | 	byName := map[string]times{} | 
 | 	for _, test := range a { | 
 | 		byName[test.Name] = times{a: test.Duration} | 
 | 	} | 
 | 	for _, test := range b { | 
 | 		t := byName[test.Name] | 
 | 		t.b = test.Duration | 
 | 		byName[test.Name] = t | 
 | 	} | 
 |  | 
 | 	type delta struct { | 
 | 		name       string | 
 | 		times      times | 
 | 		relDiff    float64 | 
 | 		absRelDiff float64 | 
 | 	} | 
 | 	deltas := []delta{} | 
 | 	for name, times := range byName { | 
 | 		if times.a == 0 || times.b == 0 { | 
 | 			continue // Assuming test was missing from a or b | 
 | 		} | 
 | 		diff := times.b - times.a | 
 | 		absDiff := diff | 
 | 		if absDiff < 0 { | 
 | 			absDiff = -absDiff | 
 | 		} | 
 | 		if absDiff < minDiff { | 
 | 			continue | 
 | 		} | 
 |  | 
 | 		relDiff := float64(times.b) / float64(times.a) | 
 | 		absRelDiff := relDiff | 
 | 		if absRelDiff < 1 { | 
 | 			absRelDiff = 1.0 / absRelDiff | 
 | 		} | 
 | 		if absRelDiff < (1.0 + minRelDiff) { | 
 | 			continue | 
 | 		} | 
 |  | 
 | 		d := delta{ | 
 | 			name:       name, | 
 | 			times:      times, | 
 | 			relDiff:    relDiff, | 
 | 			absRelDiff: absRelDiff, | 
 | 		} | 
 | 		deltas = append(deltas, d) | 
 | 	} | 
 |  | 
 | 	sort.Slice(deltas, func(i, j int) bool { return deltas[j].relDiff < deltas[i].relDiff }) | 
 |  | 
 | 	out := make(Diffs, len(deltas)) | 
 |  | 
 | 	for i, delta := range deltas { | 
 | 		a2b := delta.times.b - delta.times.a | 
 | 		out[i] = Diff{ | 
 | 			TestName:           delta.name, | 
 | 			Delta:              a2b, | 
 | 			PercentChangeAB:    100 * float64(a2b) / float64(delta.times.a), | 
 | 			PercentChangeBA:    100 * float64(-a2b) / float64(delta.times.b), | 
 | 			MultiplierChangeAB: float64(delta.times.b) / float64(delta.times.a), | 
 | 			MultiplierChangeBA: float64(delta.times.a) / float64(delta.times.b), | 
 | 			TimeA:              delta.times.a, | 
 | 			TimeB:              delta.times.b, | 
 | 		} | 
 | 	} | 
 | 	return out | 
 | } | 
 |  | 
 | func trimAggregateSuffix(name string) string { | 
 | 	name = strings.TrimSuffix(name, "_stddev") | 
 | 	name = strings.TrimSuffix(name, "_mean") | 
 | 	name = strings.TrimSuffix(name, "_median") | 
 | 	return name | 
 | } |