github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/stat/set.go (about)

     1  // Copyright 2024 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package stat
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"sort"
    10  	"strconv"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/VividCortex/gohistogram"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  )
    18  
    19  // This file provides prometheus/streamz style metrics (Val type) for instrumenting code for monitoring.
    20  // It also provides a registry for such metrics (set type) and a global default registry.
    21  //
    22  // Simple uses of metrics:
    23  //
    24  //	statFoo := stat.New("metric name", "metric description")
    25  //	statFoo.Add(1)
    26  //
    27  //	stat.New("metric name", "metric description", LenOf(mySlice, rwMutex))
    28  //
    29  // Metric visualization code uses Collect/RenderGraphs functions to obtain values of all registered metrics.
    30  
    31  type UI struct {
    32  	Name  string
    33  	Desc  string
    34  	Link  string
    35  	Level Level
    36  	Value string
    37  	V     int
    38  }
    39  
    40  func New(name, desc string, opts ...any) *Val {
    41  	return global.New(name, desc, opts...)
    42  }
    43  
    44  func Collect(level Level) []UI {
    45  	return global.Collect(level)
    46  }
    47  
    48  func RenderGraphs() []UIGraph {
    49  	return global.RenderGraphs()
    50  }
    51  
    52  var global = newSet(256, true)
    53  
    54  type set struct {
    55  	mu           sync.Mutex
    56  	vals         map[string]*Val
    57  	graphs       map[string]*graph
    58  	nextOrder    atomic.Uint64
    59  	totalTicks   int
    60  	historySize  int
    61  	historyTicks int
    62  	historyPos   int
    63  	historyScale int
    64  }
    65  
    66  type graph struct {
    67  	level   Level
    68  	stacked bool
    69  	lines   map[string]*line
    70  }
    71  
    72  type line struct {
    73  	name  string
    74  	desc  string
    75  	order uint64
    76  	rate  bool
    77  	data  []float64
    78  	hist  []*gohistogram.NumericHistogram
    79  }
    80  
    81  const (
    82  	tickPeriod       = time.Second
    83  	histogramBuckets = 255
    84  )
    85  
    86  func newSet(histSize int, tick bool) *set {
    87  	s := &set{
    88  		vals:         make(map[string]*Val),
    89  		historySize:  histSize,
    90  		historyScale: 1,
    91  		graphs:       make(map[string]*graph),
    92  	}
    93  	if tick {
    94  		go func() {
    95  			for range time.NewTicker(tickPeriod).C {
    96  				s.tick()
    97  			}
    98  		}()
    99  	}
   100  	return s
   101  }
   102  
   103  func (s *set) Collect(level Level) []UI {
   104  	s.mu.Lock()
   105  	defer s.mu.Unlock()
   106  	period := time.Duration(s.totalTicks) * tickPeriod
   107  	if period == 0 {
   108  		period = tickPeriod
   109  	}
   110  	var res []UI
   111  	for _, v := range s.vals {
   112  		if v.level < level {
   113  			continue
   114  		}
   115  		val := v.Val()
   116  		res = append(res, UI{
   117  			Name:  v.name,
   118  			Desc:  v.desc,
   119  			Link:  v.link,
   120  			Level: v.level,
   121  			Value: v.fmt(val, period),
   122  			V:     val,
   123  		})
   124  	}
   125  	sort.Slice(res, func(i, j int) bool {
   126  		if res[i].Level != res[j].Level {
   127  			return res[i].Level > res[j].Level
   128  		}
   129  		return res[i].Name < res[j].Name
   130  	})
   131  	return res
   132  }
   133  
   134  // Additional options for Val metrics.
   135  
   136  // Level controls if the metric should be printed to console in periodic heartbeat logs,
   137  // or showed on the simple web interface, or showed in the expert interface only.
   138  type Level int
   139  
   140  const (
   141  	All Level = iota
   142  	Simple
   143  	Console
   144  )
   145  
   146  // Link adds a hyperlink to metric name.
   147  type Link string
   148  
   149  // Prometheus exports the metric to Prometheus under the given name.
   150  type Prometheus string
   151  
   152  // Rate says to collect/visualize metric rate per unit of time rather then total value.
   153  type Rate struct{}
   154  
   155  // Distribution says to collect/visualize histogram of individual sample distributions.
   156  type Distribution struct{}
   157  
   158  // Graph allows to combine multiple related metrics on a single graph.
   159  type Graph string
   160  
   161  // StackedGraph is like Graph, but shows metrics on a stacked graph.
   162  type StackedGraph string
   163  
   164  // NoGraph says to not visualize the metric as a graph.
   165  const NoGraph Graph = ""
   166  
   167  // LenOf reads the metric value from the given slice/map/chan.
   168  func LenOf(containerPtr any, mu *sync.RWMutex) func() int {
   169  	v := reflect.ValueOf(containerPtr)
   170  	_ = v.Elem().Len() // panics if container is not slice/map/chan
   171  	return func() int {
   172  		mu.RLock()
   173  		defer mu.RUnlock()
   174  		return v.Elem().Len()
   175  	}
   176  }
   177  
   178  func FormatMB(v int, period time.Duration) string {
   179  	const KB, MB = 1 << 10, 1 << 20
   180  	return fmt.Sprintf("%v MB (%v kb/sec)", (v+MB/2)/MB, (v+KB/2)/KB/int(period/time.Second))
   181  }
   182  
   183  // Addittionally a custom 'func() int' can be passed to read the metric value from the function.
   184  // and 'func(int, time.Duration) string' can be passed for custom formatting of the metric value.
   185  
   186  func (s *set) New(name, desc string, opts ...any) *Val {
   187  	v := &Val{
   188  		name:  name,
   189  		desc:  desc,
   190  		graph: name,
   191  		order: s.nextOrder.Add(1),
   192  		fmt:   func(v int, period time.Duration) string { return strconv.Itoa(v) },
   193  	}
   194  	stacked := false
   195  	for _, o := range opts {
   196  		switch opt := o.(type) {
   197  		case Level:
   198  			v.level = opt
   199  		case Link:
   200  			v.link = string(opt)
   201  		case Graph:
   202  			v.graph = string(opt)
   203  		case StackedGraph:
   204  			v.graph = string(opt)
   205  			stacked = true
   206  		case Rate:
   207  			v.rate = true
   208  			v.fmt = formatRate
   209  		case Distribution:
   210  			v.hist = true
   211  		case func() int:
   212  			v.ext = opt
   213  		case func(int, time.Duration) string:
   214  			v.fmt = opt
   215  		case Prometheus:
   216  			// Prometheus Instrumentation https://prometheus.io/docs/guides/go-application.
   217  			prometheus.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
   218  				Name: string(opt),
   219  				Help: desc,
   220  			},
   221  				func() float64 { return float64(v.Val()) },
   222  			))
   223  		default:
   224  			panic(fmt.Sprintf("unknown stats option %#v", o))
   225  		}
   226  	}
   227  	s.mu.Lock()
   228  	defer s.mu.Unlock()
   229  	s.vals[name] = v
   230  	if v.graph != "" {
   231  		if s.graphs[v.graph] == nil {
   232  			s.graphs[v.graph] = &graph{
   233  				lines: make(map[string]*line),
   234  			}
   235  		}
   236  		s.graphs[v.graph].level = max(s.graphs[v.graph].level, v.level)
   237  		s.graphs[v.graph].stacked = stacked
   238  	}
   239  	return v
   240  }
   241  
   242  type Val struct {
   243  	name    string
   244  	desc    string
   245  	link    string
   246  	graph   string
   247  	level   Level
   248  	order   uint64
   249  	val     atomic.Uint64
   250  	ext     func() int
   251  	fmt     func(int, time.Duration) string
   252  	rate    bool
   253  	hist    bool
   254  	prev    int
   255  	histMu  sync.Mutex
   256  	histVal *gohistogram.NumericHistogram
   257  }
   258  
   259  func (v *Val) Add(val int) {
   260  	if v.ext != nil {
   261  		panic(fmt.Sprintf("stat %v is in external mode", v.name))
   262  	}
   263  	if v.hist {
   264  		v.histMu.Lock()
   265  		if v.histVal == nil {
   266  			v.histVal = gohistogram.NewHistogram(histogramBuckets)
   267  		}
   268  		v.histVal.Add(float64(val))
   269  		v.histMu.Unlock()
   270  		return
   271  	}
   272  	v.val.Add(uint64(val))
   273  }
   274  
   275  func (v *Val) Val() int {
   276  	if v.ext != nil {
   277  		return v.ext()
   278  	}
   279  	if v.hist {
   280  		v.histMu.Lock()
   281  		defer v.histMu.Unlock()
   282  		if v.histVal == nil {
   283  			return 0
   284  		}
   285  		return int(v.histVal.Mean())
   286  	}
   287  	return int(v.val.Load())
   288  }
   289  
   290  func formatRate(v int, period time.Duration) string {
   291  	secs := int(period.Seconds())
   292  	if x := v / secs; x >= 10 {
   293  		return fmt.Sprintf("%v (%v/sec)", v, x)
   294  	}
   295  	if x := v * 60 / secs; x >= 10 {
   296  		return fmt.Sprintf("%v (%v/min)", v, x)
   297  	}
   298  	x := v * 60 * 60 / secs
   299  	return fmt.Sprintf("%v (%v/hour)", v, x)
   300  }
   301  
   302  func (s *set) tick() {
   303  	s.mu.Lock()
   304  	defer s.mu.Unlock()
   305  
   306  	if s.historyPos == s.historySize {
   307  		s.compress()
   308  	}
   309  
   310  	s.totalTicks++
   311  	s.historyTicks++
   312  	for _, v := range s.vals {
   313  		if v.graph == "" {
   314  			continue
   315  		}
   316  		graph := s.graphs[v.graph]
   317  		ln := graph.lines[v.name]
   318  		if ln == nil {
   319  			ln = &line{
   320  				name:  v.name,
   321  				desc:  v.desc,
   322  				order: v.order,
   323  				rate:  v.rate,
   324  			}
   325  			if v.hist {
   326  				ln.hist = make([]*gohistogram.NumericHistogram, s.historySize)
   327  			} else {
   328  				ln.data = make([]float64, s.historySize)
   329  			}
   330  			graph.lines[v.name] = ln
   331  		}
   332  		if v.hist {
   333  			if s.historyTicks == s.historyScale {
   334  				v.histMu.Lock()
   335  				ln.hist[s.historyPos] = v.histVal
   336  				v.histVal = nil
   337  				v.histMu.Unlock()
   338  			}
   339  		} else {
   340  			val := v.Val()
   341  			pv := &ln.data[s.historyPos]
   342  			if v.rate {
   343  				*pv += float64(val-v.prev) / float64(s.historyScale)
   344  				v.prev = val
   345  			} else {
   346  				*pv = max(*pv, float64(val))
   347  			}
   348  		}
   349  	}
   350  	if s.historyTicks != s.historyScale {
   351  		return
   352  	}
   353  	s.historyTicks = 0
   354  	s.historyPos++
   355  }
   356  
   357  func (s *set) compress() {
   358  	half := s.historySize / 2
   359  	s.historyPos = half
   360  	s.historyScale *= 2
   361  	for _, graph := range s.graphs {
   362  		for _, line := range graph.lines {
   363  			for i := 0; i < half; i++ {
   364  				if line.hist != nil {
   365  					h1, h2 := line.hist[2*i], line.hist[2*i+1]
   366  					line.hist[2*i], line.hist[2*i+1] = nil, nil
   367  					line.hist[i] = h1
   368  					if h1 == nil {
   369  						line.hist[i] = h2
   370  					}
   371  				} else {
   372  					v1, v2 := line.data[2*i], line.data[2*i+1]
   373  					line.data[2*i], line.data[2*i+1] = 0, 0
   374  					if line.rate {
   375  						line.data[i] = (v1 + v2) / 2
   376  					} else {
   377  						line.data[i] = v1
   378  						if v2 > v1 {
   379  							line.data[i] = v2
   380  						}
   381  					}
   382  				}
   383  			}
   384  		}
   385  	}
   386  }
   387  
   388  type UIGraph struct {
   389  	ID      int
   390  	Title   string
   391  	Stacked bool
   392  	Level   Level
   393  	Lines   []string
   394  	Points  []UIPoint
   395  }
   396  
   397  type UIPoint struct {
   398  	X int
   399  	Y []float64
   400  }
   401  
   402  func (s *set) RenderGraphs() []UIGraph {
   403  	s.mu.Lock()
   404  	defer s.mu.Unlock()
   405  	var graphs []UIGraph
   406  	tick := s.historyScale * int(tickPeriod.Seconds())
   407  	for title, graph := range s.graphs {
   408  		if len(graph.lines) == 0 {
   409  			continue
   410  		}
   411  		var lines []*line
   412  		for _, ln := range graph.lines {
   413  			lines = append(lines, ln)
   414  		}
   415  		sort.Slice(lines, func(i, j int) bool {
   416  			return lines[i].order < lines[j].order
   417  		})
   418  		g := UIGraph{
   419  			ID:      len(graphs),
   420  			Title:   title,
   421  			Stacked: graph.stacked,
   422  			Level:   graph.level,
   423  			Points:  make([]UIPoint, s.historyPos),
   424  		}
   425  		for i := 0; i < s.historyPos; i++ {
   426  			g.Points[i].X = i * tick
   427  		}
   428  		for _, ln := range lines {
   429  			if ln.hist == nil {
   430  				g.Lines = append(g.Lines, ln.name+": "+ln.desc)
   431  				for i := 0; i < s.historyPos; i++ {
   432  					g.Points[i].Y = append(g.Points[i].Y, ln.data[i])
   433  				}
   434  			} else {
   435  				for _, percent := range []int{10, 50, 90} {
   436  					g.Lines = append(g.Lines, fmt.Sprintf("%v%%", percent))
   437  					for i := 0; i < s.historyPos; i++ {
   438  						v := 0.0
   439  						if ln.hist[i] != nil {
   440  							v = ln.hist[i].Quantile(float64(percent) / 100)
   441  						}
   442  						g.Points[i].Y = append(g.Points[i].Y, v)
   443  					}
   444  				}
   445  			}
   446  		}
   447  		graphs = append(graphs, g)
   448  	}
   449  	sort.Slice(graphs, func(i, j int) bool {
   450  		if graphs[i].Level != graphs[j].Level {
   451  			return graphs[i].Level > graphs[j].Level
   452  		}
   453  		return graphs[i].Title < graphs[j].Title
   454  	})
   455  	return graphs
   456  }