19563: Log memory usage of arv-mount, crunch-run, and keepstore.
[arvados.git] / lib / crunchstat / crunchstat.go
index 10cd7cfce43a03472e2e942b68512efcdd7d0c61..443d2202cecb5420ad174ae56ca41009cd7bea1b 100644 (file)
@@ -13,10 +13,11 @@ import (
        "fmt"
        "io"
        "io/ioutil"
-       "log"
        "os"
+       "sort"
        "strconv"
        "strings"
+       "sync"
        "syscall"
        "time"
 )
@@ -47,7 +48,9 @@ type Reporter struct {
        TempDir string
 
        // Where to write statistics. Must not be nil.
-       Logger *log.Logger
+       Logger interface {
+               Printf(fmt string, args ...interface{})
+       }
 
        reportedStatFile    map[string]string
        lastNetSample       map[string]ioSample
@@ -55,6 +58,9 @@ type Reporter struct {
        lastCPUSample       cpuSample
        lastDiskSpaceSample diskSpaceSample
 
+       reportPIDs   map[string]int
+       reportPIDsMu sync.Mutex
+
        done    chan struct{} // closed when we should stop reporting
        flushed chan struct{} // closed when we have made our last report
 }
@@ -76,6 +82,17 @@ func (r *Reporter) Start() {
        go r.run()
 }
 
+// ReportPID starts reporting stats for a specified process.
+func (r *Reporter) ReportPID(name string, pid int) {
+       r.reportPIDsMu.Lock()
+       defer r.reportPIDsMu.Unlock()
+       if r.reportPIDs == nil {
+               r.reportPIDs = map[string]int{name: pid}
+       } else {
+               r.reportPIDs[name] = pid
+       }
+}
+
 // Stop reporting. Do not call more than once, or before calling
 // Start.
 //
@@ -256,6 +273,45 @@ func (r *Reporter) doMemoryStats() {
                }
        }
        r.Logger.Printf("mem%s\n", outstat.String())
+
+       r.reportPIDsMu.Lock()
+       defer r.reportPIDsMu.Unlock()
+       procnames := make([]string, 0, len(r.reportPIDs))
+       for name := range r.reportPIDs {
+               procnames = append(procnames, name)
+       }
+       sort.Strings(procnames)
+       procmem := ""
+       for _, procname := range procnames {
+               pid := r.reportPIDs[procname]
+               buf, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
+               if err != nil {
+                       continue
+               }
+               // If the executable name contains a ')' char,
+               // /proc/$pid/stat will look like '1234 (exec name)) S
+               // 123 ...' -- the last ')' is the end of the 2nd
+               // field.
+               paren := bytes.LastIndexByte(buf, ')')
+               if paren < 0 {
+                       continue
+               }
+               fields := bytes.SplitN(buf[paren:], []byte{' '}, 24)
+               if len(fields) < 24 {
+                       continue
+               }
+               // rss is the 24th field in .../stat, and fields[0]
+               // here is the last char ')' of the 2nd field, so
+               // rss is fields[22]
+               rss, err := strconv.Atoi(string(fields[22]))
+               if err != nil {
+                       continue
+               }
+               procmem += fmt.Sprintf(" %d %s", rss, procname)
+       }
+       if procmem != "" {
+               r.Logger.Printf("procmem%s\n", procmem)
+       }
 }
 
 func (r *Reporter) doNetworkStats() {