Skip to content

Commit

Permalink
distribute utils and add plotting helper
Browse files Browse the repository at this point in the history
  • Loading branch information
shravanasati committed Jan 30, 2024
1 parent 9367f6d commit 260d95c
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 106 deletions.
50 changes: 0 additions & 50 deletions internal/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"path/filepath"
"slices"
"strings"
"sync"
"text/template"
"time"
)
Expand Down Expand Up @@ -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":
Expand Down
58 changes: 4 additions & 54 deletions internal/utils.go → internal/io_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
22 changes: 21 additions & 1 deletion internal/plotter.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -31,3 +47,7 @@ func Histogram(results []*SpeedResult, timeUnit string) {
panic(err)
}
}

func Plot(plotFormats []string, results []*SpeedResult, timeUnit time.Duration) {

}
110 changes: 110 additions & 0 deletions internal/time_utils.go
Original file line number Diff line number Diff line change
@@ -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()
}
}
23 changes: 22 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

})

Expand Down

0 comments on commit 260d95c

Please sign in to comment.