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 }