Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Counter faster for simple Inc or Add with an integer #367

Merged
merged 4 commits into from
Jan 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions prometheus/counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ package prometheus

import (
"errors"
"math"
"sync/atomic"

dto "github.com/prometheus/client_model/go"
)

// Counter is a Metric that represents a single numerical value that only ever
Expand Down Expand Up @@ -42,27 +46,73 @@ type Counter interface {
type CounterOpts Opts

// NewCounter creates a new Counter based on the provided CounterOpts.
//
// The returned implementation tracks the counter value in two separate
// variables, a float64 and a uint64. The latter is used to track calls of the
// Inc method and calls of the Add method with a value that can be represented
// as a uint64. This allows atomic increments of the counter with optimal
// performance. (It is common to have an Inc call in very hot execution paths.)
// Both internal tracking values are added up in the Write method. This has to
// be taken into account when it comes to precision and overflow behavior.
func NewCounter(opts CounterOpts) Counter {
desc := NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
)
result := &counter{value: value{desc: desc, valType: CounterValue, labelPairs: desc.constLabelPairs}}
result := &counter{desc: desc, labelPairs: desc.constLabelPairs}
result.init(result) // Init self-collection.
return result
}

type counter struct {
value
// valBits contains the bits of the represented float64 value, while
// valInt stores values that are exact integers. Both have to go first
// in the struct to guarantee alignment for atomic operations.
// http://golang.org/pkg/sync/atomic/#pkg-note-BUG
valBits uint64
valInt uint64

selfCollector
desc *Desc

labelPairs []*dto.LabelPair
}

func (c *counter) Desc() *Desc {
return c.desc
}

func (c *counter) Add(v float64) {
if v < 0 {
panic(errors.New("counter cannot decrease in value"))
}
c.value.Add(v)
ival := uint64(v)
if float64(ival) == v {
atomic.AddUint64(&c.valInt, ival)
return
}

for {
oldBits := atomic.LoadUint64(&c.valBits)
newBits := math.Float64bits(math.Float64frombits(oldBits) + v)
if atomic.CompareAndSwapUint64(&c.valBits, oldBits, newBits) {
return
}
}
}

func (c *counter) Inc() {
atomic.AddUint64(&c.valInt, 1)
}

func (c *counter) Write(out *dto.Metric) error {
fval := math.Float64frombits(atomic.LoadUint64(&c.valBits))
ival := atomic.LoadUint64(&c.valInt)
val := fval + float64(ival)

return populateMetric(CounterValue, val, c.labelPairs, out)
}

// CounterVec is a Collector that bundles a set of Counters that all share the
Expand All @@ -85,11 +135,10 @@ func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
)
return &CounterVec{
metricVec: newMetricVec(desc, func(lvs ...string) Metric {
result := &counter{value: value{
desc: desc,
valType: CounterValue,
labelPairs: makeLabelPairs(desc, lvs),
}}
if len(lvs) != len(desc.variableLabels) {
panic(errInconsistentCardinality)
}
result := &counter{desc: desc, labelPairs: makeLabelPairs(desc, lvs)}
result.init(result) // Init self-collection.
return result
}),
Expand Down
104 changes: 101 additions & 3 deletions prometheus/counter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,27 @@ func TestCounterAdd(t *testing.T) {
ConstLabels: Labels{"a": "1", "b": "2"},
}).(*counter)
counter.Inc()
if expected, got := 1., math.Float64frombits(counter.valBits); expected != got {
if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got {
t.Errorf("Expected %f, got %f.", expected, got)
}
if expected, got := uint64(1), counter.valInt; expected != got {
t.Errorf("Expected %d, got %d.", expected, got)
}
counter.Add(42)
if expected, got := 43., math.Float64frombits(counter.valBits); expected != got {
if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got {
t.Errorf("Expected %f, got %f.", expected, got)
}
if expected, got := uint64(43), counter.valInt; expected != got {
t.Errorf("Expected %d, got %d.", expected, got)
}

counter.Add(24.42)
if expected, got := 24.42, math.Float64frombits(counter.valBits); expected != got {
t.Errorf("Expected %f, got %f.", expected, got)
}
if expected, got := uint64(43), counter.valInt; expected != got {
t.Errorf("Expected %d, got %d.", expected, got)
}

if expected, got := "counter cannot decrease in value", decreaseCounter(counter).Error(); expected != got {
t.Errorf("Expected error %q, got %q.", expected, got)
Expand All @@ -43,7 +57,7 @@ func TestCounterAdd(t *testing.T) {
m := &dto.Metric{}
counter.Write(m)

if expected, got := `label:<name:"a" value:"1" > label:<name:"b" value:"2" > counter:<value:43 > `, m.String(); expected != got {
if expected, got := `label:<name:"a" value:"1" > label:<name:"b" value:"2" > counter:<value:67.42 > `, m.String(); expected != got {
t.Errorf("expected %q, got %q", expected, got)
}
}
Expand Down Expand Up @@ -112,3 +126,87 @@ func expectPanic(t *testing.T, op func(), errorMsg string) {

op()
}

func TestCounterAddInf(t *testing.T) {
counter := NewCounter(CounterOpts{
Name: "test",
Help: "test help",
}).(*counter)

counter.Inc()
if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got {
t.Errorf("Expected %f, got %f.", expected, got)
}
if expected, got := uint64(1), counter.valInt; expected != got {
t.Errorf("Expected %d, got %d.", expected, got)
}

counter.Add(math.Inf(1))
if expected, got := math.Inf(1), math.Float64frombits(counter.valBits); expected != got {
t.Errorf("valBits expected %f, got %f.", expected, got)
}
if expected, got := uint64(1), counter.valInt; expected != got {
t.Errorf("valInts expected %d, got %d.", expected, got)
}

counter.Inc()
if expected, got := math.Inf(1), math.Float64frombits(counter.valBits); expected != got {
t.Errorf("Expected %f, got %f.", expected, got)
}
if expected, got := uint64(2), counter.valInt; expected != got {
t.Errorf("Expected %d, got %d.", expected, got)
}

m := &dto.Metric{}
counter.Write(m)

if expected, got := `counter:<value:inf > `, m.String(); expected != got {
t.Errorf("expected %q, got %q", expected, got)
}
}

func TestCounterAddLarge(t *testing.T) {
counter := NewCounter(CounterOpts{
Name: "test",
Help: "test help",
}).(*counter)

// large overflows the underlying type and should therefore be stored in valBits.
large := float64(math.MaxUint64 + 1)
counter.Add(large)
if expected, got := large, math.Float64frombits(counter.valBits); expected != got {
t.Errorf("valBits expected %f, got %f.", expected, got)
}
if expected, got := uint64(0), counter.valInt; expected != got {
t.Errorf("valInts expected %d, got %d.", expected, got)
}

m := &dto.Metric{}
counter.Write(m)

if expected, got := fmt.Sprintf("counter:<value:%0.16e > ", large), m.String(); expected != got {
t.Errorf("expected %q, got %q", expected, got)
}
}

func TestCounterAddSmall(t *testing.T) {
counter := NewCounter(CounterOpts{
Name: "test",
Help: "test help",
}).(*counter)
small := 0.000000000001
counter.Add(small)
if expected, got := small, math.Float64frombits(counter.valBits); expected != got {
t.Errorf("valBits expected %f, got %f.", expected, got)
}
if expected, got := uint64(0), counter.valInt; expected != got {
t.Errorf("valInts expected %d, got %d.", expected, got)
}

m := &dto.Metric{}
counter.Write(m)

if expected, got := fmt.Sprintf("counter:<value:%0.0e > ", small), m.String(); expected != got {
t.Errorf("expected %q, got %q", expected, got)
}
}
80 changes: 77 additions & 3 deletions prometheus/gauge.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@

package prometheus

import (
"math"
"sync/atomic"
"time"

dto "github.com/prometheus/client_model/go"
)

// Gauge is a Metric that represents a single numerical value that can
// arbitrarily go up and down.
//
Expand Down Expand Up @@ -48,13 +56,74 @@ type Gauge interface {
type GaugeOpts Opts

// NewGauge creates a new Gauge based on the provided GaugeOpts.
//
// The returned implementation is optimized for a fast Set method. If you have a
// choice for managing the value of a Gauge via Set vs. Inc/Dec/Add/Sub, pick
// the former. For example, the Inc method of the returned Gauge is slower than
// the Inc method of a Counter returned by NewCounter. This matches the typical
// scenarios for Gauges and Counters, where the former tends to be Set-heavy and
// the latter Inc-heavy.
func NewGauge(opts GaugeOpts) Gauge {
return newValue(NewDesc(
desc := NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
), GaugeValue, 0)
)
result := &gauge{desc: desc, labelPairs: desc.constLabelPairs}
result.init(result) // Init self-collection.
return result
}

type gauge struct {
// valBits contains the bits of the represented float64 value. It has
// to go first in the struct to guarantee alignment for atomic
// operations. http://golang.org/pkg/sync/atomic/#pkg-note-BUG
valBits uint64

selfCollector

desc *Desc
labelPairs []*dto.LabelPair
}

func (g *gauge) Desc() *Desc {
return g.desc
}

func (g *gauge) Set(val float64) {
atomic.StoreUint64(&g.valBits, math.Float64bits(val))
}

func (g *gauge) SetToCurrentTime() {
g.Set(float64(time.Now().UnixNano()) / 1e9)
}

func (g *gauge) Inc() {
g.Add(1)
}

func (g *gauge) Dec() {
g.Add(-1)
}

func (g *gauge) Add(val float64) {
for {
oldBits := atomic.LoadUint64(&g.valBits)
newBits := math.Float64bits(math.Float64frombits(oldBits) + val)
if atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) {
return
}
}
}

func (g *gauge) Sub(val float64) {
g.Add(val * -1)
}

func (g *gauge) Write(out *dto.Metric) error {
val := math.Float64frombits(atomic.LoadUint64(&g.valBits))
return populateMetric(GaugeValue, val, g.labelPairs, out)
}

// GaugeVec is a Collector that bundles a set of Gauges that all share the same
Expand All @@ -77,7 +146,12 @@ func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
)
return &GaugeVec{
metricVec: newMetricVec(desc, func(lvs ...string) Metric {
return newValue(desc, GaugeValue, 0, lvs...)
if len(lvs) != len(desc.variableLabels) {
panic(errInconsistentCardinality)
}
result := &gauge{desc: desc, labelPairs: makeLabelPairs(desc, lvs)}
result.init(result) // Init self-collection.
return result
}),
}
}
Expand Down
4 changes: 2 additions & 2 deletions prometheus/gauge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestGaugeConcurrency(t *testing.T) {
}
start.Done()

if expected, got := <-result, math.Float64frombits(gge.(*value).valBits); math.Abs(expected-got) > 0.000001 {
if expected, got := <-result, math.Float64frombits(gge.(*gauge).valBits); math.Abs(expected-got) > 0.000001 {
t.Fatalf("expected approx. %f, got %f", expected, got)
return false
}
Expand Down Expand Up @@ -147,7 +147,7 @@ func TestGaugeVecConcurrency(t *testing.T) {
start.Done()

for i := range sStreams {
if expected, got := <-results[i], math.Float64frombits(gge.WithLabelValues(string('A'+i)).(*value).valBits); math.Abs(expected-got) > 0.000001 {
if expected, got := <-results[i], math.Float64frombits(gge.WithLabelValues(string('A'+i)).(*gauge).valBits); math.Abs(expected-got) > 0.000001 {
t.Fatalf("expected approx. %f, got %f", expected, got)
return false
}
Expand Down
Loading