endpoint_counter.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. // Unless explicitly stated otherwise all files in this repository are licensed
  2. // under the Apache License Version 2.0.
  3. // This product includes software developed at Datadog (https://www.datadoghq.com/).
  4. // Copyright 2023 Datadog, Inc.
  5. package traceprof
  6. import (
  7. "sync"
  8. "sync/atomic"
  9. )
  10. // globalEndpointCounter is shared between the profiler and the tracer.
  11. var globalEndpointCounter = (func() *EndpointCounter {
  12. // Create endpoint counter with arbitrary limit.
  13. // The pathological edge case would be a service with a high rate (10k/s) of
  14. // short (100ms) spans with unique endpoints (resource names). Over a 60s
  15. // period this would grow the map to 600k items which may cause noticable
  16. // memory, GC overhead and lock contention overhead. The pprof endpoint
  17. // labels are less problematic since there will only be 1000 spans in-flight
  18. // on average. Using a limit of 1000 will result in a similar overhead of
  19. // this features compared to the pprof labels. It also seems like a
  20. // reasonable upper bound for the number of endpoints a normal application
  21. // may service in a 60s period.
  22. ec := NewEndpointCounter(1000)
  23. // Disabled by default ensures almost-zero overhead for tracing users that
  24. // don't have the profiler turned on.
  25. ec.SetEnabled(false)
  26. return ec
  27. })()
  28. // GlobalEndpointCounter returns the endpoint counter that is shared between
  29. // tracing and profiling to support the unit of work feature.
  30. func GlobalEndpointCounter() *EndpointCounter {
  31. return globalEndpointCounter
  32. }
  33. // NewEndpointCounter returns a new NewEndpointCounter that will track hit
  34. // counts for up to limit endpoints. A limit of <= 0 indicates no limit.
  35. func NewEndpointCounter(limit int) *EndpointCounter {
  36. return &EndpointCounter{enabled: 1, limit: limit, counts: map[string]uint64{}}
  37. }
  38. // EndpointCounter counts hits per endpoint.
  39. //
  40. // TODO: This is a naive implementation with poor performance, e.g. 125ns/op in
  41. // BenchmarkEndpointCounter on M1. We can do 10-20x better with something more
  42. // complicated [1]. This will be done in a follow-up PR.
  43. // [1] https://github.com/felixge/countermap/blob/main/xsync_map_counter_map.go
  44. type EndpointCounter struct {
  45. enabled uint64
  46. mu sync.Mutex
  47. counts map[string]uint64
  48. limit int
  49. }
  50. // SetEnabled changes if endpoint counting is enabled or not. The previous
  51. // value is returned.
  52. func (e *EndpointCounter) SetEnabled(enabled bool) bool {
  53. oldVal := atomic.SwapUint64(&e.enabled, boolToUint64(enabled))
  54. return oldVal == 1
  55. }
  56. // Inc increments the hit counter for the given endpoint by 1. If endpoint
  57. // counting is disabled, this method does nothing and is almost zero-cost.
  58. func (e *EndpointCounter) Inc(endpoint string) {
  59. // Fast-path return if endpoint counter is disabled.
  60. if atomic.LoadUint64(&e.enabled) == 0 {
  61. return
  62. }
  63. // Acquire lock until func returns
  64. e.mu.Lock()
  65. defer e.mu.Unlock()
  66. // Don't add another endpoint to the map if the limit is reached. See
  67. // globalEndpointCounter comment.
  68. count, ok := e.counts[endpoint]
  69. if !ok && e.limit > 0 && len(e.counts) >= e.limit {
  70. return
  71. }
  72. // Increment the endpoint count
  73. e.counts[endpoint] = count + 1
  74. }
  75. // GetAndReset returns the hit counts for all endpoints and resets their counts
  76. // back to 0.
  77. func (e *EndpointCounter) GetAndReset() map[string]uint64 {
  78. // Acquire lock until func returns
  79. e.mu.Lock()
  80. defer e.mu.Unlock()
  81. // Return current counts and reset internal map.
  82. counts := e.counts
  83. e.counts = make(map[string]uint64)
  84. return counts
  85. }
  86. // boolToUint64 converts b to 0 if false or 1 if true.
  87. func boolToUint64(b bool) uint64 {
  88. if b {
  89. return 1
  90. }
  91. return 0
  92. }