blob: 3cf5b5d9cf5fe74474f6c3591090691b705b2471 [file] [log] [blame]
// 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())
}