| // Copyright (c) 2025 The BoringSSL 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. | |
| //go:build ignore | |
| // analyze_bssl_speed analyzes the JSON-formatted output of bssl speed | |
| // and derives performance components in ns/op and ns/KiB by linear regression. | |
| package main | |
| import ( | |
| "encoding/json" | |
| "flag" | |
| "fmt" | |
| "io" | |
| "log" | |
| "maps" | |
| "math" | |
| "os" | |
| "regexp" | |
| "slices" | |
| "strings" | |
| "time" | |
| ) | |
| var ( | |
| cpuTime = flag.Bool("cpu_time", false, "use CPU time, not wall time, for benchmark evaluation") | |
| ) | |
| type googlebenchmark struct { | |
| Name string `json:"name"` | |
| RealTime float64 `json:"real_time"` | |
| CPUTime float64 `json:"cpu_time"` | |
| TimeUnit string `json:"time_unit"` | |
| } | |
| type googlebenchmarks struct { | |
| Benchmarks []googlebenchmark `json:"benchmarks"` | |
| } | |
| type record struct { | |
| name string | |
| timePerCall float64 // time.Duration but unrounded. | |
| bytesPerCall int | |
| } | |
| type benchmarkGroup []record | |
| type groupedBenchmarks map[string]benchmarkGroup | |
| var removeRE = regexp.MustCompile(`/InputSize:(\d+)$`) | |
| func (b googlebenchmarks) group() groupedBenchmarks { | |
| g := groupedBenchmarks{} | |
| for _, b := range b.Benchmarks { | |
| match := removeRE.FindStringSubmatchIndex(b.Name) | |
| if match == nil { | |
| log.Printf("Skipping unsized benchmark: %v.", b.Name) | |
| continue | |
| } | |
| name := b.Name[:match[0]] | |
| var bytes int | |
| _, err := fmt.Sscanf(b.Name[match[2]:match[3]], "%d", &bytes) | |
| if err != nil { | |
| log.Printf("Could not get input size for benchmark: %v.", b.Name) | |
| continue | |
| } | |
| var unit time.Duration | |
| switch b.TimeUnit { | |
| case "ns": | |
| unit = time.Nanosecond | |
| case "us": | |
| unit = time.Microsecond | |
| case "ms": | |
| unit = time.Millisecond | |
| default: | |
| log.Printf("Skipping benchmark with unsupported time unit %q: %v.", b.TimeUnit, b.Name) | |
| continue | |
| } | |
| var count float64 | |
| if *cpuTime { | |
| count = b.CPUTime | |
| } else { | |
| count = b.RealTime | |
| } | |
| r := record{ | |
| name: b.Name, | |
| timePerCall: count * float64(unit), | |
| bytesPerCall: bytes, | |
| } | |
| g[name] = append(g[name], r) | |
| } | |
| return g | |
| } | |
| type regression struct { | |
| dataPoints int | |
| constant float64 | |
| perKiB float64 | |
| coefficient float64 | |
| err error | |
| } | |
| // String returns a nice string representation of this. | |
| func (r regression) String() string { | |
| if r.err != nil { | |
| return r.err.Error() | |
| } | |
| return fmt.Sprintf("%v + n * %v/KiB (n=%v, r=%v)", | |
| time.Duration(r.constant+0.5), | |
| time.Duration(r.perKiB+0.5), | |
| r.dataPoints, | |
| r.coefficient) | |
| } | |
| type regressions map[string]regression | |
| func (g benchmarkGroup) regress() regression { | |
| if len(g) < 2 { | |
| return regression{ | |
| err: fmt.Errorf("not enough datapoints: got %v, want >= 2", len(g)), | |
| } | |
| } | |
| var sx, sxx, sxy, sy, syy float64 | |
| for _, r := range g { | |
| x := float64(r.bytesPerCall) | |
| y := r.timePerCall | |
| sx += x | |
| sxx += x * x | |
| sxy += x * y | |
| sy += y | |
| syy += y * y | |
| } | |
| n := float64(len(g)) | |
| d1 := n*sxx - sx*sx | |
| if d1 == 0 { | |
| return regression{ | |
| err: fmt.Errorf("all x values are equal: %v", sy/n), | |
| } | |
| } | |
| n1 := n*sxy - sx*sy | |
| m := n1 / d1 | |
| b := (sy*sxx - sx*sxy) / d1 | |
| r := 1.0 | |
| d2 := n*syy - sy*sy | |
| if d2 != 0 { | |
| r = n1 / math.Sqrt(d1*d2) | |
| } | |
| return regression{ | |
| dataPoints: len(g), | |
| constant: b, | |
| perKiB: m * 1024, | |
| coefficient: r, | |
| } | |
| } | |
| func (b groupedBenchmarks) regress() regressions { | |
| r := regressions{} | |
| for d, g := range b { | |
| r[d] = g.regress() | |
| } | |
| return r | |
| } | |
| // String returns a nice string representation of this. | |
| func (r regressions) String() string { | |
| var ret []string | |
| for _, k := range slices.Sorted(maps.Keys(r)) { | |
| ret = append(ret, fmt.Sprintf("%v: %v", k, r[k])) | |
| } | |
| return strings.Join(ret, "\n") | |
| } | |
| func main() { | |
| flag.Parse() | |
| j := json.NewDecoder(os.Stdin) | |
| var g googlebenchmarks | |
| for { | |
| var gnew googlebenchmarks | |
| err := j.Decode(&gnew) | |
| if err == io.EOF { | |
| break | |
| } | |
| if err != nil { | |
| log.Panicf("failed decoding: %v", err) | |
| } | |
| gnew.Benchmarks = append(g.Benchmarks, gnew.Benchmarks...) | |
| g = gnew | |
| } | |
| fmt.Printf("%v\n", g.group().regress()) | |
| } |