mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-10 20:15:11 -04:00
[client] Debug: dump GUI memory profiles to /tmp/nbgui
Temporary debug patch for the GUI memory consumption investigation. Writes two snapshots - one at startup, one after 5 minutes - each into its own /tmp/nbgui/<timestamp>-<pid> directory containing the heap, goroutine and threadcreate profiles plus a memstats.txt summary.
This commit is contained in:
@@ -80,6 +80,11 @@ func init() {
|
||||
|
||||
func main() {
|
||||
daemonAddr, userSetLogFile := parseFlagsAndInitLog()
|
||||
|
||||
// Debug patch, not for release: dumps heap/goroutine profiles to
|
||||
// /tmp/nbgui for the memory consumption investigation.
|
||||
startMemProfiler()
|
||||
|
||||
conn := NewConn(daemonAddr)
|
||||
|
||||
// Without --log-file, the GUI manages a gui-client.log that follows the
|
||||
|
||||
173
client/ui/memprof.go
Normal file
173
client/ui/memprof.go
Normal file
@@ -0,0 +1,173 @@
|
||||
//go:build !android && !ios && !freebsd && !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const memProfDelay = 5 * time.Minute
|
||||
|
||||
type memProfileSpec struct {
|
||||
profile string
|
||||
file string
|
||||
debug int
|
||||
}
|
||||
|
||||
var memProfileSpecs = []memProfileSpec{
|
||||
{profile: "heap", file: "heap.pprof", debug: 0},
|
||||
{profile: "heap", file: "heap.txt", debug: 1},
|
||||
{profile: "goroutine", file: "goroutine.txt", debug: 1},
|
||||
{profile: "threadcreate", file: "threadcreate.txt", debug: 1},
|
||||
}
|
||||
|
||||
var memProfStart = time.Now()
|
||||
|
||||
// startMemProfiler dumps two profile snapshots for memory analysis: one at
|
||||
// startup and one after memProfDelay, each into its own timestamped directory
|
||||
// under memProfBaseDir. Every failure is logged and never stops the GUI.
|
||||
func startMemProfiler() {
|
||||
base := memProfBaseDir()
|
||||
log.Infof("memory profiler enabled, writing to %s (snapshots at startup and after %s)", base, memProfDelay)
|
||||
|
||||
go func() {
|
||||
writeMemProfile()
|
||||
time.Sleep(memProfDelay)
|
||||
writeMemProfile()
|
||||
}()
|
||||
}
|
||||
|
||||
// memProfBaseDir returns the directory holding the snapshot directories.
|
||||
func memProfBaseDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.TempDir(), "nbgui")
|
||||
}
|
||||
return "/tmp/nbgui"
|
||||
}
|
||||
|
||||
// writeMemProfile creates a <timestamp>-<pid> directory and fills it with the
|
||||
// runtime profiles and the memory statistics summary.
|
||||
func writeMemProfile() {
|
||||
name := fmt.Sprintf("%s-%d", time.Now().Format("20060102-150405"), os.Getpid())
|
||||
dir := filepath.Join(memProfBaseDir(), name)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
log.Warnf("create memory profile dir %s: %v", dir, err)
|
||||
return
|
||||
}
|
||||
|
||||
// The heap profile reports live objects as of the last collection, so force
|
||||
// one to keep inuse_space from counting garbage that is already unreachable.
|
||||
runtime.GC()
|
||||
|
||||
if err := writeMemStats(filepath.Join(dir, "memstats.txt")); err != nil {
|
||||
log.Warnf("write memory statistics: %v", err)
|
||||
}
|
||||
|
||||
for _, spec := range memProfileSpecs {
|
||||
if err := writeMemProfileFile(spec, filepath.Join(dir, spec.file)); err != nil {
|
||||
log.Warnf("write %s profile: %v", spec.profile, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("memory profile written to %s", dir)
|
||||
}
|
||||
|
||||
// writeMemProfileFile writes a single runtime profile to path.
|
||||
func writeMemProfileFile(spec memProfileSpec, path string) error {
|
||||
p := pprof.Lookup(spec.profile)
|
||||
if p == nil {
|
||||
return fmt.Errorf("unknown profile %q", spec.profile)
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create %s: %w", path, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Debugf("close %s: %v", path, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := p.WriteTo(f, spec.debug); err != nil {
|
||||
return fmt.Errorf("write %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeMemStats dumps the runtime memory statistics next to the process
|
||||
// resident set size. A resident set much larger than Sys means the memory sits
|
||||
// outside the Go heap (webview, GTK, other cgo allocations), where the pprof
|
||||
// profiles cannot see it.
|
||||
func writeMemStats(path string) error {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "time: %s\n", time.Now().Format(time.RFC3339))
|
||||
fmt.Fprintf(&b, "uptime: %s\n", time.Since(memProfStart).Round(time.Second))
|
||||
fmt.Fprintf(&b, "pid: %d\n", os.Getpid())
|
||||
fmt.Fprintf(&b, "\n")
|
||||
|
||||
rss, vms := processMemory()
|
||||
fmt.Fprintf(&b, "process_rss: %s\n", rss)
|
||||
fmt.Fprintf(&b, "process_vms: %s\n", vms)
|
||||
fmt.Fprintf(&b, "\n")
|
||||
|
||||
fmt.Fprintf(&b, "sys: %s\n", formatMemBytes(m.Sys))
|
||||
fmt.Fprintf(&b, "heap_alloc: %s\n", formatMemBytes(m.HeapAlloc))
|
||||
fmt.Fprintf(&b, "heap_sys: %s\n", formatMemBytes(m.HeapSys))
|
||||
fmt.Fprintf(&b, "heap_inuse: %s\n", formatMemBytes(m.HeapInuse))
|
||||
fmt.Fprintf(&b, "heap_idle: %s\n", formatMemBytes(m.HeapIdle))
|
||||
fmt.Fprintf(&b, "heap_released: %s\n", formatMemBytes(m.HeapReleased))
|
||||
fmt.Fprintf(&b, "heap_objects: %d\n", m.HeapObjects)
|
||||
fmt.Fprintf(&b, "stack_inuse: %s\n", formatMemBytes(m.StackInuse))
|
||||
fmt.Fprintf(&b, "stack_sys: %s\n", formatMemBytes(m.StackSys))
|
||||
fmt.Fprintf(&b, "mspan_sys: %s\n", formatMemBytes(m.MSpanSys))
|
||||
fmt.Fprintf(&b, "mcache_sys: %s\n", formatMemBytes(m.MCacheSys))
|
||||
fmt.Fprintf(&b, "gc_sys: %s\n", formatMemBytes(m.GCSys))
|
||||
fmt.Fprintf(&b, "other_sys: %s\n", formatMemBytes(m.OtherSys))
|
||||
fmt.Fprintf(&b, "next_gc: %s\n", formatMemBytes(m.NextGC))
|
||||
fmt.Fprintf(&b, "num_gc: %d\n", m.NumGC)
|
||||
fmt.Fprintf(&b, "\n")
|
||||
|
||||
fmt.Fprintf(&b, "goroutines: %d\n", runtime.NumGoroutine())
|
||||
fmt.Fprintf(&b, "cgo_calls: %d\n", runtime.NumCgoCall())
|
||||
fmt.Fprintf(&b, "gomaxprocs: %d\n", runtime.GOMAXPROCS(0))
|
||||
|
||||
if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processMemory returns the formatted resident and virtual size of this process.
|
||||
func processMemory() (string, string) {
|
||||
p, err := process.NewProcess(int32(os.Getpid()))
|
||||
if err != nil {
|
||||
unavailable := fmt.Sprintf("unavailable (%v)", err)
|
||||
return unavailable, unavailable
|
||||
}
|
||||
|
||||
info, err := p.MemoryInfo()
|
||||
if err != nil {
|
||||
unavailable := fmt.Sprintf("unavailable (%v)", err)
|
||||
return unavailable, unavailable
|
||||
}
|
||||
|
||||
return formatMemBytes(info.RSS), formatMemBytes(info.VMS)
|
||||
}
|
||||
|
||||
// formatMemBytes renders a byte count as megabytes with the raw value kept.
|
||||
func formatMemBytes(n uint64) string {
|
||||
return fmt.Sprintf("%8.1f MB (%d bytes)", float64(n)/(1024*1024), n)
|
||||
}
|
||||
Reference in New Issue
Block a user