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  }