aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/stat
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2024-07-24 12:08:49 +0200
committerDmitry Vyukov <dvyukov@google.com>2024-07-24 14:39:45 +0000
commit49e6369fe732c0f81e5b03b36e345afbf3c79a15 (patch)
tree651e322e41a8084abd6f2c80e4f9b7ff50a1dfe9 /pkg/stat
parent1f032c27c8158e44723253179928104813d45cdc (diff)
pkg/stat: rename package name to singular form
Go package names should generally be singular form: https://go.dev/blog/package-names https://rakyll.org/style-packages https://groups.google.com/g/golang-nuts/c/buBwLar1gNw
Diffstat (limited to 'pkg/stat')
-rw-r--r--pkg/stat/avg.go32
-rw-r--r--pkg/stat/sample/pvalue.go20
-rw-r--r--pkg/stat/sample/sample.go73
-rw-r--r--pkg/stat/sample/sample_test.go66
-rw-r--r--pkg/stat/set.go488
-rw-r--r--pkg/stat/set_test.go219
-rw-r--r--pkg/stat/syzbotstats/bug.go32
7 files changed, 930 insertions, 0 deletions
diff --git a/pkg/stat/avg.go b/pkg/stat/avg.go
new file mode 100644
index 000000000..fcfc9f5d5
--- /dev/null
+++ b/pkg/stat/avg.go
@@ -0,0 +1,32 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package stat
+
+import (
+ "sync"
+ "time"
+)
+
+type AverageParameter interface {
+ time.Duration
+}
+
+type AverageValue[T AverageParameter] struct {
+ mu sync.Mutex
+ total int64
+ avg T
+}
+
+func (av *AverageValue[T]) Value() T {
+ av.mu.Lock()
+ defer av.mu.Unlock()
+ return av.avg
+}
+
+func (av *AverageValue[T]) Save(val T) {
+ av.mu.Lock()
+ defer av.mu.Unlock()
+ av.total++
+ av.avg += (val - av.avg) / T(av.total)
+}
diff --git a/pkg/stat/sample/pvalue.go b/pkg/stat/sample/pvalue.go
new file mode 100644
index 000000000..acfff4bc4
--- /dev/null
+++ b/pkg/stat/sample/pvalue.go
@@ -0,0 +1,20 @@
+// Copyright 2021 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package sample
+
+// TODO: I didn't find the substitution as of Feb 2023. Let's keep it as is while it works.
+import "golang.org/x/perf/benchstat" // nolint:all
+
+// Mann-Whitney U test.
+func UTest(old, new *Sample) (pval float64, err error) {
+ // Unfortunately we cannot just invoke MannWhitneyUTest from x/perf/benchstat/internal/stats,
+ // so we first wrap the data in Metrics.
+ mOld := benchstat.Metrics{
+ RValues: old.Xs,
+ }
+ mNew := benchstat.Metrics{
+ RValues: new.Xs,
+ }
+ return benchstat.UTest(&mOld, &mNew)
+}
diff --git a/pkg/stat/sample/sample.go b/pkg/stat/sample/sample.go
new file mode 100644
index 000000000..740f9aefe
--- /dev/null
+++ b/pkg/stat/sample/sample.go
@@ -0,0 +1,73 @@
+// Copyright 2021 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+// Package sample provides various statistical operations and algorithms.
+package sample
+
+import (
+ "math"
+ "sort"
+)
+
+// Sample represents a single sample - set of data points collected during an experiment.
+type Sample struct {
+ Xs []float64
+ Sorted bool
+}
+
+func (s *Sample) Percentile(p float64) float64 {
+ s.Sort()
+ // The code below is taken from golang.org/x/perf/internal/stats
+ // Unfortunately, that package is internal and we cannot just import and use it.
+ N := float64(len(s.Xs))
+ n := 1/3.0 + p*(N+1/3.0) // R8
+ kf, frac := math.Modf(n)
+ k := int(kf)
+ if k <= 0 {
+ return s.Xs[0]
+ } else if k >= len(s.Xs) {
+ return s.Xs[len(s.Xs)-1]
+ }
+ return s.Xs[k-1] + frac*(s.Xs[k]-s.Xs[k-1])
+}
+
+func (s *Sample) Median() float64 {
+ return s.Percentile(0.5)
+}
+
+// Remove outliers by the Tukey's fences method.
+func (s *Sample) RemoveOutliers() *Sample {
+ if len(s.Xs) < 4 {
+ // If the data set is too small, we cannot reliably detect outliers anyway.
+ return s.Copy()
+ }
+ s.Sort()
+ Q1 := s.Percentile(0.25)
+ Q3 := s.Percentile(0.75)
+ minValue := Q1 - 1.5*(Q3-Q1)
+ maxValue := Q3 + 1.5*(Q3-Q1)
+ xs := []float64{}
+ for _, value := range s.Xs {
+ if value >= minValue && value <= maxValue {
+ xs = append(xs, value)
+ }
+ }
+ return &Sample{
+ Xs: xs,
+ Sorted: s.Sorted,
+ }
+}
+
+func (s *Sample) Copy() *Sample {
+ return &Sample{
+ Xs: append([]float64{}, s.Xs...),
+ Sorted: s.Sorted,
+ }
+}
+
+func (s *Sample) Sort() {
+ if !s.Sorted {
+ sort.Slice(s.Xs, func(i, j int) bool { return s.Xs[i] < s.Xs[j] })
+ s.Sorted = true
+ }
+}
diff --git a/pkg/stat/sample/sample_test.go b/pkg/stat/sample/sample_test.go
new file mode 100644
index 000000000..ac7845ccf
--- /dev/null
+++ b/pkg/stat/sample/sample_test.go
@@ -0,0 +1,66 @@
+// Copyright 2021 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package sample
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestMedian(t *testing.T) {
+ tests := []struct {
+ input []float64
+ minMedian float64
+ maxMedian float64
+ }{
+ {
+ input: []float64{1, 2, 3},
+ minMedian: 1.99, // we cannot do exact floating point equality comparison
+ maxMedian: 2.01,
+ },
+ {
+ input: []float64{0, 1, 2, 3},
+ minMedian: 1.0,
+ maxMedian: 2.0,
+ },
+ }
+ for _, test := range tests {
+ sample := Sample{Xs: test.input}
+ median := sample.Median()
+ if median < test.minMedian || median > test.maxMedian {
+ t.Errorf("sample %v, median got %v, median expected [%v;%v]",
+ test.input, median, test.minMedian, test.maxMedian)
+ }
+ }
+}
+
+func TestRemoveOutliers(t *testing.T) {
+ // Some tests just to check the overall sanity of the method.
+ tests := []struct {
+ input []float64
+ output []float64
+ }{
+ {
+ input: []float64{-20, 1, 2, 3, 4, 5},
+ output: []float64{1, 2, 3, 4, 5},
+ },
+ {
+ input: []float64{1, 2, 3, 4, 25},
+ output: []float64{1, 2, 3, 4},
+ },
+ {
+ input: []float64{-10, -5, 0, 5, 10, 15},
+ output: []float64{-10, -5, 0, 5, 10, 15},
+ },
+ }
+ for _, test := range tests {
+ sample := Sample{Xs: test.input}
+ result := sample.RemoveOutliers()
+ result.Sort()
+ if !reflect.DeepEqual(result.Xs, test.output) {
+ t.Errorf("input: %v, expected no outliers: %v, got: %v",
+ test.input, test.output, result.Xs)
+ }
+ }
+}
diff --git a/pkg/stat/set.go b/pkg/stat/set.go
new file mode 100644
index 000000000..690d0406a
--- /dev/null
+++ b/pkg/stat/set.go
@@ -0,0 +1,488 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package stat
+
+import (
+ "bytes"
+ "fmt"
+ "reflect"
+ "sort"
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/VividCortex/gohistogram"
+ "github.com/google/syzkaller/pkg/html/pages"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// This file provides prometheus/streamz style metrics (Val type) for instrumenting code for monitoring.
+// It also provides a registry for such metrics (set type) and a global default registry.
+//
+// Simple uses of metrics:
+//
+// statFoo := stat.New("metric name", "metric description")
+// statFoo.Add(1)
+//
+// stat.New("metric name", "metric description", LenOf(mySlice, rwMutex))
+//
+// Metric visualization code uses Collect/RenderHTML functions to obtain values of all registered metrics.
+
+type UI struct {
+ Name string
+ Desc string
+ Link string
+ Level Level
+ Value string
+ V int
+}
+
+func New(name, desc string, opts ...any) *Val {
+ return global.New(name, desc, opts...)
+}
+
+func Collect(level Level) []UI {
+ return global.Collect(level)
+}
+
+func RenderHTML() ([]byte, error) {
+ return global.RenderHTML()
+}
+
+var global = newSet(256, true)
+
+type set struct {
+ mu sync.Mutex
+ vals map[string]*Val
+ graphs map[string]*graph
+ totalTicks int
+ historySize int
+ historyTicks int
+ historyPos int
+ historyScale int
+}
+
+type graph struct {
+ level Level
+ stacked bool
+ lines map[string]*line
+}
+
+type line struct {
+ desc string
+ rate bool
+ data []float64
+ hist []*gohistogram.NumericHistogram
+}
+
+const (
+ tickPeriod = time.Second
+ histogramBuckets = 255
+)
+
+func newSet(histSize int, tick bool) *set {
+ s := &set{
+ vals: make(map[string]*Val),
+ historySize: histSize,
+ historyScale: 1,
+ graphs: make(map[string]*graph),
+ }
+ if tick {
+ go func() {
+ for range time.NewTicker(tickPeriod).C {
+ s.tick()
+ }
+ }()
+ }
+ return s
+}
+
+func (s *set) Collect(level Level) []UI {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ period := time.Duration(s.totalTicks) * tickPeriod
+ if period == 0 {
+ period = tickPeriod
+ }
+ var res []UI
+ for _, v := range s.vals {
+ if v.level < level {
+ continue
+ }
+ val := v.Val()
+ res = append(res, UI{
+ Name: v.name,
+ Desc: v.desc,
+ Link: v.link,
+ Level: v.level,
+ Value: v.fmt(val, period),
+ V: val,
+ })
+ }
+ sort.Slice(res, func(i, j int) bool {
+ if res[i].Level != res[j].Level {
+ return res[i].Level > res[j].Level
+ }
+ return res[i].Name < res[j].Name
+ })
+ return res
+}
+
+// Additional options for Val metrics.
+
+// Level controls if the metric should be printed to console in periodic heartbeat logs,
+// or showed on the simple web interface, or showed in the expert interface only.
+type Level int
+
+const (
+ All Level = iota
+ Simple
+ Console
+)
+
+// Link adds a hyperlink to metric name.
+type Link string
+
+// Prometheus exports the metric to Prometheus under the given name.
+type Prometheus string
+
+// Rate says to collect/visualize metric rate per unit of time rather then total value.
+type Rate struct{}
+
+// Distribution says to collect/visualize histogram of individual sample distributions.
+type Distribution struct{}
+
+// Graph allows to combine multiple related metrics on a single graph.
+type Graph string
+
+// StackedGraph is like Graph, but shows metrics on a stacked graph.
+type StackedGraph string
+
+// NoGraph says to not visualize the metric as a graph.
+const NoGraph Graph = ""
+
+// LenOf reads the metric value from the given slice/map/chan.
+func LenOf(containerPtr any, mu *sync.RWMutex) func() int {
+ v := reflect.ValueOf(containerPtr)
+ _ = v.Elem().Len() // panics if container is not slice/map/chan
+ return func() int {
+ mu.RLock()
+ defer mu.RUnlock()
+ return v.Elem().Len()
+ }
+}
+
+func FormatMB(v int, period time.Duration) string {
+ const KB, MB = 1 << 10, 1 << 20
+ return fmt.Sprintf("%v MB (%v kb/sec)", (v+MB/2)/MB, (v+KB/2)/KB/int(period/time.Second))
+}
+
+// Addittionally a custom 'func() int' can be passed to read the metric value from the function.
+// and 'func(int, time.Duration) string' can be passed for custom formatting of the metric value.
+
+func (s *set) New(name, desc string, opts ...any) *Val {
+ v := &Val{
+ name: name,
+ desc: desc,
+ graph: name,
+ fmt: func(v int, period time.Duration) string { return strconv.Itoa(v) },
+ }
+ stacked := false
+ for _, o := range opts {
+ switch opt := o.(type) {
+ case Level:
+ v.level = opt
+ case Link:
+ v.link = string(opt)
+ case Graph:
+ v.graph = string(opt)
+ case StackedGraph:
+ v.graph = string(opt)
+ stacked = true
+ case Rate:
+ v.rate = true
+ v.fmt = formatRate
+ case Distribution:
+ v.hist = true
+ case func() int:
+ v.ext = opt
+ case func(int, time.Duration) string:
+ v.fmt = opt
+ case Prometheus:
+ // Prometheus Instrumentation https://prometheus.io/docs/guides/go-application.
+ prometheus.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
+ Name: string(opt),
+ Help: desc,
+ },
+ func() float64 { return float64(v.Val()) },
+ ))
+ default:
+ panic(fmt.Sprintf("unknown stats option %#v", o))
+ }
+ }
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.vals[name] = v
+ if v.graph != "" {
+ if s.graphs[v.graph] == nil {
+ s.graphs[v.graph] = &graph{
+ lines: make(map[string]*line),
+ }
+ }
+ if s.graphs[v.graph].level < v.level {
+ s.graphs[v.graph].level = v.level
+ }
+ s.graphs[v.graph].stacked = stacked
+ }
+ return v
+}
+
+type Val struct {
+ name string
+ desc string
+ link string
+ graph string
+ level Level
+ val atomic.Uint64
+ ext func() int
+ fmt func(int, time.Duration) string
+ rate bool
+ hist bool
+ prev int
+ histMu sync.Mutex
+ histVal *gohistogram.NumericHistogram
+}
+
+func (v *Val) Add(val int) {
+ if v.ext != nil {
+ panic(fmt.Sprintf("stat %v is in external mode", v.name))
+ }
+ if v.hist {
+ v.histMu.Lock()
+ if v.histVal == nil {
+ v.histVal = gohistogram.NewHistogram(histogramBuckets)
+ }
+ v.histVal.Add(float64(val))
+ v.histMu.Unlock()
+ return
+ }
+ v.val.Add(uint64(val))
+}
+
+func (v *Val) Val() int {
+ if v.ext != nil {
+ return v.ext()
+ }
+ if v.hist {
+ v.histMu.Lock()
+ defer v.histMu.Unlock()
+ if v.histVal == nil {
+ return 0
+ }
+ return int(v.histVal.Mean())
+ }
+ return int(v.val.Load())
+}
+
+func formatRate(v int, period time.Duration) string {
+ secs := int(period.Seconds())
+ if x := v / secs; x >= 10 {
+ return fmt.Sprintf("%v (%v/sec)", v, x)
+ }
+ if x := v * 60 / secs; x >= 10 {
+ return fmt.Sprintf("%v (%v/min)", v, x)
+ }
+ x := v * 60 * 60 / secs
+ return fmt.Sprintf("%v (%v/hour)", v, x)
+}
+
+func (s *set) tick() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.historyPos == s.historySize {
+ s.compress()
+ }
+
+ s.totalTicks++
+ s.historyTicks++
+ for _, v := range s.vals {
+ if v.graph == "" {
+ continue
+ }
+ graph := s.graphs[v.graph]
+ ln := graph.lines[v.name]
+ if ln == nil {
+ ln = &line{
+ desc: v.desc,
+ rate: v.rate,
+ }
+ if v.hist {
+ ln.hist = make([]*gohistogram.NumericHistogram, s.historySize)
+ } else {
+ ln.data = make([]float64, s.historySize)
+ }
+ graph.lines[v.name] = ln
+ }
+ if v.hist {
+ if s.historyTicks == s.historyScale {
+ v.histMu.Lock()
+ ln.hist[s.historyPos] = v.histVal
+ v.histVal = nil
+ v.histMu.Unlock()
+ }
+ } else {
+ val := v.Val()
+ pv := &ln.data[s.historyPos]
+ if v.rate {
+ *pv += float64(val-v.prev) / float64(s.historyScale)
+ v.prev = val
+ } else {
+ if *pv < float64(val) {
+ *pv = float64(val)
+ }
+ }
+ }
+ }
+ if s.historyTicks != s.historyScale {
+ return
+ }
+ s.historyTicks = 0
+ s.historyPos++
+}
+
+func (s *set) compress() {
+ half := s.historySize / 2
+ s.historyPos = half
+ s.historyScale *= 2
+ for _, graph := range s.graphs {
+ for _, line := range graph.lines {
+ for i := 0; i < half; i++ {
+ if line.hist != nil {
+ h1, h2 := line.hist[2*i], line.hist[2*i+1]
+ line.hist[2*i], line.hist[2*i+1] = nil, nil
+ line.hist[i] = h1
+ if h1 == nil {
+ line.hist[i] = h2
+ }
+ } else {
+ v1, v2 := line.data[2*i], line.data[2*i+1]
+ line.data[2*i], line.data[2*i+1] = 0, 0
+ if line.rate {
+ line.data[i] = (v1 + v2) / 2
+ } else {
+ line.data[i] = v1
+ if v2 > v1 {
+ line.data[i] = v2
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+func (s *set) RenderHTML() ([]byte, error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ type Point struct {
+ X int
+ Y []float64
+ }
+ type Graph struct {
+ ID int
+ Title string
+ Stacked bool
+ Level Level
+ Lines []string
+ Points []Point
+ }
+ var graphs []Graph
+ tick := s.historyScale * int(tickPeriod.Seconds())
+ for title, graph := range s.graphs {
+ if len(graph.lines) == 0 {
+ continue
+ }
+ g := Graph{
+ ID: len(graphs),
+ Title: title,
+ Stacked: graph.stacked,
+ Level: graph.level,
+ Points: make([]Point, s.historyPos),
+ }
+ for i := 0; i < s.historyPos; i++ {
+ g.Points[i].X = i * tick
+ }
+ for name, ln := range graph.lines {
+ if ln.hist == nil {
+ g.Lines = append(g.Lines, name+": "+ln.desc)
+ for i := 0; i < s.historyPos; i++ {
+ g.Points[i].Y = append(g.Points[i].Y, ln.data[i])
+ }
+ } else {
+ for _, percent := range []int{10, 50, 90} {
+ g.Lines = append(g.Lines, fmt.Sprintf("%v%%", percent))
+ for i := 0; i < s.historyPos; i++ {
+ v := 0.0
+ if ln.hist[i] != nil {
+ v = ln.hist[i].Quantile(float64(percent) / 100)
+ }
+ g.Points[i].Y = append(g.Points[i].Y, v)
+ }
+ }
+ }
+ }
+ graphs = append(graphs, g)
+ }
+ sort.Slice(graphs, func(i, j int) bool {
+ if graphs[i].Level != graphs[j].Level {
+ return graphs[i].Level > graphs[j].Level
+ }
+ return graphs[i].Title < graphs[j].Title
+ })
+ buf := new(bytes.Buffer)
+ err := htmlTemplate.Execute(buf, graphs)
+ return buf.Bytes(), err
+}
+
+var htmlTemplate = pages.Create(`
+<!doctype html>
+<html>
+<head>
+ <title>syzkaller stats</title>
+ <script type="text/javascript" src="https://www.google.com/jsapi"></script>
+ {{HEAD}}
+</head>
+<body>
+{{range $g := .}}
+ <div id="div_{{$g.ID}}"></div>
+ <script type="text/javascript">
+ google.load("visualization", "1", {packages:["corechart"]});
+ google.setOnLoadCallback(function() {
+ new google.visualization. {{if $g.Stacked}} AreaChart {{else}} LineChart {{end}} (
+ document.getElementById('div_{{$g.ID}}')).
+ draw(google.visualization.arrayToDataTable([
+ ["-" {{range $line := $g.Lines}} , '{{$line}}' {{end}}],
+ {{range $p := $g.Points}} [ {{$p.X}} {{range $y := $p.Y}} , {{$y}} {{end}} ], {{end}}
+ ]), {
+ title: '{{$g.Title}}',
+ titlePosition: 'in',
+ width: "95%",
+ height: "400",
+ chartArea: {width: '95%', height: '85%'},
+ legend: {position: 'in'},
+ lineWidth: 2,
+ focusTarget: "category",
+ {{if $g.Stacked}} isStacked: true, {{end}}
+ vAxis: {minValue: 1, textPosition: 'in', gridlines: {multiple: 1}, minorGridlines: {multiple: 1}},
+ hAxis: {minValue: 1, textPosition: 'out', maxAlternation: 1, gridlines: {multiple: 1},
+ minorGridlines: {multiple: 1}},
+ })
+ });
+ </script>
+{{end}}
+</body>
+</html>
+`)
diff --git a/pkg/stat/set_test.go b/pkg/stat/set_test.go
new file mode 100644
index 000000000..4313db1ef
--- /dev/null
+++ b/pkg/stat/set_test.go
@@ -0,0 +1,219 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package stat
+
+import (
+ "fmt"
+ "math/rand"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSet(t *testing.T) {
+ a := assert.New(t)
+ set := newSet(4, false)
+ a.Empty(set.Collect(All))
+ _, err := set.RenderHTML()
+ a.NoError(err)
+
+ v0 := set.New("v0", "desc0")
+ a.Equal(v0.Val(), 0)
+ v0.Add(1)
+ a.Equal(v0.Val(), 1)
+ v0.Add(1)
+ a.Equal(v0.Val(), 2)
+
+ vv1 := 0
+ v1 := set.New("v1", "desc1", Simple, func() int { return vv1 })
+ a.Equal(v1.Val(), 0)
+ vv1 = 11
+ a.Equal(v1.Val(), 11)
+ a.Panics(func() { v1.Add(1) })
+
+ v2 := set.New("v2", "desc2", Console, func(v int, period time.Duration) string {
+ return fmt.Sprintf("v2 %v %v", v, period)
+ })
+ v2.Add(100)
+
+ v3 := set.New("v3", "desc3", Link("/v3"), NoGraph, Distribution{})
+ a.Equal(v3.Val(), 0)
+ v3.Add(10)
+ a.Equal(v3.Val(), 10)
+ v3.Add(20)
+ a.Equal(v3.Val(), 15)
+ v3.Add(20)
+ a.Equal(v3.Val(), 16)
+ v3.Add(30)
+ a.Equal(v3.Val(), 20)
+ v3.Add(30)
+ a.Equal(v3.Val(), 22)
+ v3.Add(30)
+ v3.Add(30)
+ a.Equal(v3.Val(), 24)
+
+ v4 := set.New("v4", "desc4", Rate{}, Graph("graph"))
+ v4.Add(10)
+ a.Equal(v4.Val(), 10)
+ v4.Add(10)
+ a.Equal(v4.Val(), 20)
+
+ a.Panics(func() { set.New("v0", "desc0", float64(1)) })
+
+ ui := set.Collect(All)
+ a.Equal(len(ui), 5)
+ a.Equal(ui[0], UI{"v2", "desc2", "", Console, "v2 100 1s", 100})
+ a.Equal(ui[1], UI{"v1", "desc1", "", Simple, "11", 11})
+ a.Equal(ui[2], UI{"v0", "desc0", "", All, "2", 2})
+ a.Equal(ui[3], UI{"v3", "desc3", "/v3", All, "24", 24})
+ a.Equal(ui[4], UI{"v4", "desc4", "", All, "20 (20/sec)", 20})
+
+ ui1 := set.Collect(Simple)
+ a.Equal(len(ui1), 2)
+ a.Equal(ui1[0].Name, "v2")
+ a.Equal(ui1[1].Name, "v1")
+
+ ui2 := set.Collect(Console)
+ a.Equal(len(ui2), 1)
+ a.Equal(ui2[0].Name, "v2")
+
+ _, err = set.RenderHTML()
+ a.NoError(err)
+}
+
+func TestSetRateFormat(t *testing.T) {
+ a := assert.New(t)
+ set := newSet(4, false)
+ v := set.New("v", "desc", Rate{})
+ a.Equal(set.Collect(All)[0].Value, "0 (0/hour)")
+ v.Add(1)
+ a.Equal(set.Collect(All)[0].Value, "1 (60/min)")
+ v.Add(99)
+ a.Equal(set.Collect(All)[0].Value, "100 (100/sec)")
+}
+
+func TestSetHistoryCounter(t *testing.T) {
+ a := assert.New(t)
+ set := newSet(4, false)
+ v := set.New("v0", "desc0")
+ set.tick()
+ hist := func() []float64 { return set.graphs["v0"].lines["v0"].data[:set.historyPos] }
+ step := func(n int) []float64 {
+ v.Add(n)
+ set.tick()
+ return hist()
+ }
+ a.Equal(hist(), []float64{0})
+ v.Add(1)
+ v.Add(1)
+ a.Equal(hist(), []float64{0})
+ set.tick()
+ a.Equal(hist(), []float64{0, 2})
+ v.Add(3)
+ a.Equal(hist(), []float64{0, 2})
+ set.tick()
+ a.Equal(hist(), []float64{0, 2, 5})
+ a.Equal(step(-1), []float64{0, 2, 5, 4})
+ // Compacted, each new history value will require 2 steps.
+ a.Equal(step(7), []float64{2, 5})
+ a.Equal(step(-10), []float64{2, 5, 11})
+ a.Equal(step(2), []float64{2, 5, 11})
+ a.Equal(step(1), []float64{2, 5, 11, 4})
+ // 4 steps for each new value.
+ a.Equal(step(1), []float64{5, 11})
+ a.Equal(step(1), []float64{5, 11})
+ a.Equal(step(1), []float64{5, 11})
+ a.Equal(step(1), []float64{5, 11, 8})
+}
+
+func TestSetHistoryRate(t *testing.T) {
+ a := assert.New(t)
+ set := newSet(4, false)
+ v := set.New("v0", "desc0", Rate{})
+ step := func(n int) []float64 {
+ v.Add(n)
+ set.tick()
+ return set.graphs["v0"].lines["v0"].data[:set.historyPos]
+ }
+ a.Equal(step(3), []float64{3})
+ a.Equal(step(1), []float64{3, 1})
+ a.Equal(step(2), []float64{3, 1, 2})
+ a.Equal(step(5), []float64{3, 1, 2, 5})
+ a.Equal(step(1), []float64{2, 3.5})
+ a.Equal(step(2), []float64{2, 3.5, 1.5})
+ a.Equal(step(2), []float64{2, 3.5, 1.5})
+ a.Equal(step(4), []float64{2, 3.5, 1.5, 3})
+ a.Equal(step(1), []float64{2.75, 2.25})
+ a.Equal(step(2), []float64{2.75, 2.25})
+ a.Equal(step(3), []float64{2.75, 2.25})
+ a.Equal(step(4), []float64{2.75, 2.25, 2.5})
+}
+
+func TestSetHistoryDistribution(t *testing.T) {
+ a := assert.New(t)
+ set := newSet(4, false)
+ v := set.New("v0", "desc0", Distribution{})
+ step := func(n int) [3][]float64 {
+ v.Add(n)
+ set.tick()
+ var history [3][]float64
+ for p, percent := range []int{10, 50, 90} {
+ history[p] = make([]float64, set.historyPos)
+ for i := 0; i < set.historyPos; i++ {
+ hist := set.graphs["v0"].lines["v0"].hist[i]
+ if hist != nil {
+ history[p][i] = hist.Quantile(float64(percent) / 100)
+ }
+ }
+ }
+ return history
+ }
+ a.Equal(step(3), [3][]float64{{3}, {3}, {3}})
+ a.Equal(step(6), [3][]float64{{3, 6}, {3, 6}, {3, 6}})
+ a.Equal(step(1), [3][]float64{{3, 6, 1}, {3, 6, 1}, {3, 6, 1}})
+ a.Equal(step(2), [3][]float64{{3, 6, 1, 2}, {3, 6, 1, 2}, {3, 6, 1, 2}})
+ a.Equal(step(1), [3][]float64{{3, 1}, {3, 1}, {3, 1}})
+ a.Equal(step(10), [3][]float64{{3, 1, 1}, {3, 1, 1}, {3, 1, 10}})
+}
+
+func TestSetStress(t *testing.T) {
+ set := newSet(4, false)
+ var stop atomic.Bool
+ var seq atomic.Uint64
+ start := func(f func()) {
+ go func() {
+ for !stop.Load() {
+ f()
+ }
+ }()
+ }
+ for p := 0; p < 2; p++ {
+ for _, opt := range []any{Link(""), NoGraph, Rate{}, Distribution{}} {
+ opt := opt
+ go func() {
+ v := set.New(fmt.Sprintf("v%v", seq.Add(1)), "desc", opt)
+ for p1 := 0; p1 < 2; p1++ {
+ start(func() { v.Val() })
+ start(func() { v.Add(rand.Intn(10000)) })
+ }
+ }()
+ }
+ go func() {
+ var vv atomic.Uint64
+ v := set.New(fmt.Sprintf("v%v", seq.Add(1)), "desc",
+ func() int { return int(vv.Load()) })
+ for p1 := 0; p1 < 2; p1++ {
+ start(func() { v.Val() })
+ start(func() { vv.Store(uint64(rand.Intn(10000))) })
+ }
+ }()
+ start(func() { set.Collect(All) })
+ start(func() { set.RenderHTML() })
+ start(func() { set.tick() })
+ }
+ time.Sleep(time.Second)
+ stop.Store(true)
+}
diff --git a/pkg/stat/syzbotstats/bug.go b/pkg/stat/syzbotstats/bug.go
new file mode 100644
index 000000000..c11274583
--- /dev/null
+++ b/pkg/stat/syzbotstats/bug.go
@@ -0,0 +1,32 @@
+// Copyright 2023 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package syzbotstats
+
+import "time"
+
+type BugStatSummary struct {
+ Title string
+ IDs []string // IDs used by syzbot for this bug.
+ FirstTime time.Time // When the bug was first hit.
+ ReleasedTime time.Time // When the bug was published.
+ ReproTime time.Time // When we found the reproducer.
+ CauseBisectTime time.Time // When we found cause bisection.
+ ResolvedTime time.Time // When the bug was resolved.
+ Status BugStatus
+ Subsystems []string
+ Strace bool // Whether we managed to reproduce under strace.
+ HitsPerDay float64 // Average number of bug hits per day.
+ FixHashes []string // Hashes of fix commits.
+ HappenedOn []string // Managers on which the crash happened.
+}
+
+type BugStatus string
+
+const (
+ BugFixed BugStatus = "fixed"
+ BugInvalidated BugStatus = "invalidated"
+ BugAutoInvalidated BugStatus = "auto-invalidated"
+ BugDup BugStatus = "dup"
+ BugPending BugStatus = "pending"
+)