Skip to content

Commit

Permalink
wip export
Browse files Browse the repository at this point in the history
  • Loading branch information
shravanasati committed Jan 20, 2024
1 parent 5f15345 commit 5e3f72d
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 78 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ bin/
atomic-summary*
atomic

*.gif
*.gif
test*
135 changes: 91 additions & 44 deletions internal/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 {

Check failure on line 95 in internal/export.go

View workflow job for this annotation

GitHub Actions / build

undefined: r
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.
Expand Down Expand Up @@ -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()

Check failure on line 239 in internal/export.go

View workflow job for this annotation

GitHub Actions / build

not enough arguments in call to jsonify
case "csv":
csvify()

Check failure on line 241 in internal/export.go

View workflow job for this annotation

GitHub Actions / build

not enough arguments in call to csvify
case "markdown":
markdownify()

Check failure on line 243 in internal/export.go

View workflow job for this annotation

GitHub Actions / build

not enough arguments in call to markdownify
case "txt":
printables := MapFunc[[]*SpeedResult, []*PrintableResult](func(r *SpeedResult) *PrintableResult { return NewPrintableResult().FromSpeedResult(*r) }, results)
textify(printables)
}
}
}
34 changes: 30 additions & 4 deletions internal/result.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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)
Expand All @@ -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
}
14 changes: 9 additions & 5 deletions internal/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
39 changes: 15 additions & 24 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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)
}

})

Expand Down

0 comments on commit 5e3f72d

Please sign in to comment.