-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Winston Ho
committed
Sep 24, 2022
0 parents
commit 04babf7
Showing
5 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Sokutei | ||
|
||
A command-line benchmarking tool written in Go. | ||
|
||
# Installation | ||
|
||
```sh | ||
go install github.com/violentestpen/sokutei@latest | ||
``` | ||
|
||
# Inspired by | ||
|
||
- [Hyperfine](https://github.com/sharkdp/hyperfine) | ||
- [bench](https://github.com/Gabriella439/bench) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module github.com/violenttestpen/sokutei | ||
|
||
go 1.16 | ||
|
||
require ( | ||
github.com/briandowns/spinner v1.19.0 // indirect | ||
github.com/fatih/color v1.13.0 | ||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E= | ||
github.com/briandowns/spinner v1.19.0/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= | ||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= | ||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= | ||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= | ||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= | ||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= | ||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= | ||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | ||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= | ||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= | ||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= | ||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"flag" | ||
"fmt" | ||
"math" | ||
"os/exec" | ||
"runtime" | ||
"sort" | ||
"time" | ||
|
||
"github.com/fatih/color" | ||
) | ||
|
||
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes?redirectedfrom=MSDN | ||
|
||
const ( | ||
progressDoneRune = "█" | ||
progressPendingRune = "▒" | ||
) | ||
|
||
var ( | ||
noShell bool | ||
runs int64 | ||
shell string | ||
warmup int64 | ||
|
||
setupCmd string | ||
|
||
noColor bool | ||
) | ||
|
||
type benchmarkResult struct { | ||
cmd string | ||
|
||
mean int64 | ||
stdev float64 | ||
min int64 | ||
max int64 | ||
|
||
denominator float64 | ||
unit string | ||
} | ||
|
||
func init() { | ||
switch runtime.GOOS { | ||
case "windows": | ||
shell = "cmd.exe" | ||
default: | ||
shell = "/bin/sh" | ||
} | ||
} | ||
|
||
func runSetup(ctx context.Context, cmdToSetup string) error { | ||
cmdParts := list2Cmdline(cmdToSetup) | ||
if len(cmdParts) == 0 { | ||
return errors.New("empty command string") | ||
} | ||
return exec.CommandContext(ctx, cmdParts[0], cmdParts[1:]...).Run() | ||
} | ||
|
||
func runBenchmark(ctx context.Context, cmdToBenchmark string) (*benchmarkResult, error) { | ||
cmdParts := list2Cmdline(cmdToBenchmark) | ||
if len(cmdParts) == 0 { | ||
return nil, errors.New("empty command string") | ||
} | ||
|
||
fmt.Print("Performing warmup runs") | ||
for i := int64(0); i < warmup; i++ { | ||
cmd := exec.CommandContext(ctx, cmdParts[0], cmdParts[1:]...) | ||
if err := cmd.Run(); err != nil { | ||
return nil, err | ||
} | ||
} | ||
clearCurrentTerminalLine(color.Output) | ||
|
||
var currentEstimate int64 | ||
var currentDenominator float64 | ||
var currentUnitEstimate string | ||
elapsedRuns := make([]int64, runs) | ||
|
||
fmt.Print("Initial time measurement") | ||
for i := int64(0); i < runs; i++ { | ||
cmd := exec.CommandContext(ctx, cmdParts[0], cmdParts[1:]...) | ||
|
||
startTime := time.Now() | ||
err := cmd.Run() | ||
elapsedRuns[i] = int64(time.Since(startTime)) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Calculate current estimate and ETA | ||
currentEstimate = (currentEstimate*i + elapsedRuns[i]) / (i + 1) | ||
currentDenominator, currentUnitEstimate = getMeasurementMetrics(currentEstimate) | ||
etaEstimate := time.Duration(currentEstimate * (runs - i)) | ||
|
||
clearCurrentTerminalLine(color.Output) | ||
line := fmt.Sprintf("Current estimate: %s ", | ||
color.GreenString("%.2f %s", float64(currentEstimate)/currentDenominator, currentUnitEstimate)) | ||
printProgressLine(line, float64(i+1)/float64(runs), etaEstimate) | ||
} | ||
clearCurrentTerminalLine(color.Output) | ||
|
||
// Calculate mean, min and max timings | ||
minElapsed := int64(math.MaxInt64) | ||
maxElapsed := int64(math.MinInt64) | ||
var totalElapsed int64 | ||
for _, elapsed := range elapsedRuns { | ||
totalElapsed += int64(elapsed) | ||
if elapsed < minElapsed { | ||
minElapsed = elapsed | ||
} | ||
if elapsed > maxElapsed { | ||
maxElapsed = elapsed | ||
} | ||
} | ||
|
||
result := &benchmarkResult{ | ||
cmd: cmdToBenchmark, | ||
mean: totalElapsed / runs, | ||
min: minElapsed, | ||
max: maxElapsed, | ||
} | ||
|
||
// Calculate standard deviation and get appropriate meansurement unit | ||
result.stdev = stdev(elapsedRuns, result.mean) | ||
result.denominator, result.unit = getMeasurementMetrics(result.mean) | ||
return result, nil | ||
} | ||
|
||
func main() { | ||
flag.BoolVar(&noShell, "N", false, "Run benchmarks without an intermediate shell") | ||
flag.Int64Var(&runs, "runs", 10, "Number of rounds to warmup") | ||
flag.Int64Var(&warmup, "warmup", 0, "Number of rounds to warmup") | ||
flag.StringVar(&setupCmd, "setup", "", "Command to run before all benchmarks") | ||
flag.StringVar(&shell, "S", shell, "The intermediate shell to run benchmarks in") | ||
flag.BoolVar(&noColor, "no-color", false, "Disable coloured output") | ||
flag.Parse() | ||
cmds := flag.Args() | ||
|
||
color.NoColor = noColor | ||
ctx := context.Background() | ||
|
||
if setupCmd != "" { | ||
if err := runSetup(ctx, setupCmd); err != nil { | ||
fmt.Println("An error occurred during setup:", err) | ||
return | ||
} | ||
} | ||
|
||
if !noShell && shell != "" { | ||
// Measuring shell spawning time | ||
} | ||
|
||
results := make([]*benchmarkResult, 0, len(cmds)) | ||
for i, cmd := range cmds { | ||
fmt.Printf("Benchmark #%d: %s\n", i+1, cmd) | ||
result, err := runBenchmark(ctx, cmd) | ||
if err != nil { | ||
fmt.Println("An error occurred during benchmark:", err) | ||
} else { | ||
fmt.Fprintf(color.Output, " Time (%s ± %s):\t%s ± %s\n", | ||
color.GreenString("mean"), | ||
color.GreenString("σ"), | ||
color.GreenString("%.2f %s", float64(result.mean)/result.denominator, result.unit), | ||
color.GreenString("%.2f %s", result.stdev/result.denominator, result.unit)) | ||
fmt.Fprintf(color.Output, " Range (%s … %s):\t%s … %s\t%s\n", | ||
color.CyanString("min"), | ||
color.RedString("max"), | ||
color.CyanString("%.2f %s", float64(result.min)/result.denominator, result.unit), | ||
color.RedString("%.2f %s", float64(result.max)/result.denominator, result.unit), | ||
color.HiBlackString("%d runs", runs)) | ||
fmt.Println() | ||
|
||
results = append(results, result) | ||
} | ||
} | ||
|
||
if len(results) > 1 { | ||
fmt.Println("Summary") | ||
|
||
sort.SliceStable(results, func(i, j int) bool { return results[i].mean < results[j].mean }) | ||
var fastestResult *benchmarkResult | ||
for _, result := range results { | ||
if fastestResult == nil { | ||
fastestResult = result | ||
fmt.Fprintf(color.Output, " '%s' ran\n", color.CyanString(fastestResult.cmd)) | ||
} else { | ||
meanMultiplier := float64(result.mean) / float64(fastestResult.mean) | ||
posStdevMultiplier := (float64(result.mean)+result.stdev)/(float64(fastestResult.mean)+fastestResult.stdev) - meanMultiplier | ||
negStdevMultiplier := meanMultiplier - (float64(result.mean)-result.stdev)/(float64(fastestResult.mean)-fastestResult.stdev) | ||
fmt.Fprintf(color.Output, " %s ± %s times faster than '%s'\n", | ||
color.GreenString("%.2f", meanMultiplier), | ||
color.GreenString("%.2f", math.Abs(posStdevMultiplier)+math.Abs(negStdevMultiplier)), | ||
color.RedString(result.cmd)) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"math" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
"github.com/fatih/color" | ||
"golang.org/x/term" | ||
) | ||
|
||
var denominators = []int64{int64(time.Hour), int64(time.Minute), int64(time.Second), int64(time.Millisecond), int64(time.Microsecond), int64(time.Nanosecond)} | ||
var units = []string{"h", "m", "s", "ms", "µs", "ns"} | ||
|
||
// Translate a sequence of arguments into a command line string, using the same rules as the MS C runtime: | ||
// 1) Arguments are delimited by white space, which is either a space or a tab. | ||
// 2) A string surrounded by double quotation marks is interpreted as a single argument, | ||
// regardless of white space contained within. A quoted string can be embedded in an argument. | ||
// 3) A double quotation mark preceded by a backslash is interpreted as a literal double quotation mark. | ||
// 4) Backslashes are interpreted literally, unless they immediately precede a double quotation mark. | ||
// 5) If backslashes immediately precede a double quotation mark, | ||
// every pair of backslashes is interpreted as a literal backslash. | ||
// If the number of backslashes is odd, the last backslash escapes the next double quotation mark as described in rule 3. | ||
func list2Cmdline(cmd string) []string { | ||
var cmdParts []string | ||
var inQuote rune | ||
|
||
var b strings.Builder | ||
for i, ch := range cmd { | ||
if (ch == '"' || ch == '\'') && (i == 0 || cmd[i-1] != '\\') { | ||
switch inQuote { | ||
case rune(0): | ||
inQuote = ch | ||
case ch: | ||
inQuote = rune(0) | ||
default: | ||
b.WriteRune(ch) | ||
} | ||
} else if (ch == ' ' || ch == '\t') && inQuote == 0 { | ||
cmdParts = append(cmdParts, b.String()) | ||
b.Reset() | ||
} else { | ||
b.WriteRune(ch) | ||
} | ||
} | ||
cmdParts = append(cmdParts, b.String()) | ||
return cmdParts | ||
} | ||
|
||
func getMeasurementMetrics(timing int64) (float64, string) { | ||
for i, denominator := range denominators { | ||
if timing/int64(denominator) > 0 { | ||
return float64(denominator), units[i] | ||
} | ||
} | ||
return 0, "" | ||
} | ||
|
||
func stdev(values []int64, mean int64) float64 { | ||
var numerator int64 | ||
for _, value := range values { | ||
delta := value - mean | ||
numerator += delta * delta | ||
} | ||
return math.Sqrt(float64(numerator) / float64(len(values)-1)) | ||
} | ||
|
||
func clearCurrentTerminalLine(w io.Writer) { | ||
w.Write([]byte("\r\033[K")) | ||
} | ||
|
||
func printProgressLine(line string, progress float64, eta time.Duration) { | ||
// Calculate progress bar | ||
terminalWidth, _, _ := term.GetSize(int(os.Stdout.Fd())) | ||
terminalWidth -= len(line) + 2 + 12 | ||
progressChunks := int(progress * float64(terminalWidth)) | ||
progressLine := strings.Repeat(progressDoneRune, progressChunks) | ||
progressLine += strings.Repeat(progressPendingRune, terminalWidth-progressChunks) | ||
|
||
fmt.Fprintf(color.Output, "%s %s ETA %02d:%02d:%02d", line, progressLine, | ||
int64(eta.Hours()), int64(eta.Minutes()), int64(eta.Seconds())) | ||
} |