package crashlog import ( "fmt" "log" "runtime" "strings" "sync" "time" ) // ── Mutex with debug tracking ── // DebugMutex wraps a sync.Mutex with lock-holder tracking. type DebugMutex struct { mu sync.Mutex holder string lockedAt time.Time } // Lock acquires the mutex with caller tracking. func (m *DebugMutex) Lock() { m.mu.Lock() _, file, line, _ := runtime.Caller(1) if idx := strings.LastIndex(file, "Cyrene/"); idx >= 0 { file = file[idx+len("Cyrene/"):] } m.holder = fmt.Sprintf("%s:%d", file, line) m.lockedAt = time.Now() } // Unlock releases the mutex. func (m *DebugMutex) Unlock() { m.holder = "" m.mu.Unlock() } // TryLock attempts to acquire the lock with a timeout. Returns true if acquired. func (m *DebugMutex) TryLock(timeout time.Duration) bool { done := make(chan struct{}) go func() { m.mu.Lock() m.mu.Unlock() close(done) }() select { case <-done: return true case <-time.After(timeout): return false } } // Holder returns who holds the lock. func (m *DebugMutex) Holder() (string, time.Time) { return m.holder, m.lockedAt } // ── Channel debug helpers ── // ChSend sends to a channel with a timeout and logs if it blocks. func ChSend[T any](name string, ch chan<- T, val T, timeout time.Duration) bool { select { case ch <- val: return true case <-time.After(timeout): log.Printf("[debug] ChSend timeout: %s after %v", name, timeout) return false } } // ── Goroutine lifecycle ── // GoStart logs goroutine start and wraps fn with panic recovery. func GoStart(name string, fn func()) { log.Printf("[debug] goroutine START: %s", name) go func() { defer func() { if r := recover(); r != nil { stack := make([]byte, 4096) n := runtime.Stack(stack, false) log.Printf("[debug] goroutine PANIC: %s panic=%v\n%s", name, r, stack[:n]) } log.Printf("[debug] goroutine EXIT: %s", name) }() fn() }() } // ── Timing ── // Since logs if a duration exceeds a threshold. func Since(name string, start time.Time, threshold time.Duration) { elapsed := time.Since(start) if elapsed > threshold { log.Printf("[debug] SLOW: %s took %v (threshold=%v)", name, elapsed.Round(time.Millisecond), threshold) } }