whatcanGOwrong
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package counter_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/telemetry/counter/countertest"
|
||||
"golang.org/x/telemetry/internal/counter"
|
||||
"golang.org/x/telemetry/internal/regtest"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
"golang.org/x/telemetry/internal/testenv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Catch any bugs encountered while mapping counters.
|
||||
counter.CrashOnBugs = true
|
||||
}
|
||||
|
||||
func TestConcurrentExtension(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
// This test verifies that files may be concurrently extended: when one file
|
||||
// discovers that its entries exceed the mapped data, it remaps the data.
|
||||
|
||||
// Both programs populate enough new records to extend the file multiple
|
||||
// times.
|
||||
const numCounters = 50000
|
||||
prog1 := regtest.NewProgram(t, "inc1", func() int {
|
||||
for i := 0; i < numCounters; i++ {
|
||||
counter.New(fmt.Sprint("gophers", i)).Inc()
|
||||
}
|
||||
return 0
|
||||
})
|
||||
prog2 := regtest.NewProgram(t, "inc2", func() int {
|
||||
for i := numCounters; i < 2*numCounters; i++ {
|
||||
counter.New(fmt.Sprint("gophers", i)).Inc()
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
dir := t.TempDir()
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Run a no-op program in the telemetry dir to ensure that the weekends file
|
||||
// exists, and avoid the race described in golang/go#68390.
|
||||
// (We could also call countertest.Open here, but better to avoid mutating
|
||||
// state in the current process for a test that is otherwise hermetic)
|
||||
prog0 := regtest.NewProgram(t, "init", func() int { return 0 })
|
||||
if _, err := regtest.RunProgAsOf(t, dir, now, prog0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
// Run the programs concurrently.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if out, err := regtest.RunProgAsOf(t, dir, now, prog1); err != nil {
|
||||
t.Errorf("prog1 failed: %v; output:\n%s", err, out)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if out, err := regtest.RunProgAsOf(t, dir, now, prog2); err != nil {
|
||||
t.Errorf("prog2 failed: %v; output:\n%s", err, out)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
counts := readCountsForDir(t, telemetry.NewDir(dir).LocalDir())
|
||||
if got, want := len(counts), 2*numCounters; got != want {
|
||||
t.Errorf("Got %d counters, want %d", got, want)
|
||||
}
|
||||
|
||||
for name, value := range counts {
|
||||
if value != 1 {
|
||||
t.Errorf("count(%s) = %d, want 1", name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readCountsForDir(t *testing.T, dir string) map[string]uint64 {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var countFiles []string
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".count") {
|
||||
countFiles = append(countFiles, filepath.Join(dir, entry.Name()))
|
||||
}
|
||||
}
|
||||
if len(countFiles) != 1 {
|
||||
t.Fatalf("found %d count files, want 1; directory contents: %v", len(countFiles), entries)
|
||||
}
|
||||
|
||||
counters, _, err := countertest.ReadFile(countFiles[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return counters
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal/counter implements the internals of the public counter package.
|
||||
// In addition to the public API, this package also includes APIs to parse and
|
||||
// manage the counter files, needed by the upload package.
|
||||
package counter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var (
|
||||
// Note: not using internal/godebug, so that internal/godebug can use
|
||||
// internal/counter.
|
||||
debugCounter = strings.Contains(os.Getenv("GODEBUG"), "countertrace=1")
|
||||
CrashOnBugs = false // for testing; if set, exit on fatal log messages
|
||||
)
|
||||
|
||||
// debugPrintf formats a debug message if GODEBUG=countertrace=1.
|
||||
func debugPrintf(format string, args ...any) {
|
||||
if debugCounter {
|
||||
if len(format) == 0 || format[len(format)-1] != '\n' {
|
||||
format += "\n"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "counter: "+format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// debugFatalf logs a fatal error if GODEBUG=countertrace=1.
|
||||
func debugFatalf(format string, args ...any) {
|
||||
if debugCounter || CrashOnBugs {
|
||||
if len(format) == 0 || format[len(format)-1] != '\n' {
|
||||
format += "\n"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "counter bug: "+format, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// A Counter is a single named event counter.
|
||||
// A Counter is safe for use by multiple goroutines simultaneously.
|
||||
//
|
||||
// Counters should typically be created using New
|
||||
// and stored as global variables, like:
|
||||
//
|
||||
// package mypackage
|
||||
// var errorCount = counter.New("mypackage/errors")
|
||||
//
|
||||
// (The initialization of errorCount in this example is handled
|
||||
// entirely by the compiler and linker; this line executes no code
|
||||
// at program startup.)
|
||||
//
|
||||
// Then code can call Add to increment the counter
|
||||
// each time the corresponding event is observed.
|
||||
//
|
||||
// Although it is possible to use New to create
|
||||
// a Counter each time a particular event needs to be recorded,
|
||||
// that usage fails to amortize the construction cost over
|
||||
// multiple calls to Add, so it is more expensive and not recommended.
|
||||
type Counter struct {
|
||||
name string
|
||||
file *file
|
||||
|
||||
next atomic.Pointer[Counter]
|
||||
state counterState
|
||||
ptr counterPtr
|
||||
}
|
||||
|
||||
func (c *Counter) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
type counterPtr struct {
|
||||
m *mappedFile
|
||||
count *atomic.Uint64
|
||||
}
|
||||
|
||||
type counterState struct {
|
||||
bits atomic.Uint64
|
||||
}
|
||||
|
||||
func (s *counterState) load() counterStateBits {
|
||||
return counterStateBits(s.bits.Load())
|
||||
}
|
||||
|
||||
func (s *counterState) update(old *counterStateBits, new counterStateBits) bool {
|
||||
if s.bits.CompareAndSwap(uint64(*old), uint64(new)) {
|
||||
*old = new
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type counterStateBits uint64
|
||||
|
||||
const (
|
||||
stateReaders counterStateBits = 1<<30 - 1
|
||||
stateLocked counterStateBits = stateReaders
|
||||
stateHavePtr counterStateBits = 1 << 30
|
||||
stateExtraShift = 31
|
||||
stateExtra counterStateBits = 1<<64 - 1<<stateExtraShift
|
||||
)
|
||||
|
||||
func (b counterStateBits) readers() int { return int(b & stateReaders) }
|
||||
func (b counterStateBits) locked() bool { return b&stateReaders == stateLocked }
|
||||
func (b counterStateBits) havePtr() bool { return b&stateHavePtr != 0 }
|
||||
func (b counterStateBits) extra() uint64 { return uint64(b&stateExtra) >> stateExtraShift }
|
||||
|
||||
func (b counterStateBits) incReader() counterStateBits { return b + 1 }
|
||||
func (b counterStateBits) decReader() counterStateBits { return b - 1 }
|
||||
func (b counterStateBits) setLocked() counterStateBits { return b | stateLocked }
|
||||
func (b counterStateBits) clearLocked() counterStateBits { return b &^ stateLocked }
|
||||
func (b counterStateBits) setHavePtr() counterStateBits { return b | stateHavePtr }
|
||||
func (b counterStateBits) clearHavePtr() counterStateBits { return b &^ stateHavePtr }
|
||||
func (b counterStateBits) clearExtra() counterStateBits { return b &^ stateExtra }
|
||||
func (b counterStateBits) addExtra(n uint64) counterStateBits {
|
||||
const maxExtra = uint64(stateExtra) >> stateExtraShift // 0x1ffffffff
|
||||
x := b.extra()
|
||||
if x+n < x || x+n > maxExtra {
|
||||
x = maxExtra
|
||||
} else {
|
||||
x += n
|
||||
}
|
||||
return b.clearExtra() | counterStateBits(x)<<stateExtraShift
|
||||
}
|
||||
|
||||
// New returns a counter with the given name.
|
||||
// New can be called in global initializers and will be compiled down to
|
||||
// linker-initialized data. That is, calling New to initialize a global
|
||||
// has no cost at program startup.
|
||||
func New(name string) *Counter {
|
||||
// Note: not calling defaultFile.New in order to keep this
|
||||
// function something the compiler can inline and convert
|
||||
// into static data initializations, with no init-time footprint.
|
||||
return &Counter{name: name, file: &defaultFile}
|
||||
}
|
||||
|
||||
// Inc adds 1 to the counter.
|
||||
func (c *Counter) Inc() {
|
||||
c.Add(1)
|
||||
}
|
||||
|
||||
// Add adds n to the counter. n cannot be negative, as counts cannot decrease.
|
||||
func (c *Counter) Add(n int64) {
|
||||
debugPrintf("Add %q += %d", c.name, n)
|
||||
|
||||
if n < 0 {
|
||||
panic("Counter.Add negative")
|
||||
}
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
c.file.register(c)
|
||||
|
||||
state := c.state.load()
|
||||
for ; ; state = c.state.load() {
|
||||
switch {
|
||||
case !state.locked() && state.havePtr():
|
||||
if !c.state.update(&state, state.incReader()) {
|
||||
continue
|
||||
}
|
||||
// Counter unlocked or counter shared; has an initialized count pointer; acquired shared lock.
|
||||
if c.ptr.count == nil {
|
||||
for !c.state.update(&state, state.addExtra(uint64(n))) {
|
||||
// keep trying - we already took the reader lock
|
||||
state = c.state.load()
|
||||
}
|
||||
debugPrintf("Add %q += %d: nil extra=%d\n", c.name, n, state.extra())
|
||||
} else {
|
||||
sum := c.add(uint64(n))
|
||||
debugPrintf("Add %q += %d: count=%d\n", c.name, n, sum)
|
||||
}
|
||||
c.releaseReader(state)
|
||||
return
|
||||
|
||||
case state.locked():
|
||||
if !c.state.update(&state, state.addExtra(uint64(n))) {
|
||||
continue
|
||||
}
|
||||
debugPrintf("Add %q += %d: locked extra=%d\n", c.name, n, state.extra())
|
||||
return
|
||||
|
||||
case !state.havePtr():
|
||||
if !c.state.update(&state, state.addExtra(uint64(n)).setLocked()) {
|
||||
continue
|
||||
}
|
||||
debugPrintf("Add %q += %d: noptr extra=%d\n", c.name, n, state.extra())
|
||||
c.releaseLock(state)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Counter) releaseReader(state counterStateBits) {
|
||||
for ; ; state = c.state.load() {
|
||||
// If we are the last reader and havePtr was cleared
|
||||
// while this batch of readers was using c.ptr,
|
||||
// it's our job to update c.ptr by upgrading to a full lock
|
||||
// and letting releaseLock do the work.
|
||||
// Note: no new reader will attempt to add itself now that havePtr is clear,
|
||||
// so we are only racing against possible additions to extra.
|
||||
if state.readers() == 1 && !state.havePtr() {
|
||||
if !c.state.update(&state, state.setLocked()) {
|
||||
continue
|
||||
}
|
||||
debugPrintf("releaseReader %s: last reader, need ptr\n", c.name)
|
||||
c.releaseLock(state)
|
||||
return
|
||||
}
|
||||
|
||||
// Release reader.
|
||||
if !c.state.update(&state, state.decReader()) {
|
||||
continue
|
||||
}
|
||||
debugPrintf("releaseReader %s: released (%d readers now)\n", c.name, state.readers())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Counter) releaseLock(state counterStateBits) {
|
||||
for ; ; state = c.state.load() {
|
||||
if !state.havePtr() {
|
||||
// Set havePtr before updating ptr,
|
||||
// to avoid race with the next clear of havePtr.
|
||||
if !c.state.update(&state, state.setHavePtr()) {
|
||||
continue
|
||||
}
|
||||
debugPrintf("releaseLock %s: reset havePtr (extra=%d)\n", c.name, state.extra())
|
||||
|
||||
// Optimization: only bother loading a new pointer
|
||||
// if we have a value to add to it.
|
||||
c.ptr = counterPtr{nil, nil}
|
||||
if state.extra() != 0 {
|
||||
c.ptr = c.file.lookup(c.name)
|
||||
debugPrintf("releaseLock %s: ptr=%v\n", c.name, c.ptr)
|
||||
}
|
||||
}
|
||||
|
||||
if extra := state.extra(); extra != 0 && c.ptr.count != nil {
|
||||
if !c.state.update(&state, state.clearExtra()) {
|
||||
continue
|
||||
}
|
||||
sum := c.add(extra)
|
||||
debugPrintf("releaseLock %s: flush extra=%d -> count=%d\n", c.name, extra, sum)
|
||||
}
|
||||
|
||||
// Took care of refreshing ptr and flushing extra.
|
||||
// Now we can release the lock, unless of course
|
||||
// another goroutine cleared havePtr or added to extra,
|
||||
// in which case we go around again.
|
||||
if !c.state.update(&state, state.clearLocked()) {
|
||||
continue
|
||||
}
|
||||
debugPrintf("releaseLock %s: unlocked\n", c.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// add wraps the atomic.Uint64.Add operation to handle integer overflow.
|
||||
func (c *Counter) add(n uint64) uint64 {
|
||||
count := c.ptr.count
|
||||
for {
|
||||
old := count.Load()
|
||||
sum := old + n
|
||||
if sum < old {
|
||||
sum = ^uint64(0)
|
||||
}
|
||||
if count.CompareAndSwap(old, sum) {
|
||||
runtime.KeepAlive(c.ptr.m)
|
||||
return sum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Counter) invalidate() {
|
||||
for {
|
||||
state := c.state.load()
|
||||
if !state.havePtr() {
|
||||
debugPrintf("invalidate %s: no ptr\n", c.name)
|
||||
return
|
||||
}
|
||||
if c.state.update(&state, state.clearHavePtr()) {
|
||||
debugPrintf("invalidate %s: cleared havePtr\n", c.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Counter) refresh() {
|
||||
for {
|
||||
state := c.state.load()
|
||||
if state.havePtr() || state.readers() > 0 || state.extra() == 0 {
|
||||
debugPrintf("refresh %s: havePtr=%v readers=%d extra=%d\n", c.name, state.havePtr(), state.readers(), state.extra())
|
||||
return
|
||||
}
|
||||
if c.state.update(&state, state.setLocked()) {
|
||||
debugPrintf("refresh %s: locked havePtr=%v readers=%d extra=%d\n", c.name, state.havePtr(), state.readers(), state.extra())
|
||||
c.releaseLock(state)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads the given counter.
|
||||
// This is the implementation of x/telemetry/counter/countertest.ReadCounter.
|
||||
func Read(c *Counter) (uint64, error) {
|
||||
if c.file.current.Load() == nil {
|
||||
return c.state.load().extra(), nil
|
||||
}
|
||||
pf, err := readFile(c.file)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v, ok := pf.Count[DecodeStack(c.Name())]
|
||||
if !ok {
|
||||
return v, fmt.Errorf("not found:%q", DecodeStack(c.Name()))
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func readFile(f *file) (*File, error) {
|
||||
if f == nil {
|
||||
debugPrintf("No file")
|
||||
return nil, fmt.Errorf("counter is not initialized - was Open called?")
|
||||
}
|
||||
|
||||
// Note: don't call f.rotate here as this will enqueue a follow-up rotation.
|
||||
f.rotate1()
|
||||
|
||||
if f.err != nil {
|
||||
return nil, fmt.Errorf("failed to rotate mapped file - %v", f.err)
|
||||
}
|
||||
current := f.current.Load()
|
||||
if current == nil {
|
||||
return nil, fmt.Errorf("counter has no mapped file")
|
||||
}
|
||||
name := current.f.Name()
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read from file: %v", err)
|
||||
}
|
||||
pf, err := Parse(name, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse: %v", err)
|
||||
}
|
||||
return pf, nil
|
||||
}
|
||||
|
||||
// ReadFile reads the counters and stack counters from the given file.
|
||||
// This is the implementation of x/telemetry/counter/countertest.ReadFile.
|
||||
func ReadFile(name string) (counters, stackCounters map[string]uint64, _ error) {
|
||||
// TODO: Document the format of the stackCounters names.
|
||||
|
||||
data, err := ReadMapped(name)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read from file: %v", err)
|
||||
}
|
||||
pf, err := Parse(name, data)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse: %v", err)
|
||||
}
|
||||
counters = make(map[string]uint64)
|
||||
stackCounters = make(map[string]uint64)
|
||||
for k, v := range pf.Count {
|
||||
if IsStackCounter(k) {
|
||||
stackCounters[DecodeStack(k)] = v
|
||||
} else {
|
||||
counters[k] = v
|
||||
}
|
||||
}
|
||||
return counters, stackCounters, nil
|
||||
}
|
||||
|
||||
// ReadMapped reads the contents of the given file by memory mapping.
|
||||
//
|
||||
// This avoids file synchronization issues.
|
||||
func ReadMapped(name string) ([]byte, error) {
|
||||
f, err := os.OpenFile(name, os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapping, err := memmap(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := make([]byte, fi.Size())
|
||||
copy(data, mapping.Data)
|
||||
munmap(mapping)
|
||||
return data, nil
|
||||
}
|
||||
+570
@@ -0,0 +1,570 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package counter
|
||||
|
||||
// Builders at
|
||||
// https://build.golang.org/?repo=golang.org%2fx%2ftelemetry
|
||||
|
||||
// there are troubles with tests in Windows. all open files have to
|
||||
// be closed by the test so the test directory can be removed.
|
||||
// Once defaultFile is closed, no more tests can be run as
|
||||
// Open() will fault. (This is mysterious.)
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
"golang.org/x/telemetry/internal/testenv"
|
||||
)
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
|
||||
setup(t)
|
||||
var f file
|
||||
defer close(&f)
|
||||
c := f.New("gophers")
|
||||
c.Add(9)
|
||||
f.rotate()
|
||||
if f.err != nil {
|
||||
t.Fatal(f.err)
|
||||
}
|
||||
current := f.current.Load()
|
||||
if current == nil {
|
||||
t.Fatal("no mapped file")
|
||||
}
|
||||
c.Add(0x90)
|
||||
|
||||
name := current.f.Name()
|
||||
t.Logf("wrote %s:\n%s", name, hexDump(current.mapping.Data))
|
||||
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pf, err := Parse(name, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := map[string]uint64{"gophers": 0x99}
|
||||
if !reflect.DeepEqual(pf.Count, want) {
|
||||
t.Errorf("pf.Count = %v, want %v", pf.Count, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingLocalDir(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
err := os.RemoveAll(telemetry.Default.LocalDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
TestBasic(t)
|
||||
}
|
||||
|
||||
func TestParallel(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
|
||||
setup(t)
|
||||
var f file
|
||||
defer close(&f)
|
||||
|
||||
c := f.New("manygophers")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(100)
|
||||
for i := 0; i < 100; i++ {
|
||||
go func() {
|
||||
c.Inc()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
f.rotate()
|
||||
if f.err != nil {
|
||||
t.Fatal(f.err)
|
||||
}
|
||||
current := f.current.Load()
|
||||
if current == nil {
|
||||
t.Fatal("no mapped file")
|
||||
}
|
||||
name := current.f.Name()
|
||||
t.Logf("wrote %s:\n%s", name, hexDump(current.mapping.Data))
|
||||
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pf, err := Parse(name, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := map[string]uint64{"manygophers": 100}
|
||||
if !reflect.DeepEqual(pf.Count, want) {
|
||||
t.Errorf("pf.Count = %v, want %v", pf.Count, want)
|
||||
}
|
||||
}
|
||||
|
||||
// close ensures that the given mapped file is closed. On Windows, this is
|
||||
// necessary prior to test cleanup.
|
||||
// TODO(rfindley): rename.
|
||||
func close(f *file) {
|
||||
mf := f.current.Load()
|
||||
if mf == nil {
|
||||
// telemetry might have been off
|
||||
return
|
||||
}
|
||||
mf.close()
|
||||
}
|
||||
|
||||
func TestLarge(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
|
||||
setup(t)
|
||||
|
||||
var f file
|
||||
defer close(&f)
|
||||
f.rotate()
|
||||
for i := int64(0); i < 10000; i++ {
|
||||
c := f.New(fmt.Sprint("gophers", i))
|
||||
c.Add(i*i + 1)
|
||||
}
|
||||
for i := int64(0); i < 10000; i++ {
|
||||
c := f.New(fmt.Sprint("gophers", i))
|
||||
c.Add(i / 2)
|
||||
}
|
||||
current := f.current.Load()
|
||||
if current == nil {
|
||||
t.Fatal("no mapped file")
|
||||
}
|
||||
name := current.f.Name()
|
||||
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pf, err := Parse(name, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var errcnt int
|
||||
for i := uint64(0); i < 10000; i++ {
|
||||
key := fmt.Sprint("gophers", i)
|
||||
want := 1 + i*i + i/2
|
||||
if n := pf.Count[key]; n != want {
|
||||
// print out the first few errors
|
||||
t.Errorf("Count[%s] = %d, want %d", key, n, want)
|
||||
errcnt++
|
||||
if errcnt > 5 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorruption_Truncation(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("windows does not permit truncating a file that is mapped")
|
||||
}
|
||||
|
||||
defer func(crash bool) {
|
||||
CrashOnBugs = crash
|
||||
}(CrashOnBugs)
|
||||
CrashOnBugs = false // we're intentionally introducing corruption below
|
||||
|
||||
// In golang/go#68311, it appeared that telemetry became stuck in an infinite
|
||||
// loop of re-mapping as a result of a corrupt counter file.
|
||||
//
|
||||
// While the specific conditions that led to corruption are not understood,
|
||||
// the infinite loop was reproducible by truncating the counter file after
|
||||
// extension.
|
||||
|
||||
setup(t)
|
||||
var f file
|
||||
defer close(&f)
|
||||
f.rotate1()
|
||||
|
||||
// Populate enough data to extend the file beyond its minimum length.
|
||||
const numCounters = 1000
|
||||
for i := int64(0); i < numCounters; i++ {
|
||||
f.New(fmt.Sprint("gophers", i)).Inc()
|
||||
}
|
||||
|
||||
current := f.current.Load()
|
||||
if current == nil {
|
||||
t.Fatal("no mapped file")
|
||||
}
|
||||
if err := current.f.Truncate(minFileLen); err != nil {
|
||||
t.Fatalf("truncating %q: %v", current.f.Name(), err)
|
||||
}
|
||||
|
||||
// Increment the same counters that were created above. This should exercise
|
||||
// the corruption, as counter heads will point to file locations that no
|
||||
// longer exist.
|
||||
var f2 file
|
||||
defer close(&f2)
|
||||
f2.rotate1()
|
||||
for i := int64(0); i < numCounters; i++ {
|
||||
f2.New(fmt.Sprint("gophers", i)).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatedNew(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
|
||||
setup(t)
|
||||
var f file
|
||||
defer close(&f)
|
||||
f.rotate()
|
||||
f.New("gophers")
|
||||
c1ptr := f.lookup("gophers")
|
||||
f.New("gophers")
|
||||
c2ptr := f.lookup("gophers")
|
||||
if c1ptr != c2ptr {
|
||||
t.Errorf("c1ptr = %p, c2ptr = %p, want same", c1ptr, c2ptr)
|
||||
}
|
||||
}
|
||||
|
||||
func hexDump(data []byte) string {
|
||||
lines := strings.SplitAfter(hex.Dump(data), "\n")
|
||||
var keep []string
|
||||
for len(lines) > 0 {
|
||||
line := lines[0]
|
||||
keep = append(keep, line)
|
||||
lines = lines[1:]
|
||||
const allZeros = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
|
||||
if strings.Contains(line, allZeros) {
|
||||
i := 0
|
||||
for i < len(lines) && strings.Contains(lines[i], allZeros) {
|
||||
i++
|
||||
}
|
||||
if i > 2 {
|
||||
keep = append(keep, "*\n", lines[i-1])
|
||||
lines = lines[i:]
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(keep, "")
|
||||
}
|
||||
|
||||
func TestNewFile(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
|
||||
setup(t)
|
||||
|
||||
now := CounterTime().UTC()
|
||||
year, month, day := now.Date()
|
||||
// preserve time location as done in (*file).filename.
|
||||
testStartTime := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// test that completely new files have dates well in the future
|
||||
// Try 20 times to get 20 different random numbers.
|
||||
for i := 0; i < 20; i++ {
|
||||
var f file
|
||||
c := f.New("gophers")
|
||||
// shouldn't see a file yet
|
||||
fi, err := os.ReadDir(telemetry.Default.LocalDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fi) != 0 {
|
||||
t.Fatalf("len(fi) = %d, want 0", len(fi))
|
||||
}
|
||||
c.Add(9)
|
||||
// still shouldn't see a file
|
||||
fi, err = os.ReadDir(telemetry.Default.LocalDir())
|
||||
if err != nil {
|
||||
close(&f)
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fi) != 0 {
|
||||
close(&f)
|
||||
t.Fatalf("len(fi) = %d, want 0", len(fi))
|
||||
}
|
||||
f.rotate()
|
||||
// now we should see a count file and a weekends file
|
||||
fi, _ = os.ReadDir(telemetry.Default.LocalDir())
|
||||
if len(fi) != 2 {
|
||||
close(&f)
|
||||
t.Fatalf("len(fi) = %d, want 2", len(fi))
|
||||
}
|
||||
var countFile, weekendsFile string
|
||||
for _, f := range fi {
|
||||
switch f.Name() {
|
||||
case "weekends":
|
||||
weekendsFile = f.Name()
|
||||
// while we're here, check that is ok
|
||||
buf, err := os.ReadFile(filepath.Join(telemetry.Default.LocalDir(), weekendsFile))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf = bytes.TrimSpace(buf)
|
||||
if len(buf) == 0 || buf[0] < '0' || buf[0] >= '7' {
|
||||
t.Errorf("weekends file has bad data: %q", buf)
|
||||
}
|
||||
default:
|
||||
countFile = f.Name()
|
||||
}
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(filepath.Join(telemetry.Default.LocalDir(), countFile))
|
||||
if err != nil {
|
||||
close(&f)
|
||||
t.Fatal(err)
|
||||
}
|
||||
cf, err := Parse(countFile, buf)
|
||||
if err != nil {
|
||||
close(&f)
|
||||
t.Fatal(err)
|
||||
}
|
||||
timeEnd, err := time.Parse(time.RFC3339, cf.Meta["TimeEnd"])
|
||||
if err != nil {
|
||||
close(&f)
|
||||
t.Fatal(err)
|
||||
}
|
||||
days := (timeEnd.Sub(testStartTime)) / (24 * time.Hour)
|
||||
if days <= 0 || days > 7 {
|
||||
timeBegin, _ := time.Parse(time.RFC3339, cf.Meta["TimeBegin"])
|
||||
t.Logf("testStartTime: %v file: %v TimeBegin: %v TimeEnd: %v", testStartTime, fi[0].Name(), timeBegin, timeEnd)
|
||||
t.Errorf("%d: days = %d, want 7 < days <= 14", i, days)
|
||||
}
|
||||
close(&f)
|
||||
// remove the file for the next iteration of the loop
|
||||
os.Remove(filepath.Join(telemetry.Default.LocalDir(), countFile))
|
||||
os.Remove(filepath.Join(telemetry.Default.LocalDir(), weekendsFile))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeekends(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
setup(t)
|
||||
// get all the 49 combinations of today and when the week ends
|
||||
for i := 0; i < 7; i++ {
|
||||
CounterTime = future(i)
|
||||
for index := range "0123456" {
|
||||
os.WriteFile(filepath.Join(telemetry.Default.LocalDir(), "weekends"), []byte{byte(index + '0')}, 0666)
|
||||
var f file
|
||||
c := f.New("gophers")
|
||||
c.Add(7)
|
||||
f.rotate1()
|
||||
fis, err := os.ReadDir(telemetry.Default.LocalDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
weekends := time.Weekday(-1)
|
||||
var begins, ends time.Time
|
||||
for _, fi := range fis {
|
||||
// ignore errors for brevity: something else will fail
|
||||
if fi.Name() == "weekends" {
|
||||
buf, _ := os.ReadFile(filepath.Join(telemetry.Default.LocalDir(), fi.Name()))
|
||||
buf = bytes.TrimSpace(buf)
|
||||
weekends = time.Weekday(buf[0] - '0')
|
||||
} else if strings.HasSuffix(fi.Name(), ".count") {
|
||||
buf, _ := os.ReadFile(filepath.Join(telemetry.Default.LocalDir(), fi.Name()))
|
||||
parsed, _ := Parse(fi.Name(), buf)
|
||||
begins, _ = time.Parse(time.RFC3339, parsed.Meta["TimeBegin"])
|
||||
ends, _ = time.Parse(time.RFC3339, parsed.Meta["TimeEnd"])
|
||||
}
|
||||
}
|
||||
if weekends < 0 {
|
||||
for _, f := range fis {
|
||||
t.Errorf("in %s, weekends is %d", f.Name(), weekends)
|
||||
}
|
||||
continue
|
||||
}
|
||||
delta := int(ends.Sub(begins) / (24 * time.Hour))
|
||||
// if we're an old user, we should have a <=7 day report
|
||||
// if we're a new user, we should have a <=7+7 day report
|
||||
more := 0
|
||||
if delta <= 0+more || delta > 7+more {
|
||||
t.Errorf("delta %d, expected %d<delta<=%d",
|
||||
delta, more, more+7)
|
||||
}
|
||||
if weekends != ends.Weekday() {
|
||||
t.Errorf("weekends %s unexpecteledy not end day %s", weekends, ends.Weekday())
|
||||
}
|
||||
// On Windows, we must unmap f.current before removing files below.
|
||||
close(&f)
|
||||
|
||||
// remove files for the next iteration of the loop
|
||||
for _, f := range fis {
|
||||
os.Remove(filepath.Join(telemetry.Default.LocalDir(), f.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func future(days int) func() time.Time {
|
||||
return func() time.Time {
|
||||
return time.Now().UTC().AddDate(0, 0, days)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
|
||||
setup(t)
|
||||
var f file
|
||||
defer close(&f)
|
||||
f.rotate()
|
||||
|
||||
c := f.NewStack("foo", 5)
|
||||
c.Inc()
|
||||
c.Inc()
|
||||
names := c.Names()
|
||||
if len(names) != 2 {
|
||||
t.Fatalf("got %d names, want 2", len(names))
|
||||
}
|
||||
// each name should be 4 lines, and the two names should
|
||||
// differ only in the second line.
|
||||
n0 := strings.Split(names[0], "\n")
|
||||
n1 := strings.Split(names[1], "\n")
|
||||
if len(n0) != 4 || len(n1) != 4 {
|
||||
t.Errorf("got %d and %d lines, want 4 (%q,%q)", len(n0), len(n1), n0, n1)
|
||||
}
|
||||
for i := 0; i < 4 && i < len(n0) && i < len(n1); i++ {
|
||||
if i == 1 {
|
||||
continue
|
||||
}
|
||||
if n0[i] != n1[i] {
|
||||
t.Errorf("line %d differs:\n%s\n%s", i, n0[i], n1[i])
|
||||
}
|
||||
}
|
||||
// check that ReadStack gives the same results
|
||||
mp, err := ReadStack(c)
|
||||
if len(mp) != 2 {
|
||||
t.Errorf("ReadStack returned %d values, expected 2", len(mp))
|
||||
}
|
||||
for k, v := range mp {
|
||||
if v != 1 {
|
||||
t.Errorf("got %d for %q, expected 1", v, k)
|
||||
}
|
||||
}
|
||||
|
||||
oldnames := make(map[string]bool)
|
||||
for _, nm := range names {
|
||||
oldnames[nm] = true
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
fn(t, 4, c)
|
||||
}
|
||||
newnames := make(map[string]bool)
|
||||
for _, nm := range c.Names() {
|
||||
if !oldnames[nm] {
|
||||
newnames[nm] = true
|
||||
}
|
||||
}
|
||||
// expect 5 new names, one for each level of recursion
|
||||
if len(newnames) != 5 {
|
||||
t.Errorf("got %d new names, want 5", len(newnames))
|
||||
}
|
||||
// make sure the new names contain compression
|
||||
for k := range newnames {
|
||||
if !strings.Contains(k, "\"") {
|
||||
t.Errorf("new name %q does not contain \"", k)
|
||||
}
|
||||
}
|
||||
// look inside. old names should have a count of 1, new ones 2
|
||||
for _, ct := range c.Counters() {
|
||||
if ct == nil {
|
||||
t.Fatal("nil counter")
|
||||
}
|
||||
_, err := Read(ct)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read known counter %v", err)
|
||||
}
|
||||
if ct.ptr.count == nil {
|
||||
t.Errorf("%q has nil ptr.count", ct.Name())
|
||||
continue
|
||||
}
|
||||
if oldnames[ct.Name()] && ct.ptr.count.Load() != 1 {
|
||||
t.Errorf("old name %q has count %d, want 1", ct.Name(), ct.ptr.count.Load())
|
||||
}
|
||||
if newnames[ct.Name()] && ct.ptr.count.Load() != 2 {
|
||||
t.Errorf("new name %q has count %d, want 2", ct.Name(), ct.ptr.count.Load())
|
||||
}
|
||||
}
|
||||
// check that Parse expands compressed counter names
|
||||
current := f.current.Load()
|
||||
if current == nil {
|
||||
t.Fatal("no mapped file")
|
||||
}
|
||||
data := current.mapping.Data
|
||||
fname := "2023-01-01.v1.count" // bogus file name required by Parse.
|
||||
theFile, err := Parse(fname, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// We know what lines should appear in the stack counter names,
|
||||
// although line numbers outside our control might change.
|
||||
// A less fragile test would just check that " doesn't appear
|
||||
known := map[string]bool{
|
||||
"foo": true,
|
||||
"golang.org/x/telemetry/internal/counter.fn": true,
|
||||
"golang.org/x/telemetry/internal/counter.TestStack": true,
|
||||
"runtime.goexit": true,
|
||||
"testing.tRunner": true,
|
||||
}
|
||||
counts := theFile.Count
|
||||
for k := range counts {
|
||||
ll := strings.Split(k, "\n")
|
||||
for _, line := range ll {
|
||||
ix := strings.LastIndex(line, ":")
|
||||
if ix < 0 {
|
||||
continue // foo, for instance
|
||||
}
|
||||
line = line[:ix]
|
||||
if !known[line] {
|
||||
t.Errorf("unexpected line %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn calls itself n times recursively while incrementing the stack counter.
|
||||
func fn(t *testing.T, n int, c *StackCounter) {
|
||||
c.Inc()
|
||||
if n > 0 {
|
||||
fn(t, n-1, c)
|
||||
}
|
||||
}
|
||||
|
||||
func setup(t *testing.T) {
|
||||
log.SetFlags(log.Lshortfile)
|
||||
telemetry.Default = telemetry.NewDir(t.TempDir()) // new dir for each test
|
||||
os.MkdirAll(telemetry.Default.LocalDir(), 0777)
|
||||
os.MkdirAll(telemetry.Default.UploadDir(), 0777)
|
||||
t.Cleanup(func() {
|
||||
CounterTime = func() time.Time { return time.Now().UTC() }
|
||||
})
|
||||
}
|
||||
|
||||
func (f *file) New(name string) *Counter {
|
||||
return &Counter{name: name, file: f}
|
||||
}
|
||||
|
||||
func (f *file) NewStack(name string, depth int) *StackCounter {
|
||||
return &StackCounter{name: name, depth: depth, file: f}
|
||||
}
|
||||
+814
@@ -0,0 +1,814 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package counter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/telemetry/internal/mmap"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
// A file is a counter file.
|
||||
type file struct {
|
||||
// Linked list of all known counters.
|
||||
// (Linked list insertion is easy to make lock-free,
|
||||
// and we don't want the initial counters incremented
|
||||
// by a program to cause significant contention.)
|
||||
counters atomic.Pointer[Counter] // head of list
|
||||
end Counter // list ends at &end instead of nil
|
||||
|
||||
mu sync.Mutex
|
||||
buildInfo *debug.BuildInfo
|
||||
timeBegin, timeEnd time.Time
|
||||
err error
|
||||
// current holds the current file mapping, which may change when the file is
|
||||
// rotated or extended.
|
||||
//
|
||||
// current may be read without holding mu, but may be nil.
|
||||
//
|
||||
// The cleanup logic for file mappings is complicated, because invalidating
|
||||
// counter pointers is reentrant: [file.invalidateCounters] may call
|
||||
// [file.lookup], which acquires mu. Therefore, writing current must be done
|
||||
// as follows:
|
||||
// 1. record the previous value of current
|
||||
// 2. Store a new value in current
|
||||
// 3. unlock mu
|
||||
// 4. call invalidateCounters
|
||||
// 5. close the previous mapped value from (1)
|
||||
// TODO(rfindley): simplify
|
||||
current atomic.Pointer[mappedFile]
|
||||
}
|
||||
|
||||
var defaultFile file
|
||||
|
||||
// register ensures that the counter c is registered with the file.
|
||||
func (f *file) register(c *Counter) {
|
||||
debugPrintf("register %s %p\n", c.Name(), c)
|
||||
|
||||
// If counter is not registered with file, register it.
|
||||
// Doing this lazily avoids init-time work
|
||||
// as well as any execution cost at all for counters
|
||||
// that are not used in a given program.
|
||||
wroteNext := false
|
||||
for wroteNext || c.next.Load() == nil {
|
||||
head := f.counters.Load()
|
||||
next := head
|
||||
if next == nil {
|
||||
next = &f.end
|
||||
}
|
||||
debugPrintf("register %s next %p\n", c.Name(), next)
|
||||
if !wroteNext {
|
||||
if !c.next.CompareAndSwap(nil, next) {
|
||||
debugPrintf("register %s cas failed %p\n", c.Name(), c.next.Load())
|
||||
continue
|
||||
}
|
||||
wroteNext = true
|
||||
} else {
|
||||
c.next.Store(next)
|
||||
}
|
||||
if f.counters.CompareAndSwap(head, c) {
|
||||
debugPrintf("registered %s %p\n", c.Name(), f.counters.Load())
|
||||
return
|
||||
}
|
||||
debugPrintf("register %s cas2 failed %p %p\n", c.Name(), f.counters.Load(), head)
|
||||
}
|
||||
}
|
||||
|
||||
// invalidateCounters marks as invalid all the pointers
|
||||
// held by f's counters and then refreshes them.
|
||||
//
|
||||
// invalidateCounters cannot be called while holding f.mu,
|
||||
// because a counter refresh may call f.lookup.
|
||||
func (f *file) invalidateCounters() {
|
||||
// Mark every counter as needing to refresh its count pointer.
|
||||
if head := f.counters.Load(); head != nil {
|
||||
for c := head; c != &f.end; c = c.next.Load() {
|
||||
c.invalidate()
|
||||
}
|
||||
for c := head; c != &f.end; c = c.next.Load() {
|
||||
c.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lookup looks up the counter with the given name in the file,
|
||||
// allocating it if needed, and returns a pointer to the atomic.Uint64
|
||||
// containing the counter data.
|
||||
// If the file has not been opened yet, lookup returns nil.
|
||||
func (f *file) lookup(name string) counterPtr {
|
||||
current := f.current.Load()
|
||||
if current == nil {
|
||||
debugPrintf("lookup %s - no mapped file\n", name)
|
||||
return counterPtr{}
|
||||
}
|
||||
ptr := f.newCounter(name)
|
||||
if ptr == nil {
|
||||
return counterPtr{}
|
||||
}
|
||||
return counterPtr{current, ptr}
|
||||
}
|
||||
|
||||
// ErrDisabled is the error returned when telemetry is disabled.
|
||||
var ErrDisabled = errors.New("counter: disabled as Go telemetry is off")
|
||||
|
||||
var (
|
||||
errNoBuildInfo = errors.New("counter: missing build info")
|
||||
errCorrupt = errors.New("counter: corrupt counter file")
|
||||
)
|
||||
|
||||
// weekEnd returns the day of the week on which uploads occur (and therefore
|
||||
// counters expire).
|
||||
//
|
||||
// Reads the weekends file, creating one if none exists.
|
||||
func weekEnd() (time.Weekday, error) {
|
||||
// If there is no 'weekends' file create it and initialize it
|
||||
// to a random day of the week. There is a short interval for
|
||||
// a race.
|
||||
weekends := filepath.Join(telemetry.Default.LocalDir(), "weekends")
|
||||
day := fmt.Sprintf("%d\n", rand.Intn(7))
|
||||
if _, err := os.ReadFile(weekends); err != nil {
|
||||
if err := os.MkdirAll(telemetry.Default.LocalDir(), 0777); err != nil {
|
||||
debugPrintf("%v: could not create telemetry.LocalDir %s", err, telemetry.Default.LocalDir())
|
||||
return 0, err
|
||||
}
|
||||
if err = os.WriteFile(weekends, []byte(day), 0666); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// race is over, read the file
|
||||
buf, err := os.ReadFile(weekends)
|
||||
// There is no reasonable way of recovering from errors
|
||||
// so we just fail
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
buf = bytes.TrimSpace(buf)
|
||||
if len(buf) == 0 {
|
||||
return 0, fmt.Errorf("empty weekends file")
|
||||
}
|
||||
weekend := time.Weekday(buf[0] - '0') // 0 is Sunday
|
||||
// paranoia to make sure the value is legal
|
||||
weekend %= 7
|
||||
if weekend < 0 {
|
||||
weekend += 7
|
||||
}
|
||||
return weekend, nil
|
||||
}
|
||||
|
||||
// rotate checks to see whether the file f needs to be rotated,
|
||||
// meaning to start a new counter file with a different date in the name.
|
||||
// rotate is also used to open the file initially, meaning f.current can be nil.
|
||||
// In general rotate should be called just once for each file.
|
||||
// rotate will arrange a timer to call itself again when necessary.
|
||||
func (f *file) rotate() {
|
||||
expiry := f.rotate1()
|
||||
if !expiry.IsZero() {
|
||||
delay := time.Until(expiry)
|
||||
// Some tests set CounterTime to a time in the past, causing delay to be
|
||||
// negative. Avoid infinite loops by delaying at least a short interval.
|
||||
//
|
||||
// TODO(rfindley): instead, just also mock AfterFunc.
|
||||
const minDelay = 1 * time.Minute
|
||||
if delay < minDelay {
|
||||
delay = minDelay
|
||||
}
|
||||
// TODO(rsc): Does this do the right thing for laptops closing?
|
||||
time.AfterFunc(delay, f.rotate)
|
||||
}
|
||||
}
|
||||
|
||||
func nop() {}
|
||||
|
||||
// CounterTime returns the current UTC time.
|
||||
// Mutable for testing.
|
||||
var CounterTime = func() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// counterSpan returns the current time span for a counter file, as determined
|
||||
// by [CounterTime] and the [weekEnd].
|
||||
func counterSpan() (begin, end time.Time, _ error) {
|
||||
year, month, day := CounterTime().Date()
|
||||
begin = time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
|
||||
// files always begin today, but expire on the next day of the week
|
||||
// from the 'weekends' file.
|
||||
weekend, err := weekEnd()
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
incr := int(weekend - begin.Weekday())
|
||||
if incr <= 0 {
|
||||
incr += 7 // ensure that end is later than begin
|
||||
}
|
||||
end = time.Date(year, month, day+incr, 0, 0, 0, 0, time.UTC)
|
||||
return begin, end, nil
|
||||
}
|
||||
|
||||
// rotate1 rotates the current counter file, returning its expiry, or the zero
|
||||
// time if rotation failed.
|
||||
func (f *file) rotate1() time.Time {
|
||||
// Cleanup must be performed while unlocked, since invalidateCounters may
|
||||
// involve calls to f.lookup.
|
||||
var previous *mappedFile // read below while holding the f.mu.
|
||||
defer func() {
|
||||
// Counters must be invalidated whenever the mapped file changes.
|
||||
if next := f.current.Load(); next != previous {
|
||||
f.invalidateCounters()
|
||||
// Ensure that the previous counter mapped file is closed.
|
||||
if previous != nil {
|
||||
previous.close() // safe to call multiple times
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
previous = f.current.Load()
|
||||
|
||||
if f.err != nil {
|
||||
return time.Time{} // already in failed state; nothing to do
|
||||
}
|
||||
|
||||
fail := func(err error) {
|
||||
debugPrintf("rotate: %v", err)
|
||||
f.err = err
|
||||
f.current.Store(nil)
|
||||
}
|
||||
|
||||
if mode, _ := telemetry.Default.Mode(); mode == "off" {
|
||||
// TODO(rfindley): do we ever want to make ErrDisabled recoverable?
|
||||
// Specifically, if f.err is ErrDisabled, should we check again during when
|
||||
// rotating?
|
||||
fail(ErrDisabled)
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
if f.buildInfo == nil {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
fail(errNoBuildInfo)
|
||||
return time.Time{}
|
||||
}
|
||||
f.buildInfo = bi
|
||||
}
|
||||
|
||||
begin, end, err := counterSpan()
|
||||
if err != nil {
|
||||
fail(err)
|
||||
return time.Time{}
|
||||
}
|
||||
if f.timeBegin.Equal(begin) && f.timeEnd.Equal(end) {
|
||||
return f.timeEnd // nothing to do
|
||||
}
|
||||
f.timeBegin, f.timeEnd = begin, end
|
||||
|
||||
goVers, progPath, progVers := telemetry.ProgramInfo(f.buildInfo)
|
||||
meta := fmt.Sprintf("TimeBegin: %s\nTimeEnd: %s\nProgram: %s\nVersion: %s\nGoVersion: %s\nGOOS: %s\nGOARCH: %s\n\n",
|
||||
f.timeBegin.Format(time.RFC3339), f.timeEnd.Format(time.RFC3339),
|
||||
progPath, progVers, goVers, runtime.GOOS, runtime.GOARCH)
|
||||
if len(meta) > maxMetaLen { // should be impossible for our use
|
||||
fail(fmt.Errorf("metadata too long"))
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
if progVers != "" {
|
||||
progVers = "@" + progVers
|
||||
}
|
||||
baseName := fmt.Sprintf("%s%s-%s-%s-%s-%s.%s.count",
|
||||
path.Base(progPath),
|
||||
progVers,
|
||||
goVers,
|
||||
runtime.GOOS,
|
||||
runtime.GOARCH,
|
||||
f.timeBegin.Format(telemetry.DateOnly),
|
||||
FileVersion,
|
||||
)
|
||||
dir := telemetry.Default.LocalDir()
|
||||
if err := os.MkdirAll(dir, 0777); err != nil {
|
||||
fail(fmt.Errorf("making local dir: %v", err))
|
||||
return time.Time{}
|
||||
}
|
||||
name := filepath.Join(dir, baseName)
|
||||
|
||||
m, err := openMapped(name, meta)
|
||||
if err != nil {
|
||||
// Mapping failed:
|
||||
// If there used to be a mapped file, after cleanup
|
||||
// incrementing counters will only change their internal state.
|
||||
// (before cleanup the existing mapped file would be updated)
|
||||
fail(fmt.Errorf("openMapped: %v", err))
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
debugPrintf("using %v", m.f.Name())
|
||||
f.current.Store(m)
|
||||
return f.timeEnd
|
||||
}
|
||||
|
||||
func (f *file) newCounter(name string) *atomic.Uint64 {
|
||||
v, cleanup := f.newCounter1(name)
|
||||
cleanup()
|
||||
return v
|
||||
}
|
||||
|
||||
func (f *file) newCounter1(name string) (v *atomic.Uint64, cleanup func()) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
current := f.current.Load()
|
||||
if current == nil {
|
||||
return nil, nop
|
||||
}
|
||||
debugPrintf("newCounter %s in %s\n", name, current.f.Name())
|
||||
if v, _, _, _ := current.lookup(name); v != nil {
|
||||
return v, nop
|
||||
}
|
||||
v, newM, err := current.newCounter(name)
|
||||
if err != nil {
|
||||
debugPrintf("newCounter %s: %v\n", name, err)
|
||||
return nil, nop
|
||||
}
|
||||
|
||||
cleanup = nop
|
||||
if newM != nil {
|
||||
f.current.Store(newM)
|
||||
cleanup = func() {
|
||||
f.invalidateCounters()
|
||||
current.close()
|
||||
}
|
||||
}
|
||||
return v, cleanup
|
||||
}
|
||||
|
||||
var (
|
||||
openOnce sync.Once
|
||||
// rotating reports whether the call to Open had rotate = true.
|
||||
//
|
||||
// In golang/go#68497, we observed that file rotation can break runtime
|
||||
// deadlock detection. To minimize the fix for 1.23, we are splitting the
|
||||
// Open API into one version that rotates the counter file, and another that
|
||||
// does not. The rotating variable guards against use of both APIs from the
|
||||
// same process.
|
||||
rotating bool
|
||||
)
|
||||
|
||||
// Open associates counting with the defaultFile.
|
||||
// The returned function is for testing only, and should
|
||||
// be called after all Inc()s are finished, but before
|
||||
// any reports are generated.
|
||||
// (Otherwise expired count files will not be deleted on Windows.)
|
||||
func Open(rotate bool) func() {
|
||||
if telemetry.DisabledOnPlatform {
|
||||
return func() {}
|
||||
}
|
||||
close := func() {}
|
||||
openOnce.Do(func() {
|
||||
rotating = rotate
|
||||
if mode, _ := telemetry.Default.Mode(); mode == "off" {
|
||||
// Don't open the file when telemetry is off.
|
||||
defaultFile.err = ErrDisabled
|
||||
// No need to clean up.
|
||||
return
|
||||
}
|
||||
debugPrintf("Open(%v)", rotate)
|
||||
if rotate {
|
||||
defaultFile.rotate() // calls rotate1 and schedules a rotation
|
||||
} else {
|
||||
defaultFile.rotate1()
|
||||
}
|
||||
close = func() {
|
||||
// Once this has been called, the defaultFile is no longer usable.
|
||||
mf := defaultFile.current.Load()
|
||||
if mf == nil {
|
||||
// telemetry might have been off
|
||||
return
|
||||
}
|
||||
mf.close()
|
||||
}
|
||||
})
|
||||
if rotating != rotate {
|
||||
panic("BUG: Open called with inconsistent values for 'rotate'")
|
||||
}
|
||||
return close
|
||||
}
|
||||
|
||||
const (
|
||||
FileVersion = "v1"
|
||||
hdrPrefix = "# telemetry/counter file " + FileVersion + "\n"
|
||||
recordUnit = 32
|
||||
maxMetaLen = 512
|
||||
numHash = 512 // 2kB for hash table
|
||||
maxNameLen = 4 * 1024
|
||||
limitOff = 0
|
||||
hashOff = 4
|
||||
pageSize = 16 * 1024
|
||||
minFileLen = 16 * 1024
|
||||
)
|
||||
|
||||
// A mappedFile is a counter file mmapped into memory.
|
||||
//
|
||||
// The file layout for a mappedFile m is as follows:
|
||||
//
|
||||
// offset, byte size: description
|
||||
// ------------------ -----------
|
||||
// 0, hdrLen: header, containing metadata; see [mappedHeader]
|
||||
// hdrLen+limitOff, 4: uint32 allocation limit (byte offset of the end of counter records)
|
||||
// hdrLen+hashOff, 4*numHash: hash table, stores uint32 heads of a linked list of records, keyed by name hash
|
||||
// hdrLen+hashOff+4*numHash to limit: counter records: see record syntax below
|
||||
//
|
||||
// The record layout is as follows:
|
||||
//
|
||||
// offset, byte size: description
|
||||
// ------------------ -----------
|
||||
// 0, 8: uint64 counter value
|
||||
// 8, 12: uint32 name length
|
||||
// 12, 16: uint32 offset of next record in linked list
|
||||
// 16, name length: counter name
|
||||
type mappedFile struct {
|
||||
meta string
|
||||
hdrLen uint32
|
||||
zero [4]byte
|
||||
closeOnce sync.Once
|
||||
f *os.File
|
||||
mapping *mmap.Data
|
||||
}
|
||||
|
||||
// openMapped opens and memory maps a file.
|
||||
//
|
||||
// name is the path to the file.
|
||||
//
|
||||
// meta is the file metadata, which must match the metadata of the file on disk
|
||||
// exactly.
|
||||
//
|
||||
// existing should be nil the first time this is called for a file,
|
||||
// and when remapping, should be the previous mappedFile.
|
||||
func openMapped(name, meta string) (_ *mappedFile, err error) {
|
||||
hdr, err := mappedHeader(meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Note: using local variable m here, not return value,
|
||||
// so that return nil, err does not set m = nil and break the code in the defer.
|
||||
m := &mappedFile{
|
||||
f: f,
|
||||
meta: meta,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
m.close()
|
||||
}
|
||||
}()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Establish file header and initial data area if not already present.
|
||||
if info.Size() < minFileLen {
|
||||
if _, err := f.WriteAt(hdr, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Write zeros at the end of the file to extend it to minFileLen.
|
||||
if _, err := f.WriteAt(m.zero[:], int64(minFileLen-len(m.zero))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err = f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Size() < minFileLen {
|
||||
return nil, fmt.Errorf("counter: writing file did not extend it")
|
||||
}
|
||||
}
|
||||
|
||||
// Map into memory.
|
||||
mapping, err := memmap(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.mapping = mapping
|
||||
if !bytes.HasPrefix(m.mapping.Data, hdr) {
|
||||
// TODO(rfindley): we can and should do better here, reading the mapped
|
||||
// header length and comparing headers exactly.
|
||||
return nil, fmt.Errorf("counter: header mismatch")
|
||||
}
|
||||
m.hdrLen = uint32(len(hdr))
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func mappedHeader(meta string) ([]byte, error) {
|
||||
if len(meta) > maxMetaLen {
|
||||
return nil, fmt.Errorf("counter: metadata too large")
|
||||
}
|
||||
np := round(len(hdrPrefix), 4)
|
||||
n := round(np+4+len(meta), 32)
|
||||
hdr := make([]byte, n)
|
||||
copy(hdr, hdrPrefix)
|
||||
*(*uint32)(unsafe.Pointer(&hdr[np])) = uint32(n)
|
||||
copy(hdr[np+4:], meta)
|
||||
return hdr, nil
|
||||
}
|
||||
|
||||
func (m *mappedFile) place(limit uint32, name string) (start, end uint32) {
|
||||
if limit == 0 {
|
||||
// first record in file
|
||||
limit = m.hdrLen + hashOff + 4*numHash
|
||||
}
|
||||
n := round(uint32(16+len(name)), recordUnit)
|
||||
start = round(limit, recordUnit) // should already be rounded but just in case
|
||||
// Note: Checking for crossing a page boundary would be
|
||||
// start/pageSize != (start+n-1)/pageSize,
|
||||
// but we are checking for reaching the page end, so no -1.
|
||||
// The page end is reserved for use by extend.
|
||||
// See the comment in m.extend.
|
||||
if start/pageSize != (start+n)/pageSize {
|
||||
// bump start to next page
|
||||
start = round(limit, pageSize)
|
||||
}
|
||||
return start, start + n
|
||||
}
|
||||
|
||||
var memmap = mmap.Mmap
|
||||
var munmap = mmap.Munmap
|
||||
|
||||
func (m *mappedFile) close() {
|
||||
m.closeOnce.Do(func() {
|
||||
if m.mapping != nil {
|
||||
munmap(m.mapping)
|
||||
m.mapping = nil
|
||||
}
|
||||
if m.f != nil {
|
||||
m.f.Close() // best effort
|
||||
m.f = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// hash returns the hash code for name.
|
||||
// The implementation is FNV-1a.
|
||||
// This hash function is a fixed detail of the file format.
|
||||
// It cannot be changed without also changing the file format version.
|
||||
func hash(name string) uint32 {
|
||||
const (
|
||||
offset32 = 2166136261
|
||||
prime32 = 16777619
|
||||
)
|
||||
h := uint32(offset32)
|
||||
for i := 0; i < len(name); i++ {
|
||||
c := name[i]
|
||||
h = (h ^ uint32(c)) * prime32
|
||||
}
|
||||
return (h ^ (h >> 16)) % numHash
|
||||
}
|
||||
|
||||
func (m *mappedFile) load32(off uint32) uint32 {
|
||||
if int64(off) >= int64(len(m.mapping.Data)) {
|
||||
return 0
|
||||
}
|
||||
return (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off])).Load()
|
||||
}
|
||||
|
||||
func (m *mappedFile) cas32(off, old, new uint32) bool {
|
||||
if int64(off) >= int64(len(m.mapping.Data)) {
|
||||
panic("bad cas32") // return false would probably loop
|
||||
}
|
||||
return (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off])).CompareAndSwap(old, new)
|
||||
}
|
||||
|
||||
// entryAt reads a counter record at the given byte offset.
|
||||
//
|
||||
// See the documentation for [mappedFile] for a description of the counter record layout.
|
||||
func (m *mappedFile) entryAt(off uint32) (name []byte, next uint32, v *atomic.Uint64, ok bool) {
|
||||
if off < m.hdrLen+hashOff || int64(off)+16 > int64(len(m.mapping.Data)) {
|
||||
return nil, 0, nil, false
|
||||
}
|
||||
nameLen := m.load32(off+8) & 0x00ffffff
|
||||
if nameLen == 0 || int64(off)+16+int64(nameLen) > int64(len(m.mapping.Data)) {
|
||||
return nil, 0, nil, false
|
||||
}
|
||||
name = m.mapping.Data[off+16 : off+16+nameLen]
|
||||
next = m.load32(off + 12)
|
||||
v = (*atomic.Uint64)(unsafe.Pointer(&m.mapping.Data[off]))
|
||||
return name, next, v, true
|
||||
}
|
||||
|
||||
// writeEntryAt writes a new counter record at the given offset.
|
||||
//
|
||||
// See the documentation for [mappedFile] for a description of the counter record layout.
|
||||
//
|
||||
// writeEntryAt only returns false in the presence of some form of corruption:
|
||||
// an offset outside the bounds of the record region in the mapped file.
|
||||
func (m *mappedFile) writeEntryAt(off uint32, name string) (next *atomic.Uint32, v *atomic.Uint64, ok bool) {
|
||||
// TODO(rfindley): shouldn't this first condition be off < m.hdrLen+hashOff+4*numHash?
|
||||
if off < m.hdrLen+hashOff || int64(off)+16+int64(len(name)) > int64(len(m.mapping.Data)) {
|
||||
return nil, nil, false
|
||||
}
|
||||
copy(m.mapping.Data[off+16:], name)
|
||||
atomic.StoreUint32((*uint32)(unsafe.Pointer(&m.mapping.Data[off+8])), uint32(len(name))|0xff000000)
|
||||
next = (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off+12]))
|
||||
v = (*atomic.Uint64)(unsafe.Pointer(&m.mapping.Data[off]))
|
||||
return next, v, true
|
||||
}
|
||||
|
||||
// lookup searches the mapped file for a counter record with the given name, returning:
|
||||
// - v: the mapped counter value
|
||||
// - headOff: the offset of the head pointer (see [mappedFile])
|
||||
// - head: the value of the head pointer
|
||||
// - ok: whether lookup succeeded
|
||||
func (m *mappedFile) lookup(name string) (v *atomic.Uint64, headOff, head uint32, ok bool) {
|
||||
h := hash(name)
|
||||
headOff = m.hdrLen + hashOff + h*4
|
||||
head = m.load32(headOff)
|
||||
off := head
|
||||
for off != 0 {
|
||||
ename, next, v, ok := m.entryAt(off)
|
||||
if !ok {
|
||||
return nil, 0, 0, false
|
||||
}
|
||||
if string(ename) == name {
|
||||
return v, headOff, head, true
|
||||
}
|
||||
off = next
|
||||
}
|
||||
return nil, headOff, head, true
|
||||
}
|
||||
|
||||
// newCounter allocates and writes a new counter record with the given name.
|
||||
//
|
||||
// If name is already recorded in the file, newCounter returns the existing counter.
|
||||
func (m *mappedFile) newCounter(name string) (v *atomic.Uint64, m1 *mappedFile, err error) {
|
||||
if len(name) > maxNameLen {
|
||||
return nil, nil, fmt.Errorf("counter name too long")
|
||||
}
|
||||
orig := m
|
||||
defer func() {
|
||||
if m != orig {
|
||||
if err != nil {
|
||||
m.close()
|
||||
} else {
|
||||
m1 = m
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
v, headOff, head, ok := m.lookup(name)
|
||||
for tries := 0; !ok; tries++ {
|
||||
if tries >= 10 {
|
||||
debugFatalf("corrupt: failed to remap after 10 tries")
|
||||
return nil, nil, errCorrupt
|
||||
}
|
||||
// Lookup found an invalid pointer,
|
||||
// perhaps because the file has grown larger than the mapping.
|
||||
limit := m.load32(m.hdrLen + limitOff)
|
||||
if limit, datalen := int64(limit), int64(len(m.mapping.Data)); limit <= datalen {
|
||||
// Mapping doesn't need to grow, so lookup found actual corruption,
|
||||
// in the form of an entry pointer that exceeds the recorded allocation
|
||||
// limit. This should never happen, unless the actual file contents are
|
||||
// corrupt.
|
||||
debugFatalf("corrupt: limit %d is within mapping length %d", limit, datalen)
|
||||
return nil, nil, errCorrupt
|
||||
}
|
||||
// That the recorded limit is greater than the mapped data indicates that
|
||||
// an external process has extended the file. Re-map to pick up this extension.
|
||||
newM, err := openMapped(m.f.Name(), m.meta)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if limit, datalen := int64(limit), int64(len(newM.mapping.Data)); limit > datalen {
|
||||
// We've re-mapped, yet limit still exceeds the data length. This
|
||||
// indicates that the underlying file was somehow truncated, or the
|
||||
// recorded limit is corrupt.
|
||||
debugFatalf("corrupt: limit %d exceeds file size %d", limit, datalen)
|
||||
return nil, nil, errCorrupt
|
||||
}
|
||||
// If m != orig, this is at least the second time around the loop
|
||||
// trying to open the mapping. Close the previous attempt.
|
||||
if m != orig {
|
||||
m.close()
|
||||
}
|
||||
m = newM
|
||||
v, headOff, head, ok = m.lookup(name)
|
||||
}
|
||||
if v != nil {
|
||||
return v, nil, nil
|
||||
}
|
||||
|
||||
// Reserve space for new record.
|
||||
// We are competing against other programs using the same file,
|
||||
// so we use a compare-and-swap on the allocation limit in the header.
|
||||
var start, end uint32
|
||||
for {
|
||||
// Determine where record should end, and grow file if needed.
|
||||
limit := m.load32(m.hdrLen + limitOff)
|
||||
start, end = m.place(limit, name)
|
||||
debugPrintf("place %s at %#x-%#x\n", name, start, end)
|
||||
if int64(end) > int64(len(m.mapping.Data)) {
|
||||
newM, err := m.extend(end)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if m != orig {
|
||||
m.close()
|
||||
}
|
||||
m = newM
|
||||
continue
|
||||
}
|
||||
|
||||
// Attempt to reserve that space for our record.
|
||||
if m.cas32(m.hdrLen+limitOff, limit, end) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Write record.
|
||||
next, v, ok := m.writeEntryAt(start, name)
|
||||
if !ok {
|
||||
debugFatalf("corrupt: failed to write entry: %#x+%d vs %#x\n", start, len(name), len(m.mapping.Data))
|
||||
return nil, nil, errCorrupt // more likely our math is wrong
|
||||
}
|
||||
|
||||
// Link record into hash chain, making sure not to introduce a duplicate.
|
||||
// We know name does not appear in the chain starting at head.
|
||||
for {
|
||||
next.Store(head)
|
||||
if m.cas32(headOff, head, start) {
|
||||
return v, nil, nil
|
||||
}
|
||||
|
||||
// Check new elements in chain for duplicates.
|
||||
old := head
|
||||
head = m.load32(headOff)
|
||||
for off := head; off != old; {
|
||||
ename, enext, v, ok := m.entryAt(off)
|
||||
if !ok {
|
||||
return nil, nil, errCorrupt
|
||||
}
|
||||
if string(ename) == name {
|
||||
next.Store(^uint32(0)) // mark ours as dead
|
||||
return v, nil, nil
|
||||
}
|
||||
off = enext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mappedFile) extend(end uint32) (*mappedFile, error) {
|
||||
end = round(end, pageSize)
|
||||
info, err := m.f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Size() < int64(end) {
|
||||
// Note: multiple processes could be calling extend at the same time,
|
||||
// but this write only writes the last 4 bytes of the page.
|
||||
// The last 4 bytes of the page are reserved for this purpose and hold no data.
|
||||
// (In m.place, if a new record would extend to the very end of the page,
|
||||
// it is placed in the next page instead.)
|
||||
// So it is fine if multiple processes extend at the same time.
|
||||
if _, err := m.f.WriteAt(m.zero[:], int64(end)-int64(len(m.zero))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
newM, err := openMapped(m.f.Name(), m.meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if int64(len(newM.mapping.Data)) < int64(end) {
|
||||
// File system or logic bug: new file is somehow not extended.
|
||||
// See go.dev/issue/68311, where this appears to have been happening.
|
||||
newM.close()
|
||||
return nil, errCorrupt
|
||||
}
|
||||
return newM, err
|
||||
}
|
||||
|
||||
// round returns x rounded up to the next multiple of unit,
|
||||
// which must be a power of two.
|
||||
func round[T int | uint32](x T, unit T) T {
|
||||
return (x + unit - 1) &^ (unit - 1)
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package counter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/telemetry/internal/mmap"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Meta map[string]string
|
||||
Count map[string]uint64
|
||||
}
|
||||
|
||||
func Parse(filename string, data []byte) (*File, error) {
|
||||
if !bytes.HasPrefix(data, []byte(hdrPrefix)) || len(data) < pageSize {
|
||||
if len(data) < pageSize {
|
||||
return nil, fmt.Errorf("%s: file too short (%d<%d)", filename, len(data), pageSize)
|
||||
}
|
||||
return nil, fmt.Errorf("%s: wrong hdr (not %q)", filename, hdrPrefix)
|
||||
}
|
||||
corrupt := func() (*File, error) {
|
||||
// TODO(rfindley): return a useful error message.
|
||||
return nil, fmt.Errorf("%s: corrupt counter file", filename)
|
||||
}
|
||||
|
||||
f := &File{
|
||||
Meta: make(map[string]string),
|
||||
Count: make(map[string]uint64),
|
||||
}
|
||||
np := round(len(hdrPrefix), 4)
|
||||
hdrLen := *(*uint32)(unsafe.Pointer(&data[np]))
|
||||
if hdrLen > pageSize {
|
||||
return corrupt()
|
||||
}
|
||||
meta := data[np+4 : hdrLen]
|
||||
if i := bytes.IndexByte(meta, 0); i >= 0 {
|
||||
meta = meta[:i]
|
||||
}
|
||||
m := &mappedFile{
|
||||
meta: string(meta),
|
||||
hdrLen: hdrLen,
|
||||
mapping: &mmap.Data{Data: data},
|
||||
}
|
||||
|
||||
lines := strings.Split(m.meta, "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(line, ": ")
|
||||
if !ok {
|
||||
return corrupt()
|
||||
}
|
||||
f.Meta[k] = v
|
||||
}
|
||||
|
||||
for i := uint32(0); i < numHash; i++ {
|
||||
headOff := hdrLen + hashOff + i*4
|
||||
head := m.load32(headOff)
|
||||
off := head
|
||||
for off != 0 {
|
||||
ename, next, v, ok := m.entryAt(off)
|
||||
if !ok {
|
||||
return corrupt()
|
||||
}
|
||||
if _, ok := f.Count[string(ename)]; ok {
|
||||
return corrupt()
|
||||
}
|
||||
ctrName := DecodeStack(string(ename))
|
||||
f.Count[ctrName] = v.Load()
|
||||
off = next
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package counter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/telemetry/internal/mmap"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
"golang.org/x/telemetry/internal/testenv"
|
||||
)
|
||||
|
||||
// this test traces the life of a counter from creation
|
||||
// through two file.rotate()s, followed by a failure to rotate
|
||||
func TestRotateCounters(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
|
||||
setup(t)
|
||||
|
||||
now := getnow()
|
||||
CounterTime = func() time.Time { return now }
|
||||
|
||||
var f file
|
||||
defer close(&f)
|
||||
c := f.New("gophers")
|
||||
if c.ptr.count != nil {
|
||||
t.Error("new counter has non-nil ptr.count")
|
||||
}
|
||||
c.Inc() // make sure neither hits Counter.add()
|
||||
c.Inc() // second use takes a different code path
|
||||
// at this point c.file is not mapped so c's value is stored in extra.
|
||||
if c.ptr.count != nil {
|
||||
t.Error("counter without mapped file has non-nil ptr.count")
|
||||
}
|
||||
if c.file.current.Load() != nil {
|
||||
t.Error("counter has mapped file unexpectedly")
|
||||
}
|
||||
state := c.state.load()
|
||||
if state.extra() != 2 {
|
||||
// the value of c is in its extra field
|
||||
t.Errorf("got %d, expected 2", state.extra())
|
||||
}
|
||||
// Read should give the same answer
|
||||
if v, err := Read(c); err != nil || v != 2 {
|
||||
t.Errorf("Read got %d, %v, expected 2, nil", v, err)
|
||||
}
|
||||
f.rotate()
|
||||
c.Inc() // this goes through counter.add() safely
|
||||
if c.file.current.Load() == nil {
|
||||
t.Error("rotated file has no mapping")
|
||||
}
|
||||
// rotate called c.releaseLock(), moving c's value from extra to the file
|
||||
state = c.state.load()
|
||||
if state.extra() != 0 {
|
||||
t.Errorf("got %d, expected 0", state.extra())
|
||||
}
|
||||
if c.ptr.count == nil {
|
||||
t.Errorf("c has unexpected nil ptr")
|
||||
} else if c.ptr.count.Load() != 3 {
|
||||
// the value of c is in the mapped file
|
||||
t.Errorf("got %d, expected 3", c.ptr.count.Load())
|
||||
}
|
||||
// and Read should give the same result
|
||||
if v, err := Read(c); err != nil || v != 3 {
|
||||
t.Errorf("Read gave %d, %v, expected 3, nil", v, err)
|
||||
}
|
||||
|
||||
// move into the future and rotate the file, remapping it
|
||||
now = now.Add(7 * 24 * time.Hour)
|
||||
f.rotate()
|
||||
if got, want := f.timeBegin.Format(telemetry.DateOnly), now.Format(telemetry.DateOnly); got != want {
|
||||
t.Errorf("f.timeBegin = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// c has value 0 in the new file
|
||||
// but c won't have a pointer until the next Inc()
|
||||
state = c.state.load()
|
||||
if c.ptr.count == nil {
|
||||
t.Errorf("c unexpedtedly has nil ptr")
|
||||
} else if state.havePtr() {
|
||||
t.Error("unexpected pointer")
|
||||
}
|
||||
if state.extra() != 0 {
|
||||
t.Errorf("got %d, expected 0", state.extra())
|
||||
}
|
||||
c.Inc()
|
||||
state = c.state.load()
|
||||
if state.extra() != 0 {
|
||||
// as expected
|
||||
t.Errorf("got %d, expected 0", state.extra())
|
||||
}
|
||||
if !state.havePtr() {
|
||||
t.Errorf("expectd havePtr")
|
||||
}
|
||||
if c.ptr.count == nil || c.ptr.count.Load() != 1 {
|
||||
t.Errorf("c has wrong value")
|
||||
}
|
||||
// add a counter
|
||||
y := f.New("counter")
|
||||
|
||||
// simulate failure to remap
|
||||
oldmap := memmap
|
||||
now = now.Add(7 * 24 * time.Hour)
|
||||
memmap = func(*os.File) (*mmap.Data, error) { return nil, fmt.Errorf("too bad") }
|
||||
f.rotate()
|
||||
memmap = oldmap
|
||||
|
||||
// no mapping
|
||||
if f.current.Load() != nil {
|
||||
t.Errorf("unexpected mapping")
|
||||
}
|
||||
c.Inc()
|
||||
// c should not have a pointer, but its internal
|
||||
// count should have been incremented
|
||||
if c.ptr.count != nil {
|
||||
t.Error("expected nil ptr")
|
||||
}
|
||||
if c.state.load().extra() != 1 {
|
||||
t.Errorf("got %d, but expected extra to be 1", c.state.load().extra())
|
||||
}
|
||||
// make sure a new counter doesn't fault
|
||||
x := f.New("newcounter")
|
||||
x.Inc()
|
||||
if x.state.load().extra() != 1 {
|
||||
t.Errorf("got %d, but expected extra to be 1", c.state.load().extra())
|
||||
}
|
||||
// make sure an existing unused counter doesn't fault
|
||||
// (it's incremented, but not visible externally)
|
||||
y.Inc()
|
||||
if y.state.load().extra() != 1 {
|
||||
t.Errorf("got %d, but expected extra to be 1", c.state.load().extra())
|
||||
}
|
||||
}
|
||||
|
||||
// return the current date according to counterTime()
|
||||
func getnow() time.Time {
|
||||
year, month, day := CounterTime().Date()
|
||||
now := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
|
||||
return now
|
||||
}
|
||||
|
||||
func TestRotate(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
t.Logf("GOOS %s GOARCH %s", runtime.GOOS, runtime.GOARCH)
|
||||
now := getnow()
|
||||
setup(t)
|
||||
// pretend something was uploaded
|
||||
os.WriteFile(filepath.Join(telemetry.Default.UploadDir(), "anything"), []byte{}, 0666)
|
||||
var f file
|
||||
defer close(&f)
|
||||
c := f.New("gophers")
|
||||
c.Inc()
|
||||
var modified int
|
||||
for i := 0; i < 2; i++ {
|
||||
// nothing should change on the second rotate
|
||||
f.rotate()
|
||||
fi, err := os.ReadDir(telemetry.Default.LocalDir())
|
||||
if err != nil || len(fi) != 2 {
|
||||
t.Fatalf("err=%v, len(fi) = %d, want 2", err, len(fi))
|
||||
}
|
||||
x := fi[0].Name()
|
||||
y := x[len(x)-len(telemetry.DateOnly)-len(".v1.count") : len(x)-len(".v1.count")]
|
||||
us, err := time.ParseInLocation(telemetry.DateOnly, y, time.UTC)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// we expect today's date?
|
||||
if us != now {
|
||||
t.Errorf("us = %v, want %v, i=%d y=%s", us, now, i, y)
|
||||
}
|
||||
fd, err := os.Open(filepath.Join(telemetry.Default.LocalDir(), fi[0].Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stat, err := fd.Stat()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mt := stat.ModTime().Nanosecond()
|
||||
if modified == 0 {
|
||||
modified = mt
|
||||
}
|
||||
if modified != mt {
|
||||
t.Errorf("modified = %v, want %v", mt, modified)
|
||||
}
|
||||
fd.Close()
|
||||
}
|
||||
CounterTime = func() time.Time { return now.Add(7 * 24 * time.Hour) }
|
||||
f.rotate()
|
||||
fi, err := os.ReadDir(telemetry.Default.LocalDir())
|
||||
if err != nil || len(fi) != 3 {
|
||||
t.Fatalf("err=%v, len(fi) = %d, want 3", err, len(fi))
|
||||
}
|
||||
}
|
||||
|
||||
// These were useful while debugging failed mapping
|
||||
func (s *counterState) String() string {
|
||||
if s == nil {
|
||||
return "nil"
|
||||
}
|
||||
return s.load().String()
|
||||
}
|
||||
|
||||
func (b counterStateBits) String() string {
|
||||
rdrs := b.readers()
|
||||
locked := b.locked()
|
||||
if locked {
|
||||
rdrs = 0 // rdrs == 1<<30 - 1
|
||||
}
|
||||
havePtr := b&stateHavePtr != 0
|
||||
extra := uint64(b&stateExtra) >> stateExtraShift
|
||||
return fmt.Sprintf("rdrs:0x%x locked:%v\thavePtr:%v\textra:%d", rdrs, locked, havePtr, extra)
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package counter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// On the disk, and upstream, stack counters look like sets of
|
||||
// regular counters with names that include newlines.
|
||||
|
||||
// a StackCounter is the in-memory knowledge about a stack counter.
|
||||
// StackCounters are more expensive to use than regular Counters,
|
||||
// requiring, at a minimum, a call to runtime.Callers.
|
||||
type StackCounter struct {
|
||||
name string
|
||||
depth int
|
||||
file *file
|
||||
|
||||
mu sync.Mutex
|
||||
// as this is a detail of the implementation, it could be replaced
|
||||
// by a more efficient mechanism
|
||||
stacks []stack
|
||||
}
|
||||
|
||||
type stack struct {
|
||||
pcs []uintptr
|
||||
counter *Counter
|
||||
}
|
||||
|
||||
func NewStack(name string, depth int) *StackCounter {
|
||||
return &StackCounter{name: name, depth: depth, file: &defaultFile}
|
||||
}
|
||||
|
||||
// Inc increments a stack counter. It computes the caller's stack and
|
||||
// looks up the corresponding counter. It then increments that counter,
|
||||
// creating it if necessary.
|
||||
func (c *StackCounter) Inc() {
|
||||
pcs := make([]uintptr, c.depth)
|
||||
n := runtime.Callers(2, pcs) // caller of Inc
|
||||
pcs = pcs[:n]
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Existing counter?
|
||||
var ctr *Counter
|
||||
for _, s := range c.stacks {
|
||||
if eq(s.pcs, pcs) {
|
||||
if s.counter != nil {
|
||||
ctr = s.counter
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctr == nil {
|
||||
// Create new counter.
|
||||
ctr = &Counter{
|
||||
name: EncodeStack(pcs, c.name),
|
||||
file: c.file,
|
||||
}
|
||||
c.stacks = append(c.stacks, stack{pcs: pcs, counter: ctr})
|
||||
}
|
||||
|
||||
ctr.Inc()
|
||||
}
|
||||
|
||||
// EncodeStack returns the name of the counter to
|
||||
// use for the given stack of program counters.
|
||||
// The name encodes the stack.
|
||||
func EncodeStack(pcs []uintptr, prefix string) string {
|
||||
var locs []string
|
||||
lastImport := ""
|
||||
frs := runtime.CallersFrames(pcs)
|
||||
for {
|
||||
fr, more := frs.Next()
|
||||
// TODO(adonovan): this CutLast(".") operation isn't
|
||||
// appropriate for generic function symbols.
|
||||
path, fname := cutLastDot(fr.Function)
|
||||
if path == lastImport {
|
||||
path = `"` // (a ditto mark)
|
||||
} else {
|
||||
lastImport = path
|
||||
}
|
||||
var loc string
|
||||
if fr.Func != nil {
|
||||
// Use function-relative line numbering.
|
||||
// f:+2 means two lines into function f.
|
||||
// f:-1 should never happen, but be conservative.
|
||||
_, entryLine := fr.Func.FileLine(fr.Entry)
|
||||
loc = fmt.Sprintf("%s.%s:%+d", path, fname, fr.Line-entryLine)
|
||||
} else {
|
||||
// The function is non-Go code or is fully inlined:
|
||||
// use absolute line number within enclosing file.
|
||||
loc = fmt.Sprintf("%s.%s:=%d", path, fname, fr.Line)
|
||||
}
|
||||
locs = append(locs, loc)
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
name := prefix + "\n" + strings.Join(locs, "\n")
|
||||
if len(name) > maxNameLen {
|
||||
const bad = "\ntruncated\n"
|
||||
name = name[:maxNameLen-len(bad)] + bad
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// DecodeStack expands the (compressed) stack encoded in the counter name.
|
||||
func DecodeStack(ename string) string {
|
||||
if !strings.Contains(ename, "\n") {
|
||||
return ename // not a stack counter
|
||||
}
|
||||
lines := strings.Split(ename, "\n")
|
||||
var lastPath string // empty or ends with .
|
||||
for i, line := range lines {
|
||||
path, rest := cutLastDot(line)
|
||||
if len(path) == 0 {
|
||||
continue // unchanged
|
||||
}
|
||||
if len(path) == 1 && path[0] == '"' {
|
||||
lines[i] = lastPath + rest
|
||||
} else {
|
||||
lastPath = path + "."
|
||||
// line unchanged
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n") // trailing \n?
|
||||
}
|
||||
|
||||
// input is <import path>.<function name>
|
||||
// output is (import path, function name)
|
||||
func cutLastDot(x string) (before, after string) {
|
||||
i := strings.LastIndex(x, ".")
|
||||
if i < 0 {
|
||||
return "", x
|
||||
}
|
||||
return x[:i], x[i+1:]
|
||||
}
|
||||
|
||||
// Names reports all the counter names associated with a StackCounter.
|
||||
func (c *StackCounter) Names() []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
names := make([]string, len(c.stacks))
|
||||
for i, s := range c.stacks {
|
||||
names[i] = s.counter.Name()
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Counters returns the known Counters for a StackCounter.
|
||||
// There may be more in the count file.
|
||||
func (c *StackCounter) Counters() []*Counter {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
counters := make([]*Counter, len(c.stacks))
|
||||
for i, s := range c.stacks {
|
||||
counters[i] = s.counter
|
||||
}
|
||||
return counters
|
||||
}
|
||||
|
||||
func eq(a, b []uintptr) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ReadStack reads the given stack counter.
|
||||
// This is the implementation of
|
||||
// golang.org/x/telemetry/counter/countertest.ReadStackCounter.
|
||||
func ReadStack(c *StackCounter) (map[string]uint64, error) {
|
||||
ret := map[string]uint64{}
|
||||
for _, ctr := range c.Counters() {
|
||||
v, err := Read(ctr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[DecodeStack(ctr.Name())] = v
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// IsStackCounter reports whether the counter name is for a stack counter.
|
||||
func IsStackCounter(name string) bool {
|
||||
return strings.Contains(name, "\n")
|
||||
}
|
||||
Reference in New Issue
Block a user