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 `)