Initialize module and dependencies

This commit is contained in:
dwrz
2026-01-04 20:57:40 +00:00
commit a3b390c008
514 changed files with 310495 additions and 0 deletions

View File

@@ -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 := ReadMapped(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
}

814
vendor/golang.org/x/telemetry/internal/counter/file.go generated vendored Normal file
View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -0,0 +1,212 @@
// 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.
//
// An inlined call is replaced by a NOP instruction
// with the correct pclntab information.
_, entryLine := fr.Func.FileLine(fr.Entry)
loc = fmt.Sprintf("%s.%s:%+d,+0x%x", path, fname, fr.Line-entryLine, fr.PC-fr.Entry)
} else {
// The function is non-Go code or is fully inlined:
// use absolute line number within enclosing file.
//
// For inlined calls, the PC and Entry values
// both refer to the enclosing combined function.
// For example, both these PCs are relative to "caller":
//
// callee:=1,+0x12 ('=' means inlined)
// caller:+2,+0x34
loc = fmt.Sprintf("%s.%s:=%d,+0x%x", path, fname, fr.Line, fr.PC-fr.Entry)
}
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")
}