metrics: hardcode lazy metric construction codepath

motivation is to remove the dependency on reflect.Call

Throughout the codebase, GetOrRegister() is mostly used with lazy
construction while Register() is strictly used with eagerly constructed
metric objects. So it makes sense to isolate the lazy construction to
GetOrRegister() and make caller decide on which function makes sense
for them.
This commit is contained in:
Ömer Faruk IRMAK 2025-06-04 13:02:03 +03:00
parent 23f07d8c93
commit 04845621bc
12 changed files with 30 additions and 71 deletions

View file

@ -7,10 +7,7 @@ import (
// GetOrRegisterCounter returns an existing Counter or constructs and registers // GetOrRegisterCounter returns an existing Counter or constructs and registers
// a new Counter. // a new Counter.
func GetOrRegisterCounter(name string, r Registry) *Counter { func GetOrRegisterCounter(name string, r Registry) *Counter {
if r == nil { return GetOrRegister(name, NewCounter, r).(*Counter)
r = DefaultRegistry
}
return r.GetOrRegister(name, NewCounter).(*Counter)
} }
// NewCounter constructs a new Counter. // NewCounter constructs a new Counter.

View file

@ -8,10 +8,7 @@ import (
// GetOrRegisterCounterFloat64 returns an existing *CounterFloat64 or constructs and registers // GetOrRegisterCounterFloat64 returns an existing *CounterFloat64 or constructs and registers
// a new CounterFloat64. // a new CounterFloat64.
func GetOrRegisterCounterFloat64(name string, r Registry) *CounterFloat64 { func GetOrRegisterCounterFloat64(name string, r Registry) *CounterFloat64 {
if nil == r { return GetOrRegister(name, NewCounterFloat64, r).(*CounterFloat64)
r = DefaultRegistry
}
return r.GetOrRegister(name, NewCounterFloat64).(*CounterFloat64)
} }
// NewCounterFloat64 constructs a new CounterFloat64. // NewCounterFloat64 constructs a new CounterFloat64.

View file

@ -11,10 +11,7 @@ func (g GaugeSnapshot) Value() int64 { return int64(g) }
// GetOrRegisterGauge returns an existing Gauge or constructs and registers a // GetOrRegisterGauge returns an existing Gauge or constructs and registers a
// new Gauge. // new Gauge.
func GetOrRegisterGauge(name string, r Registry) *Gauge { func GetOrRegisterGauge(name string, r Registry) *Gauge {
if r == nil { return GetOrRegister(name, NewGauge, r).(*Gauge)
r = DefaultRegistry
}
return r.GetOrRegister(name, NewGauge).(*Gauge)
} }
// NewGauge constructs a new Gauge. // NewGauge constructs a new Gauge.

View file

@ -8,10 +8,7 @@ import (
// GetOrRegisterGaugeFloat64 returns an existing GaugeFloat64 or constructs and registers a // GetOrRegisterGaugeFloat64 returns an existing GaugeFloat64 or constructs and registers a
// new GaugeFloat64. // new GaugeFloat64.
func GetOrRegisterGaugeFloat64(name string, r Registry) *GaugeFloat64 { func GetOrRegisterGaugeFloat64(name string, r Registry) *GaugeFloat64 {
if nil == r { return GetOrRegister(name, NewGaugeFloat64, r).(*GaugeFloat64)
r = DefaultRegistry
}
return r.GetOrRegister(name, NewGaugeFloat64()).(*GaugeFloat64)
} }
// GaugeFloat64Snapshot is a read-only copy of a GaugeFloat64. // GaugeFloat64Snapshot is a read-only copy of a GaugeFloat64.

View file

@ -16,10 +16,7 @@ func (val GaugeInfoValue) String() string {
// GetOrRegisterGaugeInfo returns an existing GaugeInfo or constructs and registers a // GetOrRegisterGaugeInfo returns an existing GaugeInfo or constructs and registers a
// new GaugeInfo. // new GaugeInfo.
func GetOrRegisterGaugeInfo(name string, r Registry) *GaugeInfo { func GetOrRegisterGaugeInfo(name string, r Registry) *GaugeInfo {
if nil == r { return GetOrRegister(name, NewGaugeInfo, r).(*GaugeInfo)
r = DefaultRegistry
}
return r.GetOrRegister(name, NewGaugeInfo()).(*GaugeInfo)
} }
// NewGaugeInfo constructs a new GaugeInfo. // NewGaugeInfo constructs a new GaugeInfo.

View file

@ -23,19 +23,13 @@ type Histogram interface {
// GetOrRegisterHistogram returns an existing Histogram or constructs and // GetOrRegisterHistogram returns an existing Histogram or constructs and
// registers a new StandardHistogram. // registers a new StandardHistogram.
func GetOrRegisterHistogram(name string, r Registry, s Sample) Histogram { func GetOrRegisterHistogram(name string, r Registry, s Sample) Histogram {
if nil == r { return GetOrRegister(name, func() Histogram { return NewHistogram(s) }, r).(Histogram)
r = DefaultRegistry
}
return r.GetOrRegister(name, func() Histogram { return NewHistogram(s) }).(Histogram)
} }
// GetOrRegisterHistogramLazy returns an existing Histogram or constructs and // GetOrRegisterHistogramLazy returns an existing Histogram or constructs and
// registers a new StandardHistogram. // registers a new StandardHistogram.
func GetOrRegisterHistogramLazy(name string, r Registry, s func() Sample) Histogram { func GetOrRegisterHistogramLazy(name string, r Registry, s func() Sample) Histogram {
if nil == r { return GetOrRegister(name, func() Histogram { return NewHistogram(s()) }, r).(Histogram)
r = DefaultRegistry
}
return r.GetOrRegister(name, func() Histogram { return NewHistogram(s()) }).(Histogram)
} }
// NewHistogram constructs a new StandardHistogram from a Sample. // NewHistogram constructs a new StandardHistogram from a Sample.

View file

@ -12,10 +12,7 @@ import (
// Be sure to unregister the meter from the registry once it is of no use to // Be sure to unregister the meter from the registry once it is of no use to
// allow for garbage collection. // allow for garbage collection.
func GetOrRegisterMeter(name string, r Registry) *Meter { func GetOrRegisterMeter(name string, r Registry) *Meter {
if r == nil { return GetOrRegister(name, NewMeter, r).(*Meter)
r = DefaultRegistry
}
return r.GetOrRegister(name, NewMeter).(*Meter)
} }
// NewMeter constructs a new Meter and launches a goroutine. // NewMeter constructs a new Meter and launches a goroutine.

View file

@ -3,7 +3,6 @@ package metrics
import ( import (
"errors" "errors"
"fmt" "fmt"
"reflect"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -30,10 +29,9 @@ type Registry interface {
// GetAll metrics in the Registry. // GetAll metrics in the Registry.
GetAll() map[string]map[string]interface{} GetAll() map[string]map[string]interface{}
// GetOrRegister gets an existing metric or registers the given one. // GetOrRegister returns an existing metric or registers the one returned
// The interface can be the metric to register if not found in registry, // by the given constructor.
// or a function returning the metric for lazy instantiation. GetOrRegister(string, func() interface{}) interface{}
GetOrRegister(string, interface{}) interface{}
// Register the given metric under the given name. // Register the given metric under the given name.
Register(string, interface{}) error Register(string, interface{}) error
@ -95,19 +93,13 @@ func (r *StandardRegistry) Get(name string) interface{} {
// alternative to calling Get and Register on failure. // alternative to calling Get and Register on failure.
// The interface can be the metric to register if not found in registry, // The interface can be the metric to register if not found in registry,
// or a function returning the metric for lazy instantiation. // or a function returning the metric for lazy instantiation.
func (r *StandardRegistry) GetOrRegister(name string, i interface{}) interface{} { func (r *StandardRegistry) GetOrRegister(name string, ctor func() interface{}) interface{} {
// fast path // fast path
cached, ok := r.metrics.Load(name) cached, ok := r.metrics.Load(name)
if ok { if ok {
return cached return cached
} }
if v := reflect.ValueOf(i); v.Kind() == reflect.Func { item, _, _ := r.loadOrRegister(name, ctor())
i = v.Call(nil)[0].Interface()
}
item, _, ok := r.loadOrRegister(name, i)
if !ok {
return i
}
return item return item
} }
@ -120,9 +112,6 @@ func (r *StandardRegistry) Register(name string, i interface{}) error {
return fmt.Errorf("%w: %v", ErrDuplicateMetric, name) return fmt.Errorf("%w: %v", ErrDuplicateMetric, name)
} }
if v := reflect.ValueOf(i); v.Kind() == reflect.Func {
i = v.Call(nil)[0].Interface()
}
_, loaded, _ := r.loadOrRegister(name, i) _, loaded, _ := r.loadOrRegister(name, i)
if loaded { if loaded {
return fmt.Errorf("%w: %v", ErrDuplicateMetric, name) return fmt.Errorf("%w: %v", ErrDuplicateMetric, name)
@ -295,9 +284,9 @@ func (r *PrefixedRegistry) Get(name string) interface{} {
// GetOrRegister gets an existing metric or registers the given one. // GetOrRegister gets an existing metric or registers the given one.
// The interface can be the metric to register if not found in registry, // The interface can be the metric to register if not found in registry,
// or a function returning the metric for lazy instantiation. // or a function returning the metric for lazy instantiation.
func (r *PrefixedRegistry) GetOrRegister(name string, metric interface{}) interface{} { func (r *PrefixedRegistry) GetOrRegister(name string, ctor func() interface{}) interface{} {
realName := r.prefix + name realName := r.prefix + name
return r.underlying.GetOrRegister(realName, metric) return r.underlying.GetOrRegister(realName, ctor)
} }
// Register the given metric under the given name. The name will be prefixed. // Register the given metric under the given name. The name will be prefixed.
@ -338,8 +327,11 @@ func Get(name string) interface{} {
// GetOrRegister gets an existing metric or creates and registers a new one. Threadsafe // GetOrRegister gets an existing metric or creates and registers a new one. Threadsafe
// alternative to calling Get and Register on failure. // alternative to calling Get and Register on failure.
func GetOrRegister(name string, i interface{}) interface{} { func GetOrRegister[T any](name string, ctor func() T, r Registry) interface{} {
return DefaultRegistry.GetOrRegister(name, i) if nil == r {
r = DefaultRegistry
}
return r.GetOrRegister(name, func() any { return ctor() })
} }
// Register the given metric under the given name. Returns a ErrDuplicateMetric // Register the given metric under the given name. Returns a ErrDuplicateMetric

View file

@ -30,7 +30,7 @@ func benchmarkRegistryGetOrRegisterParallel(b *testing.B, amount int) {
wg.Add(1) wg.Add(1)
go func() { go func() {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
r.GetOrRegister("foo", NewMeter) GetOrRegister("foo", NewMeter, r)
} }
wg.Done() wg.Done()
}() }()
@ -98,8 +98,8 @@ func TestRegistryGetOrRegister(t *testing.T) {
r := NewRegistry() r := NewRegistry()
// First metric wins with GetOrRegister // First metric wins with GetOrRegister
_ = r.GetOrRegister("foo", NewCounter()) _ = GetOrRegister("foo", NewCounter, r)
m := r.GetOrRegister("foo", NewGauge()) m := GetOrRegister("foo", NewGauge, r)
if _, ok := m.(*Counter); !ok { if _, ok := m.(*Counter); !ok {
t.Fatal(m) t.Fatal(m)
} }
@ -123,8 +123,8 @@ func TestRegistryGetOrRegisterWithLazyInstantiation(t *testing.T) {
r := NewRegistry() r := NewRegistry()
// First metric wins with GetOrRegister // First metric wins with GetOrRegister
_ = r.GetOrRegister("foo", NewCounter) _ = GetOrRegister("foo", NewCounter, r)
m := r.GetOrRegister("foo", NewGauge) m := GetOrRegister("foo", NewGauge, r)
if _, ok := m.(*Counter); !ok { if _, ok := m.(*Counter); !ok {
t.Fatal(m) t.Fatal(m)
} }
@ -165,7 +165,7 @@ func TestPrefixedChildRegistryGetOrRegister(t *testing.T) {
r := NewRegistry() r := NewRegistry()
pr := NewPrefixedChildRegistry(r, "prefix.") pr := NewPrefixedChildRegistry(r, "prefix.")
_ = pr.GetOrRegister("foo", NewCounter()) _ = GetOrRegister("foo", NewCounter, pr)
i := 0 i := 0
r.Each(func(name string, m interface{}) { r.Each(func(name string, m interface{}) {
@ -182,7 +182,7 @@ func TestPrefixedChildRegistryGetOrRegister(t *testing.T) {
func TestPrefixedRegistryGetOrRegister(t *testing.T) { func TestPrefixedRegistryGetOrRegister(t *testing.T) {
r := NewPrefixedRegistry("prefix.") r := NewPrefixedRegistry("prefix.")
_ = r.GetOrRegister("foo", NewCounter()) _ = GetOrRegister("foo", NewCounter, r)
i := 0 i := 0
r.Each(func(name string, m interface{}) { r.Each(func(name string, m interface{}) {

View file

@ -8,10 +8,7 @@ import (
// GetOrRegisterResettingTimer returns an existing ResettingTimer or constructs and registers a // GetOrRegisterResettingTimer returns an existing ResettingTimer or constructs and registers a
// new ResettingTimer. // new ResettingTimer.
func GetOrRegisterResettingTimer(name string, r Registry) *ResettingTimer { func GetOrRegisterResettingTimer(name string, r Registry) *ResettingTimer {
if nil == r { return GetOrRegister(name, NewResettingTimer, r).(*ResettingTimer)
r = DefaultRegistry
}
return r.GetOrRegister(name, NewResettingTimer).(*ResettingTimer)
} }
// NewRegisteredResettingTimer constructs and registers a new ResettingTimer. // NewRegisteredResettingTimer constructs and registers a new ResettingTimer.

View file

@ -8,11 +8,8 @@ import (
) )
func getOrRegisterRuntimeHistogram(name string, scale float64, r Registry) *runtimeHistogram { func getOrRegisterRuntimeHistogram(name string, scale float64, r Registry) *runtimeHistogram {
if r == nil {
r = DefaultRegistry
}
constructor := func() Histogram { return newRuntimeHistogram(scale) } constructor := func() Histogram { return newRuntimeHistogram(scale) }
return r.GetOrRegister(name, constructor).(*runtimeHistogram) return GetOrRegister(name, constructor, r).(*runtimeHistogram)
} }
// runtimeHistogram wraps a runtime/metrics histogram. // runtimeHistogram wraps a runtime/metrics histogram.

View file

@ -10,10 +10,7 @@ import (
// Be sure to unregister the meter from the registry once it is of no use to // Be sure to unregister the meter from the registry once it is of no use to
// allow for garbage collection. // allow for garbage collection.
func GetOrRegisterTimer(name string, r Registry) *Timer { func GetOrRegisterTimer(name string, r Registry) *Timer {
if nil == r { return GetOrRegister(name, NewTimer, r).(*Timer)
r = DefaultRegistry
}
return r.GetOrRegister(name, NewTimer).(*Timer)
} }
// NewCustomTimer constructs a new Timer from a Histogram and a Meter. // NewCustomTimer constructs a new Timer from a Histogram and a Meter.