github.com/uber-go/tally/v4@v4.1.17/stats.go (about) 1 // Copyright (c) 2021 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package tally 22 23 import ( 24 "fmt" 25 "math" 26 "sort" 27 "sync" 28 "sync/atomic" 29 "time" 30 31 "github.com/uber-go/tally/v4/internal/identity" 32 ) 33 34 var ( 35 capabilitiesNone = &capabilities{ 36 reporting: false, 37 tagging: false, 38 } 39 capabilitiesReportingNoTagging = &capabilities{ 40 reporting: true, 41 tagging: false, 42 } 43 capabilitiesReportingTagging = &capabilities{ 44 reporting: true, 45 tagging: true, 46 } 47 ) 48 49 type capabilities struct { 50 reporting bool 51 tagging bool 52 } 53 54 func (c *capabilities) Reporting() bool { 55 return c.reporting 56 } 57 58 func (c *capabilities) Tagging() bool { 59 return c.tagging 60 } 61 62 type counter struct { 63 prev int64 64 curr int64 65 cachedCount CachedCount 66 } 67 68 func newCounter(cachedCount CachedCount) *counter { 69 return &counter{cachedCount: cachedCount} 70 } 71 72 func (c *counter) Inc(v int64) { 73 atomic.AddInt64(&c.curr, v) 74 } 75 76 func (c *counter) value() int64 { 77 curr := atomic.LoadInt64(&c.curr) 78 79 prev := atomic.LoadInt64(&c.prev) 80 if prev == curr { 81 return 0 82 } 83 atomic.StoreInt64(&c.prev, curr) 84 return curr - prev 85 } 86 87 func (c *counter) report(name string, tags map[string]string, r StatsReporter) { 88 delta := c.value() 89 if delta == 0 { 90 return 91 } 92 93 r.ReportCounter(name, tags, delta) 94 } 95 96 func (c *counter) cachedReport() { 97 delta := c.value() 98 if delta == 0 { 99 return 100 } 101 102 c.cachedCount.ReportCount(delta) 103 } 104 105 func (c *counter) snapshot() int64 { 106 return atomic.LoadInt64(&c.curr) - atomic.LoadInt64(&c.prev) 107 } 108 109 type gauge struct { 110 updated uint64 111 curr uint64 112 cachedGauge CachedGauge 113 } 114 115 func newGauge(cachedGauge CachedGauge) *gauge { 116 return &gauge{cachedGauge: cachedGauge} 117 } 118 119 func (g *gauge) Update(v float64) { 120 atomic.StoreUint64(&g.curr, math.Float64bits(v)) 121 atomic.StoreUint64(&g.updated, 1) 122 } 123 124 func (g *gauge) value() float64 { 125 return math.Float64frombits(atomic.LoadUint64(&g.curr)) 126 } 127 128 func (g *gauge) report(name string, tags map[string]string, r StatsReporter) { 129 if atomic.SwapUint64(&g.updated, 0) == 1 { 130 r.ReportGauge(name, tags, g.value()) 131 } 132 } 133 134 func (g *gauge) cachedReport() { 135 if atomic.SwapUint64(&g.updated, 0) == 1 { 136 g.cachedGauge.ReportGauge(g.value()) 137 } 138 } 139 140 func (g *gauge) snapshot() float64 { 141 return math.Float64frombits(atomic.LoadUint64(&g.curr)) 142 } 143 144 // NB(jra3): timers are a little special because they do no aggregate any data 145 // at the timer level. The reporter buffers may timer entries and periodically 146 // flushes. 147 type timer struct { 148 name string 149 tags map[string]string 150 reporter StatsReporter 151 cachedTimer CachedTimer 152 unreported timerValues 153 } 154 155 type timerValues struct { 156 sync.RWMutex 157 values []time.Duration 158 } 159 160 func newTimer( 161 name string, 162 tags map[string]string, 163 r StatsReporter, 164 cachedTimer CachedTimer, 165 ) *timer { 166 t := &timer{ 167 name: name, 168 tags: tags, 169 reporter: r, 170 cachedTimer: cachedTimer, 171 } 172 if r == nil { 173 t.reporter = &timerNoReporterSink{timer: t} 174 } 175 return t 176 } 177 178 func (t *timer) Record(interval time.Duration) { 179 if t.cachedTimer != nil { 180 t.cachedTimer.ReportTimer(interval) 181 } else { 182 t.reporter.ReportTimer(t.name, t.tags, interval) 183 } 184 } 185 186 func (t *timer) Start() Stopwatch { 187 return NewStopwatch(globalNow(), t) 188 } 189 190 func (t *timer) RecordStopwatch(stopwatchStart time.Time) { 191 d := globalNow().Sub(stopwatchStart) 192 t.Record(d) 193 } 194 195 func (t *timer) snapshot() []time.Duration { 196 t.unreported.RLock() 197 snap := make([]time.Duration, len(t.unreported.values)) 198 copy(snap, t.unreported.values) 199 t.unreported.RUnlock() 200 return snap 201 } 202 203 type timerNoReporterSink struct { 204 sync.RWMutex 205 timer *timer 206 } 207 208 func (r *timerNoReporterSink) ReportCounter( 209 name string, 210 tags map[string]string, 211 value int64, 212 ) { 213 } 214 215 func (r *timerNoReporterSink) ReportGauge( 216 name string, 217 tags map[string]string, 218 value float64, 219 ) { 220 } 221 222 func (r *timerNoReporterSink) ReportTimer( 223 name string, 224 tags map[string]string, 225 interval time.Duration, 226 ) { 227 r.timer.unreported.Lock() 228 r.timer.unreported.values = append(r.timer.unreported.values, interval) 229 r.timer.unreported.Unlock() 230 } 231 232 func (r *timerNoReporterSink) ReportHistogramValueSamples( 233 name string, 234 tags map[string]string, 235 buckets Buckets, 236 bucketLowerBound float64, 237 bucketUpperBound float64, 238 samples int64, 239 ) { 240 } 241 242 func (r *timerNoReporterSink) ReportHistogramDurationSamples( 243 name string, 244 tags map[string]string, 245 buckets Buckets, 246 bucketLowerBound time.Duration, 247 bucketUpperBound time.Duration, 248 samples int64, 249 ) { 250 } 251 252 func (r *timerNoReporterSink) Capabilities() Capabilities { 253 return capabilitiesReportingTagging 254 } 255 256 func (r *timerNoReporterSink) Flush() { 257 } 258 259 type sampleCounter struct { 260 counter *counter 261 cachedBucket CachedHistogramBucket 262 } 263 264 type histogram struct { 265 htype histogramType 266 name string 267 tags map[string]string 268 reporter StatsReporter 269 specification Buckets 270 buckets []histogramBucket 271 samples []sampleCounter 272 } 273 274 type histogramType int 275 276 const ( 277 valueHistogramType histogramType = iota 278 durationHistogramType 279 ) 280 281 func newHistogram( 282 htype histogramType, 283 name string, 284 tags map[string]string, 285 reporter StatsReporter, 286 storage bucketStorage, 287 cachedHistogram CachedHistogram, 288 ) *histogram { 289 h := &histogram{ 290 htype: htype, 291 name: name, 292 tags: tags, 293 reporter: reporter, 294 specification: storage.buckets, 295 buckets: storage.hbuckets, 296 samples: make([]sampleCounter, len(storage.hbuckets)), 297 } 298 299 for i := range h.samples { 300 h.samples[i].counter = newCounter(nil) 301 302 if cachedHistogram != nil { 303 switch htype { 304 case durationHistogramType: 305 h.samples[i].cachedBucket = cachedHistogram.DurationBucket( 306 durationLowerBound(storage.hbuckets, i), 307 storage.hbuckets[i].durationUpperBound, 308 ) 309 case valueHistogramType: 310 h.samples[i].cachedBucket = cachedHistogram.ValueBucket( 311 valueLowerBound(storage.hbuckets, i), 312 storage.hbuckets[i].valueUpperBound, 313 ) 314 } 315 } 316 } 317 318 return h 319 } 320 321 func (h *histogram) report(name string, tags map[string]string, r StatsReporter) { 322 for i := range h.buckets { 323 samples := h.samples[i].counter.value() 324 if samples == 0 { 325 continue 326 } 327 328 switch h.htype { 329 case valueHistogramType: 330 r.ReportHistogramValueSamples( 331 name, 332 tags, 333 h.specification, 334 valueLowerBound(h.buckets, i), 335 h.buckets[i].valueUpperBound, 336 samples, 337 ) 338 case durationHistogramType: 339 r.ReportHistogramDurationSamples( 340 name, 341 tags, 342 h.specification, 343 durationLowerBound(h.buckets, i), 344 h.buckets[i].durationUpperBound, 345 samples, 346 ) 347 } 348 } 349 } 350 351 func (h *histogram) cachedReport() { 352 for i := range h.buckets { 353 samples := h.samples[i].counter.value() 354 if samples == 0 { 355 continue 356 } 357 358 switch h.htype { 359 case valueHistogramType: 360 h.samples[i].cachedBucket.ReportSamples(samples) 361 case durationHistogramType: 362 h.samples[i].cachedBucket.ReportSamples(samples) 363 } 364 } 365 } 366 367 func (h *histogram) RecordValue(value float64) { 368 if h.htype != valueHistogramType { 369 return 370 } 371 372 // Find the highest inclusive of the bucket upper bound 373 // and emit directly to it. Since we use BucketPairs to derive 374 // buckets there will always be an inclusive bucket as 375 // we always have a math.MaxFloat64 bucket. 376 idx := sort.Search(len(h.buckets), func(i int) bool { 377 return h.buckets[i].valueUpperBound >= value 378 }) 379 h.samples[idx].counter.Inc(1) 380 } 381 382 func (h *histogram) RecordDuration(value time.Duration) { 383 if h.htype != durationHistogramType { 384 return 385 } 386 387 // Find the highest inclusive of the bucket upper bound 388 // and emit directly to it. Since we use BucketPairs to derive 389 // buckets there will always be an inclusive bucket as 390 // we always have a math.MaxInt64 bucket. 391 idx := sort.Search(len(h.buckets), func(i int) bool { 392 return h.buckets[i].durationUpperBound >= value 393 }) 394 h.samples[idx].counter.Inc(1) 395 } 396 397 func (h *histogram) Start() Stopwatch { 398 return NewStopwatch(globalNow(), h) 399 } 400 401 func (h *histogram) RecordStopwatch(stopwatchStart time.Time) { 402 d := globalNow().Sub(stopwatchStart) 403 h.RecordDuration(d) 404 } 405 406 func (h *histogram) snapshotValues() map[float64]int64 { 407 if h.htype != valueHistogramType { 408 return nil 409 } 410 411 vals := make(map[float64]int64, len(h.buckets)) 412 for i := range h.buckets { 413 vals[h.buckets[i].valueUpperBound] = h.samples[i].counter.snapshot() 414 } 415 416 return vals 417 } 418 419 func (h *histogram) snapshotDurations() map[time.Duration]int64 { 420 if h.htype != durationHistogramType { 421 return nil 422 } 423 424 durations := make(map[time.Duration]int64, len(h.buckets)) 425 for i := range h.buckets { 426 durations[h.buckets[i].durationUpperBound] = h.samples[i].counter.snapshot() 427 } 428 429 return durations 430 } 431 432 type histogramBucket struct { 433 valueUpperBound float64 434 durationUpperBound time.Duration 435 } 436 437 func durationLowerBound(buckets []histogramBucket, i int) time.Duration { 438 if i <= 0 { 439 return time.Duration(math.MinInt64) 440 } 441 return buckets[i-1].durationUpperBound 442 } 443 444 func valueLowerBound(buckets []histogramBucket, i int) float64 { 445 if i <= 0 { 446 return -math.MaxFloat64 447 } 448 return buckets[i-1].valueUpperBound 449 } 450 451 type bucketStorage struct { 452 buckets Buckets 453 hbuckets []histogramBucket 454 } 455 456 func newBucketStorage( 457 htype histogramType, 458 buckets Buckets, 459 ) bucketStorage { 460 var ( 461 pairs = BucketPairs(buckets) 462 storage = bucketStorage{ 463 buckets: buckets, 464 hbuckets: make([]histogramBucket, 0, len(pairs)), 465 } 466 ) 467 468 for _, pair := range pairs { 469 storage.hbuckets = append(storage.hbuckets, histogramBucket{ 470 valueUpperBound: pair.UpperBoundValue(), 471 durationUpperBound: pair.UpperBoundDuration(), 472 }) 473 } 474 475 return storage 476 } 477 478 type bucketCache struct { 479 mtx sync.RWMutex 480 cache map[uint64]bucketStorage 481 } 482 483 func newBucketCache() *bucketCache { 484 return &bucketCache{ 485 cache: make(map[uint64]bucketStorage), 486 } 487 } 488 489 func (c *bucketCache) Get( 490 htype histogramType, 491 buckets Buckets, 492 ) bucketStorage { 493 id := getBucketsIdentity(buckets) 494 495 c.mtx.RLock() 496 storage, ok := c.cache[id] 497 if !ok { 498 c.mtx.RUnlock() 499 c.mtx.Lock() 500 storage = newBucketStorage(htype, buckets) 501 c.cache[id] = storage 502 c.mtx.Unlock() 503 } else { 504 c.mtx.RUnlock() 505 if !bucketsEqual(buckets, storage.buckets) { 506 storage = newBucketStorage(htype, buckets) 507 } 508 } 509 510 return storage 511 } 512 513 // NullStatsReporter is an implementation of StatsReporter than simply does nothing. 514 var NullStatsReporter StatsReporter = nullStatsReporter{} 515 516 func (r nullStatsReporter) ReportCounter(name string, tags map[string]string, value int64) { 517 } 518 519 func (r nullStatsReporter) ReportGauge(name string, tags map[string]string, value float64) { 520 } 521 522 func (r nullStatsReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) { 523 } 524 525 func (r nullStatsReporter) ReportHistogramValueSamples( 526 name string, 527 tags map[string]string, 528 buckets Buckets, 529 bucketLowerBound, 530 bucketUpperBound float64, 531 samples int64, 532 ) { 533 } 534 535 func (r nullStatsReporter) ReportHistogramDurationSamples( 536 name string, 537 tags map[string]string, 538 buckets Buckets, 539 bucketLowerBound, 540 bucketUpperBound time.Duration, 541 samples int64, 542 ) { 543 } 544 545 func (r nullStatsReporter) Capabilities() Capabilities { 546 return capabilitiesNone 547 } 548 549 func (r nullStatsReporter) Flush() { 550 } 551 552 type nullStatsReporter struct{} 553 554 func getBucketsIdentity(buckets Buckets) uint64 { 555 switch b := buckets.(type) { 556 case DurationBuckets: 557 return identity.Durations(b.AsDurations()) 558 case ValueBuckets: 559 return identity.Float64s(b.AsValues()) 560 default: 561 panic(fmt.Sprintf("unexpected bucket type: %T", b)) 562 } 563 }