Read the proc status file when PID is given for metrics purpose.

To measure the MaxRSS memory correctly, read the proc status file
under /proc/<pid>/status and extract the MaxRSS value from it. The
implementation is only available for Linux based distributions.

Bug: b/169453825
Test: go test
Change-Id: I32e3068fee7447f9ef5dfb5a8d8dcb6934e0af23
diff --git a/ui/metrics/proc/Android.bp b/ui/metrics/proc/Android.bp
new file mode 100644
index 0000000..32d8217
--- /dev/null
+++ b/ui/metrics/proc/Android.bp
@@ -0,0 +1,37 @@
+// Copyright 2020 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+bootstrap_go_package {
+    name: "soong-ui-metrics-proc",
+    pkgPath: "android/soong/ui/metrics/proc",
+    deps: [
+        "soong-finder-fs",
+    ],
+    srcs: [
+        "status.go",
+    ],
+    linux: {
+        srcs: [
+            "status_linux.go",
+        ],
+        testSrcs: [
+            "status_linux_test.go",
+        ],
+    },
+    darwin: {
+        srcs: [
+            "status_darwin.go",
+        ],
+    },
+}
diff --git a/ui/metrics/proc/status.go b/ui/metrics/proc/status.go
new file mode 100644
index 0000000..f8b734c
--- /dev/null
+++ b/ui/metrics/proc/status.go
@@ -0,0 +1,128 @@
+// package proc contains functionality to read proc status files.
+package proc
+
+import (
+	"strconv"
+	"strings"
+)
+
+// ProcStatus holds information regarding the memory usage of
+// an executing process. The memory sizes in each of the field
+// is in bytes.
+type ProcStatus struct {
+	// Process PID.
+	pid int
+
+	// Peak virtual memory size.
+	VmPeak uint64
+
+	// Virtual memory size.
+	VmSize uint64
+
+	// Locked Memory size.
+	VmLck uint64
+
+	// Pinned memory size.
+	VmPin uint64
+
+	// Peak resident set size.
+	VmHWM uint64
+
+	// Resident set size (sum of RssAnon, RssFile and RssShmem).
+	VmRss uint64
+
+	// Size of resident anonymous memory.
+	RssAnon uint64
+
+	// Size of resident shared memory.
+	RssShmem uint64
+
+	// Size of data segments.
+	VmData uint64
+
+	// Size of stack segments.
+	VmStk uint64
+
+	//Size of text segments.
+	VmExe uint64
+
+	//Shared library code size.
+	VmLib uint64
+
+	// Page table entries size.
+	VmPTE uint64
+
+	// Size of second-level page tables.
+	VmPMD uint64
+
+	// Swapped-out virtual memory size by anonymous private.
+	VmSwap uint64
+
+	// Size of hugetlb memory page size.
+	HugetlbPages uint64
+}
+
+// fillProcStatus takes the key and value, converts the value
+// to the proper size unit and is stored in the ProcStatus.
+func fillProcStatus(s *ProcStatus, key, value string) {
+	v := strToUint64(value)
+	switch key {
+	case "VmPeak":
+		s.VmPeak = v
+	case "VmSize":
+		s.VmSize = v
+	case "VmLck":
+		s.VmLck = v
+	case "VmPin":
+		s.VmPin = v
+	case "VmHWM":
+		s.VmHWM = v
+	case "VmRSS":
+		s.VmRss = v
+	case "RssAnon":
+		s.RssAnon = v
+	case "RssShmem":
+		s.RssShmem = v
+	case "VmData":
+		s.VmData = v
+	case "VmStk":
+		s.VmStk = v
+	case "VmExe":
+		s.VmExe = v
+	case "VmLib":
+		s.VmLib = v
+	case "VmPTE":
+		s.VmPTE = v
+	case "VmPMD":
+		s.VmPMD = v
+	case "VmSwap":
+		s.VmSwap = v
+	case "HugetlbPages":
+		s.HugetlbPages = v
+	}
+}
+
+// strToUint64 takes the string and converts to unsigned 64-bit integer.
+// If the string contains a memory unit such as kB and is converted to
+// bytes.
+func strToUint64(v string) uint64 {
+	// v could be "1024 kB" so scan for the empty space and
+	// split between the value and the unit.
+	var separatorIndex int
+	if separatorIndex = strings.IndexAny(v, " "); separatorIndex < 0 {
+		separatorIndex = len(v)
+	}
+	value, err := strconv.ParseUint(v[:separatorIndex], 10, 64)
+	if err != nil {
+		return 0
+	}
+
+	var scale uint64 = 1
+	switch strings.TrimSpace(v[separatorIndex:]) {
+	case "kB", "KB":
+		scale = 1024
+	case "mB", "MB":
+		scale = 1024 * 1024
+	}
+	return value * scale
+}
diff --git a/ui/metrics/proc/status_darwin.go b/ui/metrics/proc/status_darwin.go
new file mode 100644
index 0000000..5c788a5
--- /dev/null
+++ b/ui/metrics/proc/status_darwin.go
@@ -0,0 +1,11 @@
+package proc
+
+import (
+	"android/soong/finder/fs"
+)
+
+// NewProcStatus returns a zero filled value of ProcStatus as it
+// is not supported for darwin distribution based.
+func NewProcStatus(pid int, _ fs.FileSystem) (*ProcStatus, error) {
+	return &ProcStatus{}, nil
+}
diff --git a/ui/metrics/proc/status_linux.go b/ui/metrics/proc/status_linux.go
new file mode 100644
index 0000000..dc0f943
--- /dev/null
+++ b/ui/metrics/proc/status_linux.go
@@ -0,0 +1,46 @@
+package proc
+
+import (
+	"io/ioutil"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"android/soong/finder/fs"
+)
+
+// NewProcStatus returns an instance of the ProcStatus that contains memory
+// information of the process. The memory information is extracted from the
+// "/proc/<pid>/status" text file. This is only available for Linux
+// distribution that supports /proc.
+func NewProcStatus(pid int, fileSystem fs.FileSystem) (*ProcStatus, error) {
+	statusFname := filepath.Join("/proc", strconv.Itoa(pid), "status")
+	r, err := fileSystem.Open(statusFname)
+	if err != nil {
+		return &ProcStatus{}, err
+	}
+	defer r.Close()
+
+	data, err := ioutil.ReadAll(r)
+	if err != nil {
+		return &ProcStatus{}, err
+	}
+
+	s := &ProcStatus{
+		pid: pid,
+	}
+
+	for _, l := range strings.Split(string(data), "\n") {
+		// If the status file does not contain "key: values", just skip the line
+		// as the information we are looking for is not needed.
+		if !strings.Contains(l, ":") {
+			continue
+		}
+
+		// At this point, we're only considering entries that has key, single value pairs.
+		kv := strings.SplitN(l, ":", 2)
+		fillProcStatus(s, strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]))
+	}
+
+	return s, nil
+}
diff --git a/ui/metrics/proc/status_linux_test.go b/ui/metrics/proc/status_linux_test.go
new file mode 100644
index 0000000..6709850
--- /dev/null
+++ b/ui/metrics/proc/status_linux_test.go
@@ -0,0 +1,112 @@
+package proc
+
+import (
+	"fmt"
+	"path/filepath"
+	"reflect"
+	"strconv"
+	"testing"
+
+	"android/soong/finder/fs"
+)
+
+func TestNewProcStatus(t *testing.T) {
+	fs := fs.NewMockFs(nil)
+
+	pid := 4032827
+	procDir := filepath.Join("/proc", strconv.Itoa(pid))
+	if err := fs.MkDirs(procDir); err != nil {
+		t.Fatalf("failed to create proc pid dir %s: %v", procDir, err)
+	}
+	statusFilename := filepath.Join(procDir, "status")
+
+	if err := fs.WriteFile(statusFilename, statusData, 0644); err != nil {
+		t.Fatalf("failed to write proc file %s: %v", statusFilename, err)
+	}
+
+	status, err := NewProcStatus(pid, fs)
+	if err != nil {
+		t.Fatalf("got %v, want nil for error", err)
+	}
+
+	fmt.Printf("%d %d\b", status.VmPeak, expectedStatus.VmPeak)
+	if !reflect.DeepEqual(status, expectedStatus) {
+		t.Errorf("got %v, expecting %v for ProcStatus", status, expectedStatus)
+	}
+}
+
+var statusData = []byte(`Name:   fake_process
+Umask:  0022
+State:  S (sleeping)
+Tgid:   4032827
+Ngid:   0
+Pid:    4032827
+PPid:   1
+TracerPid:      0
+Uid:    0       0       0       0
+Gid:    0       0       0       0
+FDSize: 512
+Groups:
+NStgid: 4032827
+NSpid:  4032827
+NSpgid: 4032827
+NSsid:  4032827
+VmPeak:   733232 kB
+VmSize:   733232 kB
+VmLck:       132 kB
+VmPin:       130 kB
+VmHWM:     69156 kB
+VmRSS:     69156 kB
+RssAnon:           50896 kB
+RssFile:           18260 kB
+RssShmem:            122 kB
+VmData:   112388 kB
+VmStk:       132 kB
+VmExe:      9304 kB
+VmLib:         8 kB
+VmPTE:       228 kB
+VmSwap:        10 kB
+HugetlbPages:          22 kB
+CoreDumping:    0
+THP_enabled:    1
+Threads:        46
+SigQ:   2/767780
+SigPnd: 0000000000000000
+ShdPnd: 0000000000000000
+SigBlk: fffffffe3bfa3a00
+SigIgn: 0000000000000000
+SigCgt: fffffffe7fc1feff
+CapInh: 0000000000000000
+CapPrm: 0000003fffffffff
+CapEff: 0000003fffffffff
+CapBnd: 0000003fffffffff
+CapAmb: 0000000000000000
+NoNewPrivs:     0
+Seccomp:        0
+Speculation_Store_Bypass:       thread vulnerable
+Cpus_allowed:   ff,ffffffff,ffffffff
+Cpus_allowed_list:      0-71
+Mems_allowed:   00000000,00000003
+Mems_allowed_list:      0-1
+voluntary_ctxt_switches:        1635
+nonvoluntary_ctxt_switches:     32
+`)
+
+var expectedStatus = &ProcStatus{
+	pid:          4032827,
+	VmPeak:       750829568,
+	VmSize:       750829568,
+	VmLck:        135168,
+	VmPin:        133120,
+	VmHWM:        70815744,
+	VmRss:        70815744,
+	RssAnon:      52117504,
+	RssShmem:     124928,
+	VmData:       115085312,
+	VmStk:        135168,
+	VmExe:        9527296,
+	VmLib:        8192,
+	VmPTE:        233472,
+	VmSwap:       10240,
+	HugetlbPages: 22528,
+}