Skip to content

Commit 06998a2

Browse files
committed
prometheus: introduce an experimental sharded counter
1 parent 20355eb commit 06998a2

4 files changed

Lines changed: 49 additions & 38 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/prometheus/client_golang
33
go 1.25.0
44

55
require (
6+
argc.dev/goexp v0.0.0-20260710001111-8c1f88208172
67
github.com/beorn7/perks v1.0.1
78
github.com/cespare/xxhash/v2 v2.3.0
89
github.com/google/go-cmp v0.7.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
argc.dev/goexp v0.0.0-20260710001111-8c1f88208172 h1:PET7L5h+6EjqOnYjG2Z5sxuMHx2NFmn4OjA/ls/ywEI=
2+
argc.dev/goexp v0.0.0-20260710001111-8c1f88208172/go.mod h1:5KW5TTv68+fSFL3H+qywujHM1tkwM9JWcc2+ZtT2Vrc=
13
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
24
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
35
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=

prometheus/counter.go

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ package prometheus
1515

1616
import (
1717
"errors"
18-
"math"
1918
"sync/atomic"
2019
"time"
2120

21+
xsync "argc.dev/goexp/sync"
2222
dto "github.com/prometheus/client_model/go"
2323
"google.golang.org/protobuf/types/known/timestamppb"
2424
)
@@ -95,19 +95,29 @@ func NewCounter(opts CounterOpts) Counter {
9595
if opts.now == nil {
9696
opts.now = time.Now
9797
}
98-
result := &counter{desc: desc, labelPairs: desc.constLabelPairs, now: opts.now}
98+
result := &counter{
99+
desc: desc,
100+
labelPairs: desc.constLabelPairs,
101+
valBits: xsync.NewCounter[float64](),
102+
valInt: xsync.NewCounter[int64](),
103+
now: opts.now,
104+
}
99105
result.init(result) // Init self-collection.
100106
result.createdTs = timestamppb.New(opts.now())
101107
return result
102108
}
103109

104110
type counter struct {
105-
// valBits contains the bits of the represented float64 value, while
106-
// valInt stores values that are exact integers. Both have to go first
107-
// in the struct to guarantee alignment for atomic operations.
108-
// http://golang.org/pkg/sync/atomic/#pkg-note-BUG
109-
valBits uint64
110-
valInt uint64
111+
// valInt accumulates the exact integer part of the counter (Inc calls
112+
// and Add calls with integral values), while valBits accumulates values
113+
// that cannot be represented as an int64. Both are per-P sharded
114+
// counters (see argc.dev/goexp/sync): increments happen on hot paths and
115+
// are made cheap and contention-free by striping them across processors,
116+
// whereas reads (only at collection time) sum the shards. The split
117+
// between integer and floating-point tracking is kept for precision, and
118+
// the two are added up in the get method.
119+
valBits *xsync.Counter[float64]
120+
valInt *xsync.Counter[int64]
111121

112122
selfCollector
113123
desc *Desc
@@ -129,19 +139,13 @@ func (c *counter) Add(v float64) {
129139
panic(errors.New("counter cannot decrease in value"))
130140
}
131141

132-
ival := uint64(v)
142+
ival := int64(v)
133143
if float64(ival) == v {
134-
atomic.AddUint64(&c.valInt, ival)
144+
c.valInt.Add(ival)
135145
return
136146
}
137147

138-
for {
139-
oldBits := atomic.LoadUint64(&c.valBits)
140-
newBits := math.Float64bits(math.Float64frombits(oldBits) + v)
141-
if atomic.CompareAndSwapUint64(&c.valBits, oldBits, newBits) {
142-
return
143-
}
144-
}
148+
c.valBits.Add(v)
145149
}
146150

147151
func (c *counter) AddWithExemplar(v float64, e Labels) {
@@ -150,13 +154,11 @@ func (c *counter) AddWithExemplar(v float64, e Labels) {
150154
}
151155

152156
func (c *counter) Inc() {
153-
atomic.AddUint64(&c.valInt, 1)
157+
c.valInt.Add(1)
154158
}
155159

156160
func (c *counter) get() float64 {
157-
fval := math.Float64frombits(atomic.LoadUint64(&c.valBits))
158-
ival := atomic.LoadUint64(&c.valInt)
159-
return fval + float64(ival)
161+
return c.valBits.Value() + float64(c.valInt.Value())
160162
}
161163

162164
func (c *counter) Write(out *dto.Metric) error {
@@ -216,7 +218,13 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec {
216218
if len(lvs) != len(desc.variableLabels.names) {
217219
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.names, lvs))
218220
}
219-
result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: opts.now}
221+
result := &counter{
222+
desc: desc,
223+
labelPairs: MakeLabelPairs(desc, lvs),
224+
valBits: xsync.NewCounter[float64](),
225+
valInt: xsync.NewCounter[int64](),
226+
now: opts.now,
227+
}
220228
result.init(result) // Init self-collection.
221229
result.createdTs = timestamppb.New(opts.now())
222230
return result

prometheus/counter_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,25 @@ func TestCounterAdd(t *testing.T) {
3434
now: func() time.Time { return now },
3535
}).(*counter)
3636
counter.Inc()
37-
if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got {
37+
if expected, got := 0.0, counter.valBits.Value(); expected != got {
3838
t.Errorf("Expected %f, got %f.", expected, got)
3939
}
40-
if expected, got := uint64(1), counter.valInt; expected != got {
40+
if expected, got := int64(1), counter.valInt.Value(); expected != got {
4141
t.Errorf("Expected %d, got %d.", expected, got)
4242
}
4343
counter.Add(42)
44-
if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got {
44+
if expected, got := 0.0, counter.valBits.Value(); expected != got {
4545
t.Errorf("Expected %f, got %f.", expected, got)
4646
}
47-
if expected, got := uint64(43), counter.valInt; expected != got {
47+
if expected, got := int64(43), counter.valInt.Value(); expected != got {
4848
t.Errorf("Expected %d, got %d.", expected, got)
4949
}
5050

5151
counter.Add(24.42)
52-
if expected, got := 24.42, math.Float64frombits(counter.valBits); expected != got {
52+
if expected, got := 24.42, counter.valBits.Value(); expected != got {
5353
t.Errorf("Expected %f, got %f.", expected, got)
5454
}
55-
if expected, got := uint64(43), counter.valInt; expected != got {
55+
if expected, got := int64(43), counter.valInt.Value(); expected != got {
5656
t.Errorf("Expected %d, got %d.", expected, got)
5757
}
5858

@@ -153,26 +153,26 @@ func TestCounterAddInf(t *testing.T) {
153153
}).(*counter)
154154

155155
counter.Inc()
156-
if expected, got := 0.0, math.Float64frombits(counter.valBits); expected != got {
156+
if expected, got := 0.0, counter.valBits.Value(); expected != got {
157157
t.Errorf("Expected %f, got %f.", expected, got)
158158
}
159-
if expected, got := uint64(1), counter.valInt; expected != got {
159+
if expected, got := int64(1), counter.valInt.Value(); expected != got {
160160
t.Errorf("Expected %d, got %d.", expected, got)
161161
}
162162

163163
counter.Add(math.Inf(1))
164-
if expected, got := math.Inf(1), math.Float64frombits(counter.valBits); expected != got {
164+
if expected, got := math.Inf(1), counter.valBits.Value(); expected != got {
165165
t.Errorf("valBits expected %f, got %f.", expected, got)
166166
}
167-
if expected, got := uint64(1), counter.valInt; expected != got {
167+
if expected, got := int64(1), counter.valInt.Value(); expected != got {
168168
t.Errorf("valInts expected %d, got %d.", expected, got)
169169
}
170170

171171
counter.Inc()
172-
if expected, got := math.Inf(1), math.Float64frombits(counter.valBits); expected != got {
172+
if expected, got := math.Inf(1), counter.valBits.Value(); expected != got {
173173
t.Errorf("Expected %f, got %f.", expected, got)
174174
}
175-
if expected, got := uint64(2), counter.valInt; expected != got {
175+
if expected, got := int64(2), counter.valInt.Value(); expected != got {
176176
t.Errorf("Expected %d, got %d.", expected, got)
177177
}
178178

@@ -203,10 +203,10 @@ func TestCounterAddLarge(t *testing.T) {
203203
// large overflows the underlying type and should therefore be stored in valBits.
204204
large := math.Nextafter(float64(math.MaxUint64), 1e20)
205205
counter.Add(large)
206-
if expected, got := large, math.Float64frombits(counter.valBits); expected != got {
206+
if expected, got := large, counter.valBits.Value(); expected != got {
207207
t.Errorf("valBits expected %f, got %f.", expected, got)
208208
}
209-
if expected, got := uint64(0), counter.valInt; expected != got {
209+
if expected, got := int64(0), counter.valInt.Value(); expected != got {
210210
t.Errorf("valInts expected %d, got %d.", expected, got)
211211
}
212212

@@ -236,10 +236,10 @@ func TestCounterAddSmall(t *testing.T) {
236236

237237
small := 0.000000000001
238238
counter.Add(small)
239-
if expected, got := small, math.Float64frombits(counter.valBits); expected != got {
239+
if expected, got := small, counter.valBits.Value(); expected != got {
240240
t.Errorf("valBits expected %f, got %f.", expected, got)
241241
}
242-
if expected, got := uint64(0), counter.valInt; expected != got {
242+
if expected, got := int64(0), counter.valInt.Value(); expected != got {
243243
t.Errorf("valInts expected %d, got %d.", expected, got)
244244
}
245245

0 commit comments

Comments
 (0)