package utils
import (
"math"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/petermattis/goid"
"golang.org/x/exp/slices"
)
const lockTrackerMaxStackDepth = 16
var lockTrackerEnabled = false
var enableLockTrackerOnce sync.Once
var lowResTime uint32 = uint32(time.Now().Unix())
var enableLockTrackerStackTrace uint32
func EnableLockTracker() {
enableLockTrackerOnce.Do(func() {
lockTrackerEnabled = true
go updateLowResTime()
})
}
func ToggleLockTrackerStackTraces(enable bool) {
var v uint32
if enable {
v = 1
}
atomic.StoreUint32(&enableLockTrackerStackTrace, v)
}
func updateLowResTime() {
ticker := time.NewTicker(time.Second)
for t := range ticker.C {
atomic.StoreUint32(&lowResTime, uint32(t.Unix()))
}
}
type weakRefList struct {
refs []uintptr
free []int
next int
}
var weakRefLock sync.Mutex
var weakRefs weakRefList
func (l *weakRefList) add(p unsafe.Pointer) int {
weakRefLock.Lock()
defer weakRefLock.Unlock()
if fi := len(l.free) - 1; fi >= 0 {
ref := l.free[fi]
l.refs[ref] = uintptr(p)
l.free = l.free[:fi]
return ref
}
l.refs = append(l.refs, uintptr(p))
return len(l.refs) - 1
}
func (l *weakRefList) remove(ref int) {
weakRefLock.Lock()
defer weakRefLock.Unlock()
l.refs[ref] = 0
l.free = append(l.free, ref)
}
func (l *weakRefList) count() int {
return len(l.refs) - len(l.free)
}
func (l *weakRefList) window(n int) []uintptr {
min := l.next
max := min + n
if len(l.refs) <= max {
max = len(l.refs)
l.next = 0
} else {
l.next = max
}
return l.refs[min:max]
}
func NumMutexes() int {
weakRefLock.Lock()
defer weakRefLock.Unlock()
return weakRefs.count()
}
func ScanTrackedLocks(threshold time.Duration) []*StuckLock {
minTS := uint32(time.Now().Add(-threshold).Unix())
weakRefLock.Lock()
defer weakRefLock.Unlock()
return scanTrackedLocks(weakRefs.refs, minTS)
}
func ScanTrackedLocksI(threshold time.Duration, n int) []*StuckLock {
minTS := uint32(time.Now().Add(-threshold).Unix())
if n <= 0 {
n = 10000
}
weakRefLock.Lock()
defer weakRefLock.Unlock()
return scanTrackedLocks(weakRefs.window(n), minTS)
}
func scanTrackedLocks(refs []uintptr, minTS uint32) []*StuckLock {
var stuck []*StuckLock
for _, ref := range refs {
if ref != 0 {
t := (*lockTracker)(unsafe.Pointer(ref))
ts := atomic.LoadUint32(&t.ts)
waiting := atomic.LoadInt32(&t.waiting)
if ts <= minTS && waiting > 0 {
stuck = append(stuck, t.toStuckLock(ts, waiting))
}
}
}
return stuck
}
func (t *lockTracker) toStuckLock(ts uint32, waiting int32) *StuckLock {
d := &StuckLock{
stack: slices.Clone(t.stack),
ts: ts,
waiting: waiting,
}
if gid := t.gid; gid != 0 {
d.gids = append(d.gids, gid)
d.held = 1
}
if t.rw {
r := (*rwLockTracker)(unsafe.Pointer(t))
d.held += r.rheld.Load()
if len(d.gids) == 0 && d.held > 0 {
d.holderStrength = HolderShared
}
for i := range r.rgids {
if gid := r.rgids[i].Load(); gid != 0 && !slices.Contains(d.gids, gid) {
d.gids = append(d.gids, gid)
}
}
}
return d
}
type HolderStrength int32
const (
HolderExclusive HolderStrength = iota HolderShared )
func (s HolderStrength) String() string {
if s == HolderShared {
return "shared"
}
return "exclusive"
}
type StuckLock struct {
stack []uintptr
ts uint32
waiting int32
held int32
holderStrength HolderStrength
gids []int64
holderStacks []string
}
func (d *StuckLock) FirstLockedAtStack() string {
fs := runtime.CallersFrames(d.stack)
var b strings.Builder
for {
f, ok := fs.Next()
if !ok {
break
}
if f.Function != "" {
b.WriteString(f.Function)
b.WriteByte('\n')
}
if f.File != "" {
b.WriteByte('\t')
b.WriteString(f.File)
b.WriteByte(':')
b.WriteString(strconv.Itoa(f.Line))
b.WriteByte('\n')
}
}
return b.String()
}
func (d *StuckLock) HeldSince() time.Time {
return time.Unix(int64(d.ts), 0)
}
func (d *StuckLock) NumGoroutineHeld() int {
return int(d.held)
}
func (d *StuckLock) NumGoroutineWaiting() int {
return int(d.waiting)
}
func (d *StuckLock) HolderGoroutineIDs() []int64 {
return d.gids
}
func (d *StuckLock) HolderStrength() HolderStrength {
return d.holderStrength
}
func (d *StuckLock) HolderStacks() string {
return strings.Join(d.holderStacks, "\n\n")
}
func PopulateHolderStacks(stuck []*StuckLock) {
byGID := make(map[int64][]*StuckLock, len(stuck))
for _, d := range stuck {
for _, gid := range d.gids {
byGID[gid] = append(byGID[gid], d)
}
}
if len(byGID) == 0 {
return
}
buf := make([]byte, min(max(1<<20, runtime.NumGoroutine()*4096), 1<<26))
for {
n := runtime.Stack(buf, true)
if n < len(buf) || len(buf) >= 1<<26 {
buf = buf[:n]
break
}
buf = make([]byte, len(buf)*2)
}
for _, g := range strings.Split(string(buf), "\n\n") {
if gid, ok := parseGoroutineHeader(g); ok {
for _, d := range byGID[gid] {
d.holderStacks = append(d.holderStacks, g)
}
}
}
}
func parseGoroutineHeader(g string) (int64, bool) {
const prefix = "goroutine "
if !strings.HasPrefix(g, prefix) {
return 0, false
}
rest := g[len(prefix):]
sp := strings.IndexByte(rest, ' ')
if sp <= 0 {
return 0, false
}
gid, err := strconv.ParseInt(rest[:sp], 10, 64)
return gid, err == nil
}
type lockTracker struct {
stack []uintptr
ts uint32
waiting int32
gid int64
rw bool
}
func (t *lockTracker) trackWait() {
atomic.AddInt32(&t.waiting, 1)
}
func (t *lockTracker) trackLock() {
atomic.AddInt32(&t.waiting, -1)
t.gid = goid.Get()
atomic.StoreUint32(&t.ts, atomic.LoadUint32(&lowResTime))
if atomic.LoadUint32(&enableLockTrackerStackTrace) == 1 {
n := runtime.Callers(2, t.stack[:lockTrackerMaxStackDepth])
t.stack = t.stack[:n]
}
}
func (t *lockTracker) trackUnlock() {
t.gid = 0
atomic.StoreUint32(&t.ts, math.MaxUint32)
}
func newLockTracker() *lockTracker {
return &lockTracker{
stack: make([]uintptr, lockTrackerMaxStackDepth),
ts: math.MaxUint32,
}
}
func loadTracker[T any](p **T) *T {
return (*T)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(p))))
}
func lazyInitTracker[T any](p **T, construct func() *T) *T {
if !lockTrackerEnabled {
return nil
}
up := (*unsafe.Pointer)(unsafe.Pointer(p))
if t := atomic.LoadPointer(up); t != nil {
return (*T)(t)
}
t := construct()
if !atomic.CompareAndSwapPointer(up, nil, unsafe.Pointer(t)) {
return (*T)(atomic.LoadPointer(up))
}
ref := weakRefs.add(unsafe.Pointer(t))
runtime.SetFinalizer(t, func(*T) {
weakRefs.remove(ref)
})
return t
}
type Mutex struct {
sync.Mutex
t *lockTracker
}
func (m *Mutex) Lock() {
t := lazyInitTracker(&m.t, newLockTracker)
if t != nil {
t.trackWait()
defer t.trackLock()
}
m.Mutex.Lock()
}
func (m *Mutex) Unlock() {
if t := loadTracker(&m.t); t != nil {
t.trackUnlock()
}
m.Mutex.Unlock()
}