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