github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/benchmark/internal/cireport/tablereport.go (about) 1 package cireport 2 3 import ( 4 "context" 5 "embed" 6 "fmt" 7 "math" 8 "os" 9 "strings" 10 "sync" 11 "text/template" 12 "time" 13 14 "golang.org/x/sync/errgroup" 15 "gopkg.in/yaml.v2" 16 ) 17 18 var ( 19 //go:embed resources/* 20 resources embed.FS 21 ) 22 23 type Querier interface { 24 Instant(query string, t time.Time) (float64, error) 25 } 26 27 type Query struct { 28 Name string `yaml:"name"` 29 Description string `yaml:"description" default:""` 30 31 Base string `yaml:"base"` 32 BaseResult float64 33 Target string `yaml:"target"` 34 TargetResult float64 35 // TODO(eh-am): implement a default value 36 DiffThreshold float64 `yaml:"diffThresholdPercent"` 37 DiffPercent float64 38 39 BiggerIsBetter bool `yaml:"biggerIsBetter"` 40 41 mu sync.Mutex 42 } 43 44 type QueriesConfig struct { 45 BaseName string `yaml:"baseName"` 46 TargetName string `yaml:"targetName"` 47 48 Queries []Query `yaml:"queries"` 49 } 50 type TableReport struct { 51 q Querier 52 } 53 54 func NewTableReport(q Querier) *TableReport { 55 return &TableReport{ 56 q, 57 } 58 } 59 60 func TableReportCli(q Querier, queriesFile string) (string, error) { 61 var qCfg QueriesConfig 62 63 // read the file 64 yamlFile, err := os.ReadFile(queriesFile) 65 if err != nil { 66 return "", err 67 } 68 err = yaml.Unmarshal(yamlFile, &qCfg) 69 if err != nil { 70 return "", err 71 } 72 73 t := NewTableReport(q) 74 75 return t.Report(context.Background(), &qCfg) 76 } 77 78 // TableReport reports query results from prometheus in markdown format 79 func (r *TableReport) Report(ctx context.Context, qCfg *QueriesConfig) (string, error) { 80 // TODO: treat each error individually? 81 g, ctx := errgroup.WithContext(ctx) 82 83 now := time.Now() 84 for index, queries := range qCfg.Queries { 85 i := index 86 q := queries 87 g.Go(func() error { 88 baseResult, err := r.q.Instant(q.Base, now) 89 if err != nil { 90 return err 91 } 92 targetResult, err := r.q.Instant(q.Target, now) 93 if err != nil { 94 return err 95 } 96 97 diffPercent := ((targetResult - baseResult) / (targetResult + baseResult)) * 100 98 99 // TODO(eh-am): should I lock the whole array? 100 q.mu.Lock() 101 qCfg.Queries[i].BaseResult = baseResult 102 qCfg.Queries[i].TargetResult = targetResult 103 104 // compute as much as possible beforehand 105 // so that the template code is cleaner 106 qCfg.Queries[i].DiffPercent = diffPercent 107 q.mu.Unlock() 108 return nil 109 }) 110 } 111 112 if err := g.Wait(); err != nil { 113 return "", err 114 } 115 116 var tpl strings.Builder 117 118 data := struct { 119 QC *QueriesConfig 120 }{ 121 QC: qCfg, 122 } 123 124 t, err := template.New("pr.gotpl"). 125 Funcs(template.FuncMap{ 126 "formatDiff": formatDiff, 127 }). 128 ParseFS(resources, "resources/pr.gotpl") 129 if err != nil { 130 return "", err 131 } 132 133 if err := t.Execute(&tpl, data); err != nil { 134 return "", err 135 } 136 137 return tpl.String(), nil 138 } 139 140 // formatDiff formats diff in a markdown intended format 141 func formatDiff(q Query) string { 142 diffPercent := ((q.TargetResult - q.BaseResult) / ((q.TargetResult + q.BaseResult) / 2)) * 100.0 143 144 res := fmt.Sprintf("%.2f (%.2f%%)", q.TargetResult-q.BaseResult, diffPercent) 145 146 // TODO: use something friendlier to colourblind people? 147 goodEmoji := ":green_square:" 148 badEmoji := ":red_square:" 149 150 // is threshold relevant? 151 if math.Abs(diffPercent) > q.DiffThreshold { 152 if q.BiggerIsBetter { // higher is better 153 emoji := badEmoji 154 if q.TargetResult > q.BaseResult { 155 emoji = goodEmoji 156 } 157 158 return fmt.Sprintf("%s %s", emoji, res) 159 } 160 161 // lower is better 162 emoji := badEmoji 163 if q.TargetResult < q.BaseResult { 164 emoji = goodEmoji 165 } 166 return fmt.Sprintf("%s %s", emoji, res) 167 } 168 169 return res 170 }