Skip to content

Commit 33f6895

Browse files
committed
counter: first version of counter package
This package provides counters and stack counters and manages the on-disk counter file. Aside from stackcounter.go it is pretty much Russ' proof-of-concept code. Included also is an internal package for abstracting mmap for Windows and unix-like systems. The counter package will not work on systems without mmap. Some tests fail on openbsd or linux-386 and are explicitly skipped. Change-Id: I2e9f8b7665737daf3c7daa78ef5c09a5eac257e2 Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/499920 TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Peter Weinberger <[email protected]> Reviewed-by: Hyang-Ah Hana Kim <[email protected]> Reviewed-by: Jamal Carvalho <[email protected]>
1 parent 24c9f17 commit 33f6895

14 files changed

+1845
-0
lines changed

counter/STATUS.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Issues in the counter package
2+
3+
## `Open()`
4+
5+
`Open()` is an exported function that asociates counters with a
6+
counter file. Should it remain exported? It is presently called in
7+
`init()` in upload.go. [We don't want to call it if telemetry is off,
8+
but it could be called conditionally in counter.init(), which would
9+
create a disk file even if no counter is ever incremented.]
10+
11+
## Generating reports and uploading
12+
13+
The simplest story would be to generate and upload reports when the
14+
counter file is rotated, but uploads might fail, so that would not be
15+
enough. The proposed way is to start a separate command each time the
16+
counter package starts.
17+
18+
The code could be in the counter package, or in a separate package, or
19+
in a separate command, for instance 'go telemetry upload'. The latter ties
20+
updates to the 'go' command release cycle, and separates the upload code from the
21+
counter package. Thus the code will be in the upload package.
22+
23+
The init() function in upload.go handles this. It checks to see if the
24+
program was invoked with a single argument `__telemetry_upload__`, and if
25+
so, executes the code to generate reports and upload them. If not it spawns
26+
a copy of the current program with that argument.
27+
28+
This commentary can be moved to the upload package when it is checked in.
29+
30+
## TODOs
31+
32+
There are a bunch of TODOs. Also there are many places in the upload code
33+
where log messages are written, but it's unclear how to recover from the
34+
errors. The log messages are written to files named `telemetry-<pid>.log`
35+
in `os.TempDir()`.
36+

counter/bootstrap.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build compiler_bootstrap
6+
7+
package counter
8+
9+
import "fmt"
10+
11+
func Add(string, int64) {}
12+
func Inc(string) {}
13+
func Open() {}
14+
15+
type Counter struct{ name string }
16+
17+
func New(name string) *Counter { return &Counter{name} }
18+
func (c *Counter) Add(n int64) {}
19+
func (c *Counter) Inc() {}
20+
func (c *Counter) Name() string { return c.name }
21+
22+
type File struct {
23+
Meta map[string]string
24+
Count map[string]uint64
25+
}
26+
27+
func Parse(filename string, data []byte) (*File, error) { return nil, fmt.Errorf("unimplemented") }

counter/counter.go

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !compiler_bootstrap
6+
7+
package counter
8+
9+
import (
10+
"fmt"
11+
"os"
12+
"runtime"
13+
"strings"
14+
"sync/atomic"
15+
)
16+
17+
// Note: not using internal/godebug, so that internal/godebug can use internal/counter.
18+
var debugCounter = strings.Contains(os.Getenv("GODEBUG"), "countertrace=1")
19+
20+
func debugPrintf(format string, args ...interface{}) {
21+
if debugCounter {
22+
if len(format) == 0 || format[len(format)-1] != '\n' {
23+
format += "\n"
24+
}
25+
fmt.Fprintf(os.Stderr, "counter: "+format, args...)
26+
}
27+
}
28+
29+
// Inc increments the counter with the given name.
30+
func Inc(name string) {
31+
New(name).Inc()
32+
}
33+
34+
// Add adds n to the counter with the given name.
35+
func Add(name string, n int64) {
36+
New(name).Add(n)
37+
}
38+
39+
// A Counter is a single named event counter.
40+
// A Counter is safe for use by multiple goroutines simultaneously.
41+
//
42+
// Counters should typically be created using New
43+
// and stored as global variables, like:
44+
//
45+
// package mypackage
46+
// var errorCount = counter.New("mypackage/errors")
47+
//
48+
// (The initialization of errorCount in this example is handled
49+
// entirely by the compiler and linker; this line executes no code
50+
// at program startup.)
51+
//
52+
// Then code can call Add to increment the counter
53+
// each time the corresponding event is observed.
54+
//
55+
// Although it is possible to use New to create
56+
// a Counter each time a particular event needs to be recorded,
57+
// that usage fails to amortize the construction cost over
58+
// multiple calls to Add, so it is more expensive and not recommended.
59+
type Counter struct {
60+
name string
61+
file *file
62+
next atomic.Pointer[Counter]
63+
state counterState
64+
ptr counterPtr
65+
}
66+
67+
// Name returns the name of the counter.
68+
func (c *Counter) Name() string {
69+
return c.name
70+
}
71+
72+
type counterPtr struct {
73+
m *mappedFile
74+
count *atomic.Uint64
75+
}
76+
77+
type counterState struct {
78+
bits atomic.Uint64
79+
}
80+
81+
func (s *counterState) load() counterStateBits {
82+
return counterStateBits(s.bits.Load())
83+
}
84+
85+
func (s *counterState) update(old *counterStateBits, new counterStateBits) bool {
86+
if s.bits.CompareAndSwap(uint64(*old), uint64(new)) {
87+
*old = new
88+
return true
89+
}
90+
return false
91+
}
92+
93+
type counterStateBits uint64
94+
95+
const (
96+
stateReaders counterStateBits = 1<<30 - 1
97+
stateLocked counterStateBits = stateReaders
98+
stateHavePtr counterStateBits = 1 << 30
99+
stateExtraShift = 31
100+
stateExtra counterStateBits = 1<<64 - 1<<stateExtraShift
101+
)
102+
103+
func (b counterStateBits) readers() int { return int(b & stateReaders) }
104+
func (b counterStateBits) locked() bool { return b&stateReaders == stateLocked }
105+
func (b counterStateBits) havePtr() bool { return b&stateHavePtr != 0 }
106+
func (b counterStateBits) extra() uint64 { return uint64(b&stateExtra) >> stateExtraShift }
107+
108+
func (b counterStateBits) incReader() counterStateBits { return b + 1 }
109+
func (b counterStateBits) decReader() counterStateBits { return b - 1 }
110+
func (b counterStateBits) setLocked() counterStateBits { return b | stateLocked }
111+
func (b counterStateBits) clearLocked() counterStateBits { return b &^ stateLocked }
112+
func (b counterStateBits) setHavePtr() counterStateBits { return b | stateHavePtr }
113+
func (b counterStateBits) clearHavePtr() counterStateBits { return b &^ stateHavePtr }
114+
func (b counterStateBits) clearExtra() counterStateBits { return b &^ stateExtra }
115+
func (b counterStateBits) addExtra(n uint64) counterStateBits {
116+
const maxExtra = uint64(stateExtra) >> stateExtraShift
117+
x := b.extra()
118+
if x+n < x || x+n > maxExtra {
119+
x = maxExtra
120+
} else {
121+
x += n
122+
}
123+
return b | counterStateBits(x)<<stateExtraShift
124+
}
125+
126+
// New returns a counter with the given name.
127+
// New can be called in global initializers and will be compiled down to
128+
// linker-initialized data. That is, calling New to initialize a global
129+
// has no cost at program startup.
130+
func New(name string) *Counter {
131+
// Note: not calling defaultFile.New in order to keep this
132+
// function something the compiler can inline and convert
133+
// into static data initializations, with no init-time footprint.
134+
return &Counter{name: name, file: &defaultFile}
135+
}
136+
137+
// Inc adds 1 to the counter.
138+
func (c *Counter) Inc() {
139+
c.Add(1)
140+
}
141+
142+
// Add adds n to the counter. n cannot be negative, as counts cannot decrease.
143+
func (c *Counter) Add(n int64) {
144+
debugPrintf("Add %q += %d", c.name, n)
145+
146+
if n < 0 {
147+
panic("Counter.Add negative")
148+
}
149+
if n == 0 {
150+
return
151+
}
152+
c.file.register(c)
153+
154+
state := c.state.load()
155+
for ; ; state = c.state.load() {
156+
switch {
157+
case !state.locked() && state.havePtr():
158+
if !c.state.update(&state, state.incReader()) {
159+
continue
160+
}
161+
// Counter unlocked or counter shared; has an initialized count pointer; acquired shared lock.
162+
if c.ptr.count == nil {
163+
for !c.state.update(&state, state.addExtra(uint64(n))) {
164+
// keep trying - we already took the reader lock
165+
}
166+
debugPrintf("Add %q += %d: nil extra=%d\n", c.name, n, state.extra())
167+
} else {
168+
sum := c.add(uint64(n))
169+
debugPrintf("Add %q += %d: count=%d\n", c.name, n, sum)
170+
}
171+
c.releaseReader(state)
172+
return
173+
174+
case state.locked():
175+
if !c.state.update(&state, state.addExtra(uint64(n))) {
176+
continue
177+
}
178+
debugPrintf("Add %q += %d: locked extra=%d\n", c.name, n, state.extra())
179+
return
180+
181+
case !state.havePtr():
182+
if !c.state.update(&state, state.addExtra(uint64(n)).setLocked()) {
183+
continue
184+
}
185+
debugPrintf("Add %q += %d: noptr extra=%d\n", c.name, n, state.extra())
186+
c.releaseLock(state)
187+
return
188+
}
189+
}
190+
}
191+
192+
func (c *Counter) releaseReader(state counterStateBits) {
193+
for ; ; state = c.state.load() {
194+
// If we are the last reader and havePtr was cleared
195+
// while this batch of readers was using c.ptr,
196+
// it's our job to update c.ptr by upgrading to a full lock
197+
// and letting releaseLock do the work.
198+
// Note: no new reader will attempt to add itself now that havePtr is clear,
199+
// so we are only racing against possible additions to extra.
200+
if state.readers() == 1 && !state.havePtr() {
201+
if !c.state.update(&state, state.setLocked()) {
202+
continue
203+
}
204+
debugPrintf("releaseReader %s: last reader, need ptr\n", c.name)
205+
c.releaseLock(state)
206+
return
207+
}
208+
209+
// Release reader.
210+
if !c.state.update(&state, state.decReader()) {
211+
continue
212+
}
213+
debugPrintf("releaseReader %s: released (%d readers now)\n", c.name, state.readers())
214+
return
215+
}
216+
}
217+
218+
func (c *Counter) releaseLock(state counterStateBits) {
219+
for ; ; state = c.state.load() {
220+
if !state.havePtr() {
221+
// Set havePtr before updating ptr,
222+
// to avoid race with the next clear of havePtr.
223+
if !c.state.update(&state, state.setHavePtr()) {
224+
continue
225+
}
226+
debugPrintf("releaseLock %s: reset havePtr (extra=%d)\n", c.name, state.extra())
227+
228+
// Optimization: only bother loading a new pointer
229+
// if we have a value to add to it.
230+
c.ptr = counterPtr{nil, nil}
231+
if state.extra() != 0 {
232+
c.ptr = c.file.lookup(c.name)
233+
debugPrintf("releaseLock %s: ptr=%v\n", c.name, c.ptr)
234+
}
235+
}
236+
237+
if extra := state.extra(); extra != 0 && c.ptr.count != nil {
238+
if !c.state.update(&state, state.clearExtra()) {
239+
continue
240+
}
241+
sum := c.add(extra)
242+
debugPrintf("releaseLock %s: flush extra=%d -> count=%d\n", c.name, extra, sum)
243+
}
244+
245+
// Took care of refreshing ptr and flushing extra.
246+
// Now we can release the lock, unless of course
247+
// another goroutine cleared havePtr or added to extra,
248+
// in which case we go around again.
249+
if !c.state.update(&state, state.clearLocked()) {
250+
continue
251+
}
252+
debugPrintf("releaseLock %s: unlocked\n", c.name)
253+
return
254+
}
255+
}
256+
257+
func (c *Counter) add(n uint64) uint64 {
258+
count := c.ptr.count
259+
for {
260+
old := count.Load()
261+
sum := old + n
262+
if sum < old {
263+
sum = ^uint64(0)
264+
}
265+
if count.CompareAndSwap(old, sum) {
266+
runtime.KeepAlive(c.ptr.m)
267+
return sum
268+
}
269+
}
270+
}
271+
272+
func (c *Counter) invalidate() {
273+
for {
274+
state := c.state.load()
275+
if !state.havePtr() {
276+
debugPrintf("invalidate %s: no ptr\n", c.name)
277+
return
278+
}
279+
if c.state.update(&state, state.clearHavePtr()) {
280+
debugPrintf("invalidate %s: cleared havePtr\n", c.name)
281+
return
282+
}
283+
}
284+
}
285+
286+
func (c *Counter) refresh() {
287+
for {
288+
state := c.state.load()
289+
if state.havePtr() || state.readers() > 0 || state.extra() == 0 {
290+
debugPrintf("refresh %s: havePtr=%v readers=%d extra=%d\n", c.name, state.havePtr(), state.readers(), state.extra())
291+
return
292+
}
293+
if c.state.update(&state, state.setLocked()) {
294+
debugPrintf("refresh %s: locked havePtr=%v readers=%d extra=%d\n", c.name, state.havePtr(), state.readers(), state.extra())
295+
c.releaseLock(state)
296+
return
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)