From 260d95c1ef9563733409873a38fbebd0a4f28352 Mon Sep 17 00:00:00 2001 From: Shravan Asati Date: Tue, 30 Jan 2024 16:21:32 +0530 Subject: [PATCH] distribute utils and add plotting helper --- internal/export.go | 50 ------------- internal/{utils.go => io_utils.go} | 58 ++------------- internal/plotter.go | 22 +++++- internal/time_utils.go | 110 +++++++++++++++++++++++++++++ main.go | 23 +++++- 5 files changed, 157 insertions(+), 106 deletions(-) rename internal/{utils.go => io_utils.go} (57%) create mode 100644 internal/time_utils.go diff --git a/internal/export.go b/internal/export.go index e7f5ef5..d1cd3db 100644 --- a/internal/export.go +++ b/internal/export.go @@ -7,7 +7,6 @@ import ( "path/filepath" "slices" "strings" - "sync" "text/template" "time" ) @@ -143,57 +142,8 @@ func VerifyExportFormats(formats string) ([]string, error) { return formatList, nil } -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.Nanoseconds()) / float64(1e6) - 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 addExtension(filename, ext string) string { - if !strings.HasSuffix(filename, "."+ext) { - return filename + "." + ext - } - return filename -} func Export(formats []string, filename 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": diff --git a/internal/utils.go b/internal/io_utils.go similarity index 57% rename from internal/utils.go rename to internal/io_utils.go index 42838fe..25c76c6 100644 --- a/internal/utils.go +++ b/internal/io_utils.go @@ -2,18 +2,13 @@ package internal import ( "bufio" - "errors" "fmt" - "math" "os" "os/user" "path/filepath" "strings" - "time" ) -var ErrInvalidTimeUnit = errors.New("invalid time unit") - // formats the text in a javascript like syntax. func format(text string, params map[string]string) string { for key, val := range params { @@ -63,11 +58,6 @@ func writeToFile(text, filename string) (err error) { return err } -func roundFloat(num float64, digits int) float64 { - tenMultiplier := math.Pow10(digits) - return math.Round(num*tenMultiplier) / tenMultiplier -} - func checkPathExists(fp string) bool { _, e := os.Stat(fp) return !os.IsNotExist(e) @@ -106,49 +96,9 @@ func readFile(file string) string { return text } -type numberLike interface { - ~int | ~float64 | ~int32 | ~int64 | ~float32 -} - -func DurationFromNumber[T numberLike](number T, unit time.Duration) time.Duration { - unitToSuffixMap := map[time.Duration]string{ - time.Nanosecond: "ns", - time.Microsecond: "us", - time.Millisecond: "ms", - time.Second: "s", - time.Minute: "m", - time.Hour: "h", - } - suffix, ok := unitToSuffixMap[unit] - if !ok { - // this function is only used internally, panic if unknown time unit is passed - panic("unknown time unit in DurationFromNumber: " + unit.String()) - } - numberFloat := roundFloat(float64(number), 2) - timeString := fmt.Sprintf("%.2f%v", numberFloat, suffix) - duration, err := time.ParseDuration(timeString) - if err != nil { - // again, function only used internally, invalid duration must not be present - panic("unable to parse duration: " + timeString + " in DurationFromNumber \n" + err.Error()) - } - return duration.Round(time.Microsecond) -} - -func ParseTimeUnit(unitString string) (time.Duration, error) { - switch strings.TrimSpace(strings.ToLower(unitString)) { - case "ns": - return time.Nanosecond, nil - case "us", "µs": - return time.Microsecond, nil - case "ms": - return time.Millisecond, nil - case "s": - return time.Second, nil - case "m": - return time.Minute, nil - case "h": - return time.Hour, nil - default: - return 0, ErrInvalidTimeUnit +func addExtension(filename, ext string) string { + if !strings.HasSuffix(filename, "."+ext) { + return filename + "." + ext } + return filename } diff --git a/internal/plotter.go b/internal/plotter.go index 8a87423..95d23de 100644 --- a/internal/plotter.go +++ b/internal/plotter.go @@ -1,12 +1,28 @@ package internal import ( + "fmt" + "slices" + "strings" + "time" + "gonum.org/v1/plot" "gonum.org/v1/plot/plotter" "gonum.org/v1/plot/vg" ) -func Histogram(results []*SpeedResult, timeUnit string) { +func VerifyPlotFormats(formats string) ([]string, error) { + validFormats := []string{"hist", "histogram", "box", "boxplot", "bar", "errorbar", "bubble"} + formatList := strings.Split(strings.ToLower(formats), ",") + for _, f := range formatList { + if !slices.Contains(validFormats, f) { + return nil, fmt.Errorf("invalid export format: %s", f) + } + } + return formatList, nil +} + +func histogram(results []*SpeedResult, timeUnit string) { p := plot.New() p.Title.Text = "Histogram" p.X.Label.Text = timeUnit @@ -31,3 +47,7 @@ func Histogram(results []*SpeedResult, timeUnit string) { panic(err) } } + +func Plot(plotFormats []string, results []*SpeedResult, timeUnit time.Duration) { + +} \ No newline at end of file diff --git a/internal/time_utils.go b/internal/time_utils.go new file mode 100644 index 0000000..6aa0565 --- /dev/null +++ b/internal/time_utils.go @@ -0,0 +1,110 @@ +package internal + +import ( + "errors" + "fmt" + "math" + "strings" + "sync" + "time" +) + +var ErrInvalidTimeUnit = errors.New("invalid time unit") + +func roundFloat(num float64, digits int) float64 { + tenMultiplier := math.Pow10(digits) + return math.Round(num*tenMultiplier) / tenMultiplier +} + +type numberLike interface { + ~int | ~float64 | ~int32 | ~int64 | ~float32 +} + +func DurationFromNumber[T numberLike](number T, unit time.Duration) time.Duration { + unitToSuffixMap := map[time.Duration]string{ + time.Nanosecond: "ns", + time.Microsecond: "us", + time.Millisecond: "ms", + time.Second: "s", + time.Minute: "m", + time.Hour: "h", + } + suffix, ok := unitToSuffixMap[unit] + if !ok { + // this function is only used internally, panic if unknown time unit is passed + panic("unknown time unit in DurationFromNumber: " + unit.String()) + } + numberFloat := roundFloat(float64(number), 2) + timeString := fmt.Sprintf("%.2f%v", numberFloat, suffix) + duration, err := time.ParseDuration(timeString) + if err != nil { + // again, function only used internally, invalid duration must not be present + panic("unable to parse duration: " + timeString + " in DurationFromNumber \n" + err.Error()) + } + return duration.Round(time.Microsecond) +} + +func ParseTimeUnit(unitString string) (time.Duration, error) { + switch strings.TrimSpace(strings.ToLower(unitString)) { + case "ns": + return time.Nanosecond, nil + case "us", "µs": + return time.Microsecond, nil + case "ms": + return time.Millisecond, nil + case "s": + return time.Second, nil + case "m": + return time.Minute, nil + case "h": + return time.Hour, nil + default: + return 0, ErrInvalidTimeUnit + } +} + +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.Nanoseconds()) / float64(1e6) + 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()) + } +} + +// ModifyTimeUnit takes a slice of [SpeedResult] and modifies its every attribute to suit accordingly +// to the given timeUnit. +func ModifyTimeUnit(results []*SpeedResult, timeUnit time.Duration) { + // 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() + } +} diff --git a/main.go b/main.go index a18b415..302a287 100644 --- a/main.go +++ b/main.go @@ -504,6 +504,7 @@ func main() { AddFlag("export,e", "Comma separated list of benchmark export formats, including json, text, csv and markdown.", commando.String, "none"). AddFlag("filename,f", "The filename to use in exports.", commando.String, "atomic-summary"). AddFlag("time-unit,u", "The time unit to use for exported results. Must be one of ns, us, ms, s, m, h.", commando.String, "ms"). + AddFlag("plot,p", "Comma separated list of plot types. Use all if you want to draw all the plots, or you can specify hist/histogram, box/boxplot, errorbar, bar, bubble.", commando.String, "none"). AddFlag("outlier-threshold", "Minimum number of runs to be outliers for the outlier warning to be displayed, in percentage.", commando.String, "0"). SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) { // * getting args and flag values @@ -661,6 +662,18 @@ func main() { return } + // * getting plot values + plotString, err := flags["plot"].GetString() + if err != nil { + internal.Log("red", "Application error: cannot parse flag values.") + return + } + plotFormats, err := internal.VerifyPlotFormats(plotString) + if err != nil && plotString != "none" { + internal.Log("red", err.Error()) + return + } + var shellCalibration = emptyRunResult() if useShell { shellEmptyCommand, err := buildCommand("''", true, shellPath) @@ -805,13 +818,21 @@ func main() { } internal.RelativeSummary(speedResults) + + // modify speedResults to convert values from microseconds to timeUnit + // if and only if either export or plotting needs to be done + if exportFormatString != "none" || plotString != "none" { + internal.ModifyTimeUnit(speedResults, timeUnit) + } if exportFormatString != "none" { fmt.Println() internal.Export(exportFormats, filename, speedResults, timeUnit) } - internal.Histogram(speedResults, timeUnit.String()[1:]) + if plotString != "none" { + internal.Plot(plotFormats, speedResults, timeUnit) + } })