internal/worker: add cgroup memory statistics

Gather information about the current cgroup, which
is the implementation of a docker container.

Move all the memory functions to a separate file.

Reduce the display values to the ones that seem to be the most
meaningful.

Change-Id: Ifad13d96750356b6343a65d4945314029e092567
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/256519
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/content/static/html/worker/index.tmpl b/content/static/html/worker/index.tmpl
index e5dffbb..f9e1c71 100644
--- a/content/static/html/worker/index.tmpl
+++ b/content/static/html/worker/index.tmpl
@@ -154,11 +154,9 @@
     <h3>Memory</h3>
     <table>
       <tr><td>Go Heap</td><td>{{.GoMemStats.HeapAlloc | bytesToMi}} Mi</td></tr>
-      <tr><td>Process Virtual</td><td>{{.ProcessStats.VSize | bytesToMi}} Mi</td></tr>
-      <tr><td>Process RSS</td><td>{{.ProcessStats.RSS | bytesToMi}} Mi</td></tr>
-      <tr><td>System Free</td><td>{{.SystemStats.Free | bytesToMi}} Mi</td></tr>
-      <tr><td>System Used</td><td>{{.SystemStats.Used | bytesToMi}} Mi</td></tr>
-      <tr><td>System Available</td><td>{{.SystemStats.Available | bytesToMi}} Mi</td></tr>
+      <tr><td>Container Limit</td><td>{{index .CgroupStats "limit" | bytesToMi}} Mi</td></tr>
+      <tr><td>Container Used</td><td>{{index .CgroupStats "usage" | bytesToMi}} Mi</td></tr>
+      <tr><td>Container Working Set</td><td>{{index .CgroupStats "workingSet" | bytesToMi}} Mi</td></tr>
     </table>
   </div>
 
diff --git a/internal/worker/memory.go b/internal/worker/memory.go
new file mode 100644
index 0000000..a71fae8
--- /dev/null
+++ b/internal/worker/memory.go
@@ -0,0 +1,184 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Functions to collect memory information from a variety of places.
+
+package worker
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"golang.org/x/pkgsite/internal/log"
+)
+
+// systemMemStats holds values from the /proc/meminfo
+// file, which describes the total system memory.
+// All values are in bytes.
+type systemMemStats struct {
+	Total     uint64
+	Free      uint64
+	Available uint64
+	Used      uint64
+	Buffers   uint64
+	Cached    uint64
+}
+
+// getSystemMemStats reads the /proc/meminfo file to get information about the
+// machine.
+func getSystemMemStats() (systemMemStats, error) {
+	f, err := os.Open("/proc/meminfo")
+	if err != nil {
+		return systemMemStats{}, err
+	}
+	defer f.Close()
+
+	// Read the number, convert kibibytes to bytes, and set p.
+	set := func(p *uint64, words []string) {
+		if len(words) != 3 || words[2] != "kB" {
+			err = fmt.Errorf("got %+v, want 3 words, third is 'kB'", words)
+			return
+		}
+		var ki uint64
+		ki, err = strconv.ParseUint(words[1], 10, 64)
+		if err == nil {
+			*p = ki * 1024
+		}
+	}
+
+	scan := bufio.NewScanner(f)
+	var sms systemMemStats
+	for scan.Scan() {
+		words := strings.Fields(scan.Text())
+		switch words[0] {
+		case "MemTotal:":
+			set(&sms.Total, words)
+		case "MemFree:":
+			set(&sms.Free, words)
+		case "MemAvailable:":
+			set(&sms.Available, words)
+		case "Buffers:":
+			set(&sms.Buffers, words)
+		case "Cached:":
+			set(&sms.Cached, words)
+		}
+	}
+	if err == nil {
+		err = scan.Err()
+	}
+	if err != nil {
+		return systemMemStats{}, err
+	}
+	sms.Used = sms.Total - sms.Free - sms.Buffers - sms.Cached // see `man free`
+	return sms, nil
+}
+
+// processMemStats holds values that describe the current process's memory.
+// All values are in bytes.
+type processMemStats struct {
+	VSize uint64 // virtual memory size
+	RSS   uint64 // resident set size (physical memory in use)
+}
+
+func getProcessMemStats() (processMemStats, error) {
+	f, err := os.Open("/proc/self/stat")
+	if err != nil {
+		return processMemStats{}, err
+	}
+	defer f.Close()
+	// Values from `man proc`.
+	var (
+		d          int
+		s          string
+		c          byte
+		vsize, rss uint64
+	)
+	_, err = fmt.Fscanf(f, "%d %s %c %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d",
+		&d, &s, &c, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &vsize, &rss)
+	if err != nil {
+		return processMemStats{}, err
+	}
+	const pageSize = 4 * 1024 // Linux page size, from `getconf PAGESIZE`
+	return processMemStats{
+		VSize: vsize,
+		RSS:   rss * pageSize,
+	}, nil
+}
+
+// Read memory information for the current cgroup.
+// (A cgroup is the sandbox in which a docker container runs.)
+// All values are in bytes.
+// Returns nil on any error.
+func getCgroupMemStats() map[string]uint64 {
+	m, err := getCgroupMemStatsErr()
+	if err != nil {
+		log.Warningf(context.Background(), "getCgroupMemStats: %v", err)
+		return nil
+	}
+	// k8s's definition of container memory, as shown by `kubectl top pods`.
+	// See https://www.magalix.com/blog/memory_working_set-vs-memory_rss.
+	workingSet := m["usage"]
+	tif := m["total_inactive_file"]
+	if tif > workingSet {
+		workingSet = 0
+	} else {
+		workingSet -= tif
+	}
+	m["workingSet"] = workingSet
+	return m
+}
+
+func getCgroupMemStatsErr() (map[string]uint64, error) {
+	const cgroupMemDir = "/sys/fs/cgroup/memory"
+
+	readUintFile := func(filename string) (uint64, error) {
+		data, err := ioutil.ReadFile(filepath.Join(cgroupMemDir, filename))
+		if err != nil {
+			return 0, err
+		}
+		u, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
+		if err != nil {
+			return 0, err
+		}
+		return u, nil
+	}
+
+	m := map[string]uint64{}
+	var err error
+	m["limit"], err = readUintFile("memory.limit_in_bytes")
+	if err != nil {
+		return nil, err
+	}
+	m["usage"], err = readUintFile("memory.usage_in_bytes")
+	if err != nil {
+		return nil, err
+	}
+	f, err := os.Open(filepath.Join(cgroupMemDir, "memory.stat"))
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	scan := bufio.NewScanner(f)
+	for scan.Scan() {
+		fs := strings.Fields(scan.Text())
+		if len(fs) != 2 {
+			return nil, fmt.Errorf("memory.stat: %q: not two fields", scan.Text())
+		}
+		u, err := strconv.ParseUint(fs[1], 10, 64)
+		if err != nil {
+			return nil, err
+		}
+		m[fs[0]] = u
+	}
+	if err := scan.Err(); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
diff --git a/internal/worker/pages.go b/internal/worker/pages.go
index cbbbb85..ef51651 100644
--- a/internal/worker/pages.go
+++ b/internal/worker/pages.go
@@ -5,17 +5,14 @@
 package worker
 
 import (
-	"bufio"
 	"bytes"
 	"context"
 	"errors"
-	"fmt"
 	"io"
 	"net/http"
 	"os"
 	"runtime"
 	"sort"
-	"strconv"
 	"strings"
 	"time"
 
@@ -86,6 +83,7 @@
 		GoMemStats            runtime.MemStats
 		ProcessStats          processMemStats
 		SystemStats           systemMemStats
+		CgroupStats           map[string]uint64
 	}{
 		Config:                s.cfg,
 		Env:                   env(s.cfg),
@@ -98,6 +96,7 @@
 		GoMemStats:            gms,
 		ProcessStats:          pms,
 		SystemStats:           sms,
+		CgroupStats:           getCgroupMemStats(),
 	}
 	return renderPage(ctx, w, page, s.templates[indexTemplate])
 }
@@ -201,92 +200,3 @@
 	}
 	return nil
 }
-
-// systemMemStats holds values from the /proc/meminfo
-// file, which describes the total system memory.
-// All values are in bytes.
-type systemMemStats struct {
-	Total     uint64
-	Free      uint64
-	Available uint64
-	Used      uint64
-	Buffers   uint64
-	Cached    uint64
-}
-
-// getSystemMemStats reads the /proc/meminfo file.
-func getSystemMemStats() (systemMemStats, error) {
-	f, err := os.Open("/proc/meminfo")
-	if err != nil {
-		return systemMemStats{}, err
-	}
-	defer f.Close()
-
-	// Read the number, convert kibibytes to bytes, and set p.
-	set := func(p *uint64, words []string) {
-		if len(words) != 3 || words[2] != "kB" {
-			err = fmt.Errorf("got %+v, want 3 words, third is 'kB'", words)
-			return
-		}
-		var ki uint64
-		ki, err = strconv.ParseUint(words[1], 10, 64)
-		if err == nil {
-			*p = ki * 1024
-		}
-	}
-
-	scan := bufio.NewScanner(f)
-	var sms systemMemStats
-	for scan.Scan() {
-		words := strings.Fields(scan.Text())
-		switch words[0] {
-		case "MemTotal:":
-			set(&sms.Total, words)
-		case "MemFree:":
-			set(&sms.Free, words)
-		case "MemAvailable:":
-			set(&sms.Available, words)
-		case "Buffers:":
-			set(&sms.Buffers, words)
-		case "Cached:":
-			set(&sms.Cached, words)
-		}
-	}
-	if err != nil {
-		return systemMemStats{}, err
-	}
-	sms.Used = sms.Total - sms.Free - sms.Buffers - sms.Cached // see `man free`
-	return sms, nil
-}
-
-// processMemStats holds values that describe the current process's memory.
-// All values are in bytes.
-type processMemStats struct {
-	VSize uint64 // virtual memory size
-	RSS   uint64 // resident set size (physical memory in use)
-}
-
-func getProcessMemStats() (processMemStats, error) {
-	f, err := os.Open("/proc/self/stat")
-	if err != nil {
-		return processMemStats{}, err
-	}
-	defer f.Close()
-	// Values from `man proc`.
-	var (
-		d          int
-		s          string
-		c          byte
-		vsize, rss uint64
-	)
-	_, err = fmt.Fscanf(f, "%d %s %c %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d",
-		&d, &s, &c, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &d, &vsize, &rss)
-	if err != nil {
-		return processMemStats{}, err
-	}
-	const pageSize = 4 * 1024 // Linux page size, from `getconf PAGESIZE`
-	return processMemStats{
-		VSize: vsize,
-		RSS:   rss * pageSize,
-	}, nil
-}