From 5e3f72db32b4c0e5c8d6da2debcf9714987e44e5 Mon Sep 17 00:00:00 2001 From: Shravan Asati Date: Sat, 20 Jan 2024 16:56:43 +0530 Subject: [PATCH] wip export --- .gitignore | 3 +- internal/export.go | 135 ++++++++++++++++++++++++++++++--------------- internal/result.go | 34 ++++++++++-- internal/stats.go | 14 +++-- main.go | 39 +++++-------- 5 files changed, 147 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index 4582797..f8cc4e7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ bin/ atomic-summary* atomic -*.gif \ No newline at end of file +*.gif +test* \ No newline at end of file diff --git a/internal/export.go b/internal/export.go index 3d0c89a..210bdce 100644 --- a/internal/export.go +++ b/internal/export.go @@ -7,12 +7,11 @@ import ( "path/filepath" "slices" "strings" + "sync" "text/template" "time" ) -// todo in all exports, include individual run details - var summaryNoColor = ` Executed Command: {{ .Command }} Total runs: {{ .Runs }} @@ -53,64 +52,56 @@ func (result *PrintableResult) String() string { } // textify writes the benchmark summary of the Result struct to a text file. -func textify(r *PrintableResult) { - text := summaryNoColor - - tmpl, err := template.New("summary").Parse(text) - if err != nil { - panic(err) - } +func textify(results []*PrintableResult) { + // temporarily turn off colors so that [PrintableResult.String] used non-colored summary + origVal := NO_COLOR + NO_COLOR = true + defer func() { + NO_COLOR = origVal + }() f, ferr := os.Create("atomic-summary.txt") if ferr != nil { Log("red", "Failed to create the file.") } defer f.Close() - if terr := tmpl.Execute(f, r); terr != nil { - Log("red", "Failed to write to the file.") - } else { - absPath, err := filepath.Abs("atomic-summary.txt") - if err != nil { - Log("red", "unable to get the absolute path for text file: "+err.Error()) - } else { - Log("green", "Successfully wrote benchmark summary to `"+absPath+"`.") - } + + for _, r := range results { + f.WriteString(r.String() + "\n") } + absPath, err := filepath.Abs("atomic-summary.txt") + if err != nil { + Log("red", "unable to get the absolute path for text file: "+err.Error()) + return + } else { + Log("green", "Successfully wrote benchmark summary to `"+absPath+"`.") + } } -func markdownify(r *PrintableResult) { +func markdownify(results []*SpeedResult) { text := ` # atomic-summary -| Fields | Values | -| ----------- | ----------- | -| Executed Command | {{.Command}} | -| Total runs | {{.Runs}} | -| Average time taken | {{.Average}} ± {{ .StandardDeviation }} | -| Range | {{.Min}} ... {{ .Max }} | +| Command | Runs | Average | User | System | Min | Max | Relative | +| ------- | ---- | ------- | ---- | ------ | --- | --- | -------- | ` - tmpl, err := template.New("summary").Parse(text) - if err != nil { - panic(err) - } - f, ferr := os.Create("atomic-summary.md") if ferr != nil { Log("red", "Failed to create the file.") } defer f.Close() - if terr := tmpl.Execute(f, r); terr != nil { - Log("red", "Failed to write to the file.") - } else { - absPath, err := filepath.Abs("atomic-summary.md") - if err != nil { - Log("red", "unable to get the absolute path for markdown file: "+err.Error()) - } else { - Log("green", "Successfully wrote benchmark summary to `"+absPath+"`.") - } + + for _, r = range results { + text += fmt.Sprintf("") } + absPath, err := filepath.Abs("atomic-summary.md") + if err != nil { + Log("red", "unable to get the absolute path for markdown file: "+err.Error()) + } else { + Log("green", "Successfully wrote benchmark summary to `"+absPath+"`.") + } } // jsonify converts the Result struct to JSON. @@ -187,16 +178,72 @@ func (result *PrintableResult) Export(exportFormats string) { } -func VerifyExportFormats(formats string) error { - validFormats := []string{"csv", "md", "txt", "json"} +func VerifyExportFormats(formats string) ([]string, error) { + validFormats := []string{"csv", "markdown", "txt", "json"} formatList := strings.Split(strings.ToLower(formats), ",") for _, f := range formatList { if !slices.Contains(validFormats, f) { - return fmt.Errorf("invalid export format: %s", f) + return nil, fmt.Errorf("invalid export format: %s", f) } } - return nil + return formatList, nil } -func Export(formats string, unit time.Duration) { +func convertToTimeUnit(given float64, unit time.Duration) float64 { + // first get duration from microseconds + duration := DurationFromNumber(given, time.Microsecond) + switch unit { + case time.Nanosecond: + return float64(duration.Nanoseconds()) + case time.Microsecond: + return float64(duration.Microseconds()) + case time.Millisecond: + return float64(duration.Milliseconds()) + case time.Second: + return duration.Seconds() + case time.Minute: + return duration.Minutes() + case time.Hour: + return duration.Hours() + default: + panic("convertToTimeUnit: unknown time unit: " + unit.String()) + } +} + +func Export(formats []string, results []*SpeedResult, timeUnit time.Duration) { + // first convert all speed results to the given time unit + // except for microseconds, because that's what used internally + if timeUnit != time.Microsecond { + var wg sync.WaitGroup + for _, sr := range results { + wg.Add(1) + go func(sr *SpeedResult) { + sr.AverageElapsed = convertToTimeUnit(sr.AverageElapsed, timeUnit) + sr.AverageUser = convertToTimeUnit(sr.AverageUser, timeUnit) + sr.AverageSystem = convertToTimeUnit(sr.AverageSystem, timeUnit) + sr.StandardDeviation = convertToTimeUnit(sr.StandardDeviation, timeUnit) + sr.Max = convertToTimeUnit(sr.Max, timeUnit) + sr.Min = convertToTimeUnit(sr.Min, timeUnit) + for i, t := range sr.Times { + sr.Times[i] = convertToTimeUnit(t, timeUnit) + } + wg.Done() + }(sr) + } + wg.Wait() + } + + for _, format := range formats { + switch format { + case "json": + jsonify() + case "csv": + csvify() + case "markdown": + markdownify() + case "txt": + printables := MapFunc[[]*SpeedResult, []*PrintableResult](func(r *SpeedResult) *PrintableResult { return NewPrintableResult().FromSpeedResult(*r) }, results) + textify(printables) + } + } } diff --git a/internal/result.go b/internal/result.go index 299d0a1..7d5f68b 100644 --- a/internal/result.go +++ b/internal/result.go @@ -1,10 +1,19 @@ package internal -// Contains all the numerical quantities (in microseconds) for relative speed comparison. +import "time" + +// Contains all the numerical quantities (in microseconds) for relative speed comparison. Also used for export. type SpeedResult struct { Command string - Average float64 + AverageElapsed float64 + AverageUser float64 + AverageSystem float64 StandardDeviation float64 + Max float64 + Min float64 + Times []float64 + RelativeMean float64 + RelativeStddev float64 } // PrintableResult struct which is shown at the end as benchmarking summary and is written to a file. @@ -21,8 +30,25 @@ type PrintableResult struct { Max string } +func NewPrintableResult() *PrintableResult { + var pr PrintableResult + return &pr +} + +func (pr *PrintableResult) FromSpeedResult(sr SpeedResult) *PrintableResult { + pr.Command = sr.Command + pr.Runs = len(sr.Times) + pr.AverageElapsed = DurationFromNumber(sr.AverageElapsed, time.Microsecond).String() + pr.AverageUser = DurationFromNumber(sr.AverageUser, time.Microsecond).String() + pr.AverageSystem = DurationFromNumber(sr.AverageSystem, time.Microsecond).String() + pr.StandardDeviation = DurationFromNumber(sr.StandardDeviation, time.Microsecond).String() + pr.Max = DurationFromNumber(sr.Max, time.Microsecond).String() + pr.Min = DurationFromNumber(sr.Min, time.Microsecond).String() + return pr +} + // Implements [sort.Interface] for []Result based on the Average field. -type ByAverage []SpeedResult +type ByAverage []*SpeedResult func (a ByAverage) Len() int { return len(a) @@ -33,5 +59,5 @@ func (a ByAverage) Swap(i, j int) { } func (a ByAverage) Less(i, j int) bool { - return a[i].Average < a[j].Average + return a[i].AverageElapsed < a[j].AverageElapsed } diff --git a/internal/stats.go b/internal/stats.go index 260b5f5..c39849a 100644 --- a/internal/stats.go +++ b/internal/stats.go @@ -81,21 +81,25 @@ func TestOutliers(data []float64) bool { return (nOutliers / totalDataPoints * 100) > OUTLIER_THRESHOLD } -// todo represent this in a struct for export -func RelativeSummary(results []SpeedResult) { +// Prints the relative summary and also sets the RelativeMean and RelativeStddev of each [SpeedResult]. +func RelativeSummary(results []*SpeedResult) { if len(results) <= 1 { return } sort.Sort(ByAverage(results)) fastest := results[0] + fastest.RelativeMean = 1.00 + fastest.RelativeStddev = 0.00 colorstring.Println("[bold][white]Summary") colorstring.Printf(" [cyan]%s[reset] ran \n", fastest.Command) for _, r := range results[1:] { - ratio := r.Average / fastest.Average + ratio := r.AverageElapsed / fastest.AverageElapsed ratioStddev := ratio * math.Sqrt( - math.Pow(r.StandardDeviation/r.Average, 2)+ - math.Pow(fastest.StandardDeviation/fastest.Average, 2), + math.Pow(r.StandardDeviation/r.AverageElapsed, 2)+ + math.Pow(fastest.StandardDeviation/fastest.AverageElapsed, 2), ) + r.RelativeMean = ratio + r.RelativeMean = ratioStddev colorstring.Printf(" [green]%.2f[reset] ± [light_green]%.2f[reset] times faster than [magenta]%s \n", ratio, ratioStddev, r.Command) } } diff --git a/main.go b/main.go index e118968..023ade3 100644 --- a/main.go +++ b/main.go @@ -645,13 +645,13 @@ func main() { } // * getting export values - exportFormats, err := flags["export"].GetString() + exportFormatString, err := flags["export"].GetString() if err != nil { internal.Log("red", "Application error: cannot parse flag values.") return } - err = internal.VerifyExportFormats(exportFormats) - if err != nil && exportFormats != "none" { + exportFormats, err := internal.VerifyExportFormats(exportFormatString) + if err != nil && exportFormatString != "none" { internal.Log("red", err.Error()) return } @@ -695,7 +695,7 @@ func main() { } // fmt.Println(shellCalibration) - var speedResults []internal.SpeedResult + var speedResults []*internal.SpeedResult // * benchmark each command given givenCommands := strings.Split(args["commands"].Value, commando.VariadicSeparator) nCommands := len(givenCommands) @@ -756,29 +756,19 @@ func main() { continue } stddev := internal.CalculateStandardDeviation(elapsedTimes, avgElapsed) - avgElapsedDuration := internal.DurationFromNumber(avgElapsed, time.Microsecond) - avgUserDuration := internal.DurationFromNumber(avgUser, time.Microsecond) - avgSystemDuration := internal.DurationFromNumber(avgSystem, time.Microsecond) - stddevDuration := internal.DurationFromNumber(stddev, time.Microsecond) max_ := slices.Max(elapsedTimes) min_ := slices.Min(elapsedTimes) - maxDuration := internal.DurationFromNumber(max_, time.Microsecond) - minDuration := internal.DurationFromNumber(min_, time.Microsecond) - speedResult := internal.SpeedResult{ + speedResult := &internal.SpeedResult{ Command: commandString, - Average: avgElapsed, + AverageElapsed: avgElapsed, + AverageUser: avgUser, + AverageSystem: avgSystem, StandardDeviation: stddev, + Max: max_, + Min: min_, + Times: elapsedTimes, } - printableResult := internal.PrintableResult{ - Command: strings.Join(command, " "), - Runs: len(runsData), - AverageElapsed: avgElapsedDuration.String(), - AverageUser: avgUserDuration.String(), - AverageSystem: avgSystemDuration.String(), - StandardDeviation: stddevDuration.String(), - Max: maxDuration.String(), - Min: minDuration.String(), - } + printableResult := internal.NewPrintableResult().FromSpeedResult(*speedResult) speedResults = append(speedResults, speedResult) fmt.Print(printableResult.String()) @@ -811,8 +801,9 @@ func main() { internal.RelativeSummary(speedResults) - // todo export all results - internal.Export(exportFormats, timeUnit) + if exportFormatString != "none" { + internal.Export(exportFormats, speedResults, timeUnit) + } })