diff options
Diffstat (limited to 'internal/debug/signals.go')
| -rw-r--r-- | internal/debug/signals.go | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/internal/debug/signals.go b/internal/debug/signals.go new file mode 100644 index 0000000..89e53ea --- /dev/null +++ b/internal/debug/signals.go @@ -0,0 +1,176 @@ +// +build !windows + +package debug + +import ( + "fmt" + "os" + "os/signal" + "path/filepath" + "runtime" + "runtime/pprof" + "strings" + "sync/atomic" + "syscall" + "time" +) + +var ( + // debugDir is the directory where debug output files are written + debugDir string + // dumping prevents concurrent dump attempts + dumping int32 +) + +// SetDebugDir sets the directory where debug output files will be written. +// If empty, the current working directory is used. +func SetDebugDir(dir string) { + debugDir = dir +} + +// InitSignalHandlers sets up signal handlers for runtime diagnostics. +// SIGUSR1: Dump goroutine stacks +// SIGUSR2: Dump full runtime profiles (goroutines, heap, cpu, block) +func InitSignalHandlers() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGUSR1, syscall.SIGUSR2) + + go func() { + for sig := range sigChan { + switch sig { + case syscall.SIGUSR1: + dumpGoroutines() + case syscall.SIGUSR2: + dumpFullProfile() + } + } + }() +} + +// dumpGoroutines writes all goroutine stacks to a file +func dumpGoroutines() { + if !atomic.CompareAndSwapInt32(&dumping, 0, 1) { + fmt.Fprintln(os.Stderr, "debug: dump already in progress, skipping") + return + } + defer atomic.StoreInt32(&dumping, 0) + + timestamp := time.Now().Format("20060102-150405") + filename := fmt.Sprintf("tasksamurai-goroutines-%s.txt", timestamp) + if debugDir != "" { + filename = filepath.Join(debugDir, filename) + } + + f, err := os.Create(filename) + if err != nil { + fmt.Fprintf(os.Stderr, "debug: failed to create goroutine dump file: %v\n", err) + return + } + defer f.Close() + + // Write header + fmt.Fprintf(f, "TaskSamurai Goroutine Dump\n") + fmt.Fprintf(f, "Timestamp: %s\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(f, "NumGoroutine: %d\n", runtime.NumGoroutine()) + fmt.Fprintf(f, "NumCPU: %d\n", runtime.NumCPU()) + fmt.Fprintf(f, "\n%s\n\n", strings.Repeat("=", 80)) + + // Get stack traces + buf := make([]byte, 1024*1024) // 1MB buffer + stackLen := runtime.Stack(buf, true) + f.Write(buf[:stackLen]) + + fmt.Fprintf(os.Stderr, "debug: goroutine stacks written to %s\n", filename) +} + +// dumpFullProfile writes comprehensive runtime profiles to files +func dumpFullProfile() { + if !atomic.CompareAndSwapInt32(&dumping, 0, 1) { + fmt.Fprintln(os.Stderr, "debug: dump already in progress, skipping") + return + } + defer atomic.StoreInt32(&dumping, 0) + + timestamp := time.Now().Format("20060102-150405") + prefix := fmt.Sprintf("tasksamurai-%s", timestamp) + if debugDir != "" { + prefix = filepath.Join(debugDir, fmt.Sprintf("tasksamurai-%s", timestamp)) + } + + fmt.Fprintf(os.Stderr, "debug: starting full profile dump...\n") + + // Dump goroutines (text format) + goroutineFile := prefix + "-goroutines.txt" + if err := writeProfile(goroutineFile, "goroutine", 1); err != nil { + fmt.Fprintf(os.Stderr, "debug: failed to write goroutine profile: %v\n", err) + } else { + fmt.Fprintf(os.Stderr, "debug: goroutine profile written to %s\n", goroutineFile) + } + + // Dump heap profile + heapFile := prefix + "-heap.pprof" + if err := writeProfile(heapFile, "heap", 0); err != nil { + fmt.Fprintf(os.Stderr, "debug: failed to write heap profile: %v\n", err) + } else { + fmt.Fprintf(os.Stderr, "debug: heap profile written to %s\n", heapFile) + } + + // Dump block profile + blockFile := prefix + "-block.pprof" + runtime.SetBlockProfileRate(1) + if err := writeProfile(blockFile, "block", 0); err != nil { + fmt.Fprintf(os.Stderr, "debug: failed to write block profile: %v\n", err) + } else { + fmt.Fprintf(os.Stderr, "debug: block profile written to %s\n", blockFile) + } + + // Dump CPU profile (5 second sample) + cpuFile := prefix + "-cpu.pprof" + if err := writeCPUProfile(cpuFile, 5*time.Second); err != nil { + fmt.Fprintf(os.Stderr, "debug: failed to write CPU profile: %v\n", err) + } else { + fmt.Fprintf(os.Stderr, "debug: CPU profile written to %s\n", cpuFile) + } + + fmt.Fprintf(os.Stderr, "debug: full profile dump complete\n") +} + +// writeProfile writes a pprof profile to a file +func writeProfile(filename, profileName string, debug int) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + // Add header for text format + if debug > 0 { + fmt.Fprintf(f, "TaskSamurai %s Profile\n", profileName) + fmt.Fprintf(f, "Timestamp: %s\n", time.Now().Format(time.RFC3339)) + fmt.Fprintf(f, "\n%s\n\n", strings.Repeat("=", 80)) + } + + profile := pprof.Lookup(profileName) + if profile == nil { + return fmt.Errorf("profile %s not found", profileName) + } + + return profile.WriteTo(f, debug) +} + +// writeCPUProfile samples CPU usage and writes the profile +func writeCPUProfile(filename string, duration time.Duration) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if err := pprof.StartCPUProfile(f); err != nil { + return err + } + + time.Sleep(duration) + pprof.StopCPUProfile() + return nil +} |
