Repository: prometheus/procfs
Pull request #343 (2020-11-20 14:46:45)

Swap read and write stats for XFS
According to the table in https://xfs.org/index.php/Runtime_Stats, the first number of `rw` stats is write and the second is read. I noticed Prometheus seemed to be reporting the opposite of what I expected.

@discordianfish @pgier


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 7dee320..26ed5f5 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -34,7 +34,7 @@ jobs:
     steps:
     - checkout
     - run: sudo pip install codespell
-    - run: codespell --skip=".git,./vendor,ttar,fixtures.ttar,./fixtures,go.mod,go.sum" -L uint,packages\',ded,alo,als,te,sie,hart
+    - run: codespell --skip=".git,./vendor,ttar,fixtures.ttar,./fixtures,go.mod,go.sum" -L uint,packages\',ded,alo,als,te,sie,hart,hda
 
 workflows:
   version: 2
diff --git a/fixtures.ttar b/fixtures.ttar
index 12494d7..4eda11e 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -111,7 +111,7 @@ Max core file size        0                    unlimited            bytes
 Max resident set          unlimited            unlimited            bytes
 Max processes             62898                62898                processes
 Max open files            2048                 4096                 files
-Max locked memory         65536                65536                bytes
+Max locked memory         18446744073708503040 18446744073708503040 bytes
 Max address space         8589934592           unlimited            bytes
 Max file locks            unlimited            unlimited            locks
 Max pending signals       62898                62898                signals
@@ -2129,6 +2129,24 @@ Lines: 6
        4    1FB3C        0          1282A8F                0
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/protocols
+Lines: 14
+protocol  size sockets  memory press maxhdr  slab module     cl co di ac io in de sh ss gs se re sp bi br ha uh gp em
+PACKET    1344      2      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
+PINGv6    1112      0      -1   NI       0   yes  kernel      y  y  y  n  n  y  n  n  y  y  y  y  n  y  y  y  y  y  n
+RAWv6     1112      1      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
+UDPLITEv6 1216      0      57   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  n  n  y  y  y  n
+UDPv6     1216     10      57   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  n  n  y  y  y  n
+TCPv6     2144   1937  1225378   no     320   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
+UNIX      1024    120      -1   NI       0   yes  kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
+UDP-Lite  1024      0      57   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
+PING       904      0      -1   NI       0   yes  kernel      y  y  y  n  n  y  n  n  y  y  y  y  n  y  y  y  y  y  n
+RAW        912      0      -1   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  n  y  y  y  y  n  n
+UDP       1024     73      57   NI       0   yes  kernel      y  y  y  n  y  y  y  n  y  y  y  y  y  n  n  y  y  y  n
+TCP       1984  93064  1225378   yes     320   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y
+NETLINK   1040     16      -1   NI       0   no   kernel      n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n  n
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/proc/net/rpc
 Mode: 755
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -4639,6 +4657,35 @@ Mode: 644
 Directory: fixtures/sys/devices/system
 Mode: 775
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/devices/system/node
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/devices/system/node/node1
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/node/node1/vmstat
+Lines: 6
+nr_free_pages 1
+nr_zone_inactive_anon 2
+nr_zone_active_anon 3
+nr_zone_inactive_file 4
+nr_zone_active_file 5
+nr_zone_unevictable 6
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/devices/system/node/node2
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/node/node2/vmstat
+Lines: 6
+nr_free_pages 7
+nr_zone_inactive_anon 8
+nr_zone_active_anon 9
+nr_zone_inactive_file 10
+nr_zone_active_file 11
+nr_zone_unevictable 12
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/devices/system/clocksource
 Mode: 775
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/mdstat.go b/mdstat.go
index 98e37aa..a5a4ba5 100644
--- a/mdstat.go
+++ b/mdstat.go
@@ -22,8 +22,9 @@ import (
 )
 
 var (
-	statusLineRE   = regexp.MustCompile(`(\d+) blocks .*\[(\d+)/(\d+)\] \[[U_]+\]`)
-	recoveryLineRE = regexp.MustCompile(`\((\d+)/\d+\)`)
+	statusLineRE      = regexp.MustCompile(`(\d+) blocks .*\[(\d+)/(\d+)\] \[[U_]+\]`)
+	recoveryLineRE    = regexp.MustCompile(`\((\d+)/\d+\)`)
+	componentDeviceRE = regexp.MustCompile(`(.*)\[\d+\]`)
 )
 
 // MDStat holds info parsed from /proc/mdstat.
@@ -44,6 +45,8 @@ type MDStat struct {
 	BlocksTotal int64
 	// Number of blocks on the device that are in sync.
 	BlocksSynced int64
+	// Name of md component devices
+	Devices []string
 }
 
 // MDStat parses an mdstat-file (/proc/mdstat) and returns a slice of
@@ -140,6 +143,7 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
 			DisksTotal:    total,
 			BlocksTotal:   size,
 			BlocksSynced:  syncedBlocks,
+			Devices:       evalComponentDevices(deviceFields),
 		})
 	}
 
@@ -195,3 +199,18 @@ func evalRecoveryLine(recoveryLine string) (syncedBlocks int64, err error) {
 
 	return syncedBlocks, nil
 }
+
+func evalComponentDevices(deviceFields []string) []string {
+	mdComponentDevices := make([]string, 0)
+	if len(deviceFields) > 3 {
+		for _, field := range deviceFields[4:] {
+			match := componentDeviceRE.FindStringSubmatch(field)
+			if match == nil {
+				continue
+			}
+			mdComponentDevices = append(mdComponentDevices, match[1])
+		}
+	}
+
+	return mdComponentDevices
+}
diff --git a/mountstats.go b/mountstats.go
index 861ced9..f7a828b 100644
--- a/mountstats.go
+++ b/mountstats.go
@@ -338,12 +338,12 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 		if len(ss) == 0 {
 			break
 		}
-		if len(ss) < 2 {
-			return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
-		}
 
 		switch ss[0] {
 		case fieldOpts:
+			if len(ss) < 2 {
+				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+			}
 			if stats.Opts == nil {
 				stats.Opts = map[string]string{}
 			}
@@ -356,6 +356,9 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 				}
 			}
 		case fieldAge:
+			if len(ss) < 2 {
+				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+			}
 			// Age integer is in seconds
 			d, err := time.ParseDuration(ss[1] + "s")
 			if err != nil {
@@ -364,6 +367,9 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 
 			stats.Age = d
 		case fieldBytes:
+			if len(ss) < 2 {
+				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+			}
 			bstats, err := parseNFSBytesStats(ss[1:])
 			if err != nil {
 				return nil, err
@@ -371,6 +377,9 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 
 			stats.Bytes = *bstats
 		case fieldEvents:
+			if len(ss) < 2 {
+				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+			}
 			estats, err := parseNFSEventsStats(ss[1:])
 			if err != nil {
 				return nil, err
diff --git a/net_protocols.go b/net_protocols.go
new file mode 100644
index 0000000..8c6de37
--- /dev/null
+++ b/net_protocols.go
@@ -0,0 +1,180 @@
+// Copyright 2020 The Prometheus Authors
+// 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.
+
+package procfs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// NetProtocolStats stores the contents from /proc/net/protocols
+type NetProtocolStats map[string]NetProtocolStatLine
+
+// NetProtocolStatLine contains a single line parsed from /proc/net/protocols. We
+// only care about the first six columns as the rest are not likely to change
+// and only serve to provide a set of capabilities for each protocol.
+type NetProtocolStatLine struct {
+	Name         string // 0 The name of the protocol
+	Size         uint64 // 1 The size, in bytes, of a given protocol structure. e.g. sizeof(struct tcp_sock) or sizeof(struct unix_sock)
+	Sockets      int64  // 2 Number of sockets in use by this protocol
+	Memory       int64  // 3 Number of 4KB pages allocated by all sockets of this protocol
+	Pressure     int    // 4 This is either yes, no, or NI (not implemented). For the sake of simplicity we treat NI as not experiencing memory pressure.
+	MaxHeader    uint64 // 5 Protocol specific max header size
+	Slab         bool   // 6 Indicates whether or not memory is allocated from the SLAB
+	ModuleName   string // 7 The name of the module that implemented this protocol or "kernel" if not from a module
+	Capabilities NetProtocolCapabilities
+}
+
+// NetProtocolCapabilities contains a list of capabilities for each protocol
+type NetProtocolCapabilities struct {
+	Close               bool // 8
+	Connect             bool // 9
+	Disconnect          bool // 10
+	Accept              bool // 11
+	IoCtl               bool // 12
+	Init                bool // 13
+	Destroy             bool // 14
+	Shutdown            bool // 15
+	SetSockOpt          bool // 16
+	GetSockOpt          bool // 17
+	SendMsg             bool // 18
+	RecvMsg             bool // 19
+	SendPage            bool // 20
+	Bind                bool // 21
+	BacklogRcv          bool // 22
+	Hash                bool // 23
+	UnHash              bool // 24
+	GetPort             bool // 25
+	EnterMemoryPressure bool // 26
+}
+
+// NetProtocols reads stats from /proc/net/protocols and returns a map of
+// PortocolStatLine entries. As of this writing no official Linux Documentation
+// exists, however the source is fairly self-explanatory and the format seems
+// stable since its introduction in 2.6.12-rc2
+// Linux 2.6.12-rc2 - https://elixir.bootlin.com/linux/v2.6.12-rc2/source/net/core/sock.c#L1452
+// Linux 5.10 - https://elixir.bootlin.com/linux/v5.10.4/source/net/core/sock.c#L3586
+func (fs FS) NetProtocols() (NetProtocolStats, error) {
+	data, err := util.ReadFileNoStat(fs.proc.Path("net/protocols"))
+	if err != nil {
+		return NetProtocolStats{}, err
+	}
+	return parseNetProtocols(bufio.NewScanner(bytes.NewReader(data)))
+}
+
+func parseNetProtocols(s *bufio.Scanner) (NetProtocolStats, error) {
+	nps := NetProtocolStats{}
+
+	// Skip the header line
+	s.Scan()
+
+	for s.Scan() {
+		line, err := nps.parseLine(s.Text())
+		if err != nil {
+			return NetProtocolStats{}, err
+		}
+
+		nps[line.Name] = *line
+	}
+	return nps, nil
+}
+
+func (ps NetProtocolStats) parseLine(rawLine string) (*NetProtocolStatLine, error) {
+	line := &NetProtocolStatLine{Capabilities: NetProtocolCapabilities{}}
+	var err error
+	const enabled = "yes"
+	const disabled = "no"
+
+	fields := strings.Fields(rawLine)
+	line.Name = fields[0]
+	line.Size, err = strconv.ParseUint(fields[1], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	line.Sockets, err = strconv.ParseInt(fields[2], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	line.Memory, err = strconv.ParseInt(fields[3], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	if fields[4] == enabled {
+		line.Pressure = 1
+	} else if fields[4] == disabled {
+		line.Pressure = 0
+	} else {
+		line.Pressure = -1
+	}
+	line.MaxHeader, err = strconv.ParseUint(fields[5], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	if fields[6] == enabled {
+		line.Slab = true
+	} else if fields[6] == disabled {
+		line.Slab = false
+	} else {
+		return nil, fmt.Errorf("unable to parse capability for protocol: %s", line.Name)
+	}
+	line.ModuleName = fields[7]
+
+	err = line.Capabilities.parseCapabilities(fields[8:])
+	if err != nil {
+		return nil, err
+	}
+
+	return line, nil
+}
+
+func (pc *NetProtocolCapabilities) parseCapabilities(capabilities []string) error {
+	// The capabilities are all bools so we can loop over to map them
+	capabilityFields := [...]*bool{
+		&pc.Close,
+		&pc.Connect,
+		&pc.Disconnect,
+		&pc.Accept,
+		&pc.IoCtl,
+		&pc.Init,
+		&pc.Destroy,
+		&pc.Shutdown,
+		&pc.SetSockOpt,
+		&pc.GetSockOpt,
+		&pc.SendMsg,
+		&pc.RecvMsg,
+		&pc.SendPage,
+		&pc.Bind,
+		&pc.BacklogRcv,
+		&pc.Hash,
+		&pc.UnHash,
+		&pc.GetPort,
+		&pc.EnterMemoryPressure,
+	}
+
+	for i := 0; i < len(capabilities); i++ {
+		if capabilities[i] == "y" {
+			*capabilityFields[i] = true
+		} else if capabilities[i] == "n" {
+			*capabilityFields[i] = false
+		} else {
+			return fmt.Errorf("unable to parse capability block for protocol: position %d", i)
+		}
+	}
+	return nil
+}
diff --git a/proc_limits.go b/proc_limits.go
index 91ee24d..f28ab9e 100644
--- a/proc_limits.go
+++ b/proc_limits.go
@@ -26,55 +26,55 @@ import (
 // http://man7.org/linux/man-pages/man2/getrlimit.2.html.
 type ProcLimits struct {
 	// CPU time limit in seconds.
-	CPUTime int64
+	CPUTime uint64
 	// Maximum size of files that the process may create.
-	FileSize int64
+	FileSize uint64
 	// Maximum size of the process's data segment (initialized data,
 	// uninitialized data, and heap).
-	DataSize int64
+	DataSize uint64
 	// Maximum size of the process stack in bytes.
-	StackSize int64
+	StackSize uint64
 	// Maximum size of a core file.
-	CoreFileSize int64
+	CoreFileSize uint64
 	// Limit of the process's resident set in pages.
-	ResidentSet int64
+	ResidentSet uint64
 	// Maximum number of processes that can be created for the real user ID of
 	// the calling process.
-	Processes int64
+	Processes uint64
 	// Value one greater than the maximum file descriptor number that can be
 	// opened by this process.
-	OpenFiles int64
+	OpenFiles uint64
 	// Maximum number of bytes of memory that may be locked into RAM.
-	LockedMemory int64
+	LockedMemory uint64
 	// Maximum size of the process's virtual memory address space in bytes.
-	AddressSpace int64
+	AddressSpace uint64
 	// Limit on the combined number of flock(2) locks and fcntl(2) leases that
 	// this process may establish.
-	FileLocks int64
+	FileLocks uint64
 	// Limit of signals that may be queued for the real user ID of the calling
 	// process.
-	PendingSignals int64
+	PendingSignals uint64
 	// Limit on the number of bytes that can be allocated for POSIX message
 	// queues for the real user ID of the calling process.
-	MsqqueueSize int64
+	MsqqueueSize uint64
 	// Limit of the nice priority set using setpriority(2) or nice(2).
-	NicePriority int64
+	NicePriority uint64
 	// Limit of the real-time priority set using sched_setscheduler(2) or
 	// sched_setparam(2).
-	RealtimePriority int64
+	RealtimePriority uint64
 	// Limit (in microseconds) on the amount of CPU time that a process
 	// scheduled under a real-time scheduling policy may consume without making
 	// a blocking system call.
-	RealtimeTimeout int64
+	RealtimeTimeout uint64
 }
 
 const (
-	limitsFields    = 3
+	limitsFields    = 4
 	limitsUnlimited = "unlimited"
 )
 
 var (
-	limitsDelimiter = regexp.MustCompile("  +")
+	limitsMatch = regexp.MustCompile(`(Max \w+\s{0,1}?\w*\s{0,1}\w*)\s{2,}(\w+)\s+(\w+)`)
 )
 
 // NewLimits returns the current soft limits of the process.
@@ -96,46 +96,50 @@ func (p Proc) Limits() (ProcLimits, error) {
 		l = ProcLimits{}
 		s = bufio.NewScanner(f)
 	)
+
+	s.Scan() // Skip limits header
+
 	for s.Scan() {
-		fields := limitsDelimiter.Split(s.Text(), limitsFields)
+		//fields := limitsMatch.Split(s.Text(), limitsFields)
+		fields := limitsMatch.FindStringSubmatch(s.Text())
 		if len(fields) != limitsFields {
 			return ProcLimits{}, fmt.Errorf(
 				"couldn't parse %s line %s", f.Name(), s.Text())
 		}
 
-		switch fields[0] {
+		switch fields[1] {
 		case "Max cpu time":
-			l.CPUTime, err = parseInt(fields[1])
+			l.CPUTime, err = parseUint(fields[2])
 		case "Max file size":
-			l.FileSize, err = parseInt(fields[1])
+			l.FileSize, err = parseUint(fields[2])
 		case "Max data size":
-			l.DataSize, err = parseInt(fields[1])
+			l.DataSize, err = parseUint(fields[2])
 		case "Max stack size":
-			l.StackSize, err = parseInt(fields[1])
+			l.StackSize, err = parseUint(fields[2])
 		case "Max core file size":
-			l.CoreFileSize, err = parseInt(fields[1])
+			l.CoreFileSize, err = parseUint(fields[2])
 		case "Max resident set":
-			l.ResidentSet, err = parseInt(fields[1])
+			l.ResidentSet, err = parseUint(fields[2])
 		case "Max processes":
-			l.Processes, err = parseInt(fields[1])
+			l.Processes, err = parseUint(fields[2])
 		case "Max open files":
-			l.OpenFiles, err = parseInt(fields[1])
+			l.OpenFiles, err = parseUint(fields[2])
 		case "Max locked memory":
-			l.LockedMemory, err = parseInt(fields[1])
+			l.LockedMemory, err = parseUint(fields[2])
 		case "Max address space":
-			l.AddressSpace, err = parseInt(fields[1])
+			l.AddressSpace, err = parseUint(fields[2])
 		case "Max file locks":
-			l.FileLocks, err = parseInt(fields[1])
+			l.FileLocks, err = parseUint(fields[2])
 		case "Max pending signals":
-			l.PendingSignals, err = parseInt(fields[1])
+			l.PendingSignals, err = parseUint(fields[2])
 		case "Max msgqueue size":
-			l.MsqqueueSize, err = parseInt(fields[1])
+			l.MsqqueueSize, err = parseUint(fields[2])
 		case "Max nice priority":
-			l.NicePriority, err = parseInt(fields[1])
+			l.NicePriority, err = parseUint(fields[2])
 		case "Max realtime priority":
-			l.RealtimePriority, err = parseInt(fields[1])
+			l.RealtimePriority, err = parseUint(fields[2])
 		case "Max realtime timeout":
-			l.RealtimeTimeout, err = parseInt(fields[1])
+			l.RealtimeTimeout, err = parseUint(fields[2])
 		}
 		if err != nil {
 			return ProcLimits{}, err
@@ -145,11 +149,11 @@ func (p Proc) Limits() (ProcLimits, error) {
 	return l, s.Err()
 }
 
-func parseInt(s string) (int64, error) {
+func parseUint(s string) (uint64, error) {
 	if s == limitsUnlimited {
-		return -1, nil
+		return 18446744073709551615, nil
 	}
-	i, err := strconv.ParseInt(s, 10, 64)
+	i, err := strconv.ParseUint(s, 10, 64)
 	if err != nil {
 		return 0, fmt.Errorf("couldn't parse value %s: %s", s, err)
 	}
diff --git a/sysfs/vmstat_numa.go b/sysfs/vmstat_numa.go
new file mode 100644
index 0000000..d71cd4a
--- /dev/null
+++ b/sysfs/vmstat_numa.go
@@ -0,0 +1,246 @@
+// Copyright 2020 The Prometheus Authors
+// 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.
+
+package sysfs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+var (
+	nodePattern      = "devices/system/node/node[0-9]*"
+	nodeNumberRegexp = regexp.MustCompile(`.*devices/system/node/node([0-9]*)`)
+)
+
+type VMStat struct {
+	NrFreePages                uint64
+	NrZoneInactiveAnon         uint64
+	NrZoneActiveAnon           uint64
+	NrZoneInactiveFile         uint64
+	NrZoneActiveFile           uint64
+	NrZoneUnevictable          uint64
+	NrZoneWritePending         uint64
+	NrMlock                    uint64
+	NrPageTablePages           uint64
+	NrKernelStack              uint64
+	NrBounce                   uint64
+	NrZspages                  uint64
+	NrFreeCma                  uint64
+	NumaHit                    uint64
+	NumaMiss                   uint64
+	NumaForeign                uint64
+	NumaInterleave             uint64
+	NumaLocal                  uint64
+	NumaOther                  uint64
+	NrInactiveAnon             uint64
+	NrActiveAnon               uint64
+	NrInactiveFile             uint64
+	NrActiveFile               uint64
+	NrUnevictable              uint64
+	NrSlabReclaimable          uint64
+	NrSlabUnreclaimable        uint64
+	NrIsolatedAnon             uint64
+	NrIsolatedFile             uint64
+	WorkingsetNodes            uint64
+	WorkingsetRefault          uint64
+	WorkingsetActivate         uint64
+	WorkingsetRestore          uint64
+	WorkingsetNodereclaim      uint64
+	NrAnonPages                uint64
+	NrMapped                   uint64
+	NrFilePages                uint64
+	NrDirty                    uint64
+	NrWriteback                uint64
+	NrWritebackTemp            uint64
+	NrShmem                    uint64
+	NrShmemHugepages           uint64
+	NrShmemPmdmapped           uint64
+	NrFileHugepages            uint64
+	NrFilePmdmapped            uint64
+	NrAnonTransparentHugepages uint64
+	NrVmscanWrite              uint64
+	NrVmscanImmediateReclaim   uint64
+	NrDirtied                  uint64
+	NrWritten                  uint64
+	NrKernelMiscReclaimable    uint64
+	NrFollPinAcquired          uint64
+	NrFollPinReleased          uint64
+}
+
+func (fs FS) VMStatNUMA() (map[int]VMStat, error) {
+	m := make(map[int]VMStat)
+	nodes, err := filepath.Glob(fs.sys.Path(nodePattern))
+	if err != nil {
+		return nil, err
+	}
+
+	for _, node := range nodes {
+		nodeNumbers := nodeNumberRegexp.FindStringSubmatch(node)
+		if len(nodeNumbers) != 2 {
+			continue
+		}
+		nodeNumber, err := strconv.Atoi(nodeNumbers[1])
+		if err != nil {
+			return nil, err
+		}
+		file, err := util.ReadFileNoStat(filepath.Join(node, "vmstat"))
+		if err != nil {
+			return nil, err
+		}
+		nodeStats, err := parseVMStatNUMA(file)
+		if err != nil {
+			return nil, err
+		}
+		m[nodeNumber] = nodeStats
+	}
+	return m, nil
+}
+
+func parseVMStatNUMA(r []byte) (VMStat, error) {
+	var (
+		vmStat  = VMStat{}
+		scanner = bufio.NewScanner(bytes.NewReader(r))
+	)
+
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if line == "" {
+			continue
+		}
+		parts := strings.Fields(line)
+		if len(parts) != 2 {
+			return vmStat, fmt.Errorf("line scan did not return 2 fields: %s", line)
+		}
+
+		fv, err := strconv.ParseUint(parts[1], 10, 64)
+		if err != nil {
+			return vmStat, fmt.Errorf("invalid value in vmstat: %w", err)
+		}
+		switch parts[0] {
+		case "nr_free_pages":
+			vmStat.NrFreePages = fv
+		case "nr_zone_inactive_anon":
+			vmStat.NrZoneInactiveAnon = fv
+		case "nr_zone_active_anon":
+			vmStat.NrZoneActiveAnon = fv
+		case "nr_zone_inactive_file":
+			vmStat.NrZoneActiveFile = fv
+		case "nr_zone_active_file":
+			vmStat.NrZoneActiveFile = fv
+		case "nr_zone_unevictable":
+			vmStat.NrZoneUnevictable = fv
+		case "nr_zone_write_pending":
+			vmStat.NrZoneWritePending = fv
+		case "nr_mlock":
+			vmStat.NrMlock = fv
+		case "nr_page_table_pages":
+			vmStat.NrPageTablePages = fv
+		case "nr_kernel_stack":
+			vmStat.NrKernelStack = fv
+		case "nr_bounce":
+			vmStat.NrBounce = fv
+		case "nr_zspages":
+			vmStat.NrZspages = fv
+		case "nr_free_cma":
+			vmStat.NrFreeCma = fv
+		case "numa_hit":
+			vmStat.NumaHit = fv
+		case "numa_miss":
+			vmStat.NumaMiss = fv
+		case "numa_foreign":
+			vmStat.NumaForeign = fv
+		case "numa_interleave":
+			vmStat.NumaInterleave = fv
+		case "numa_local":
+			vmStat.NumaLocal = fv
+		case "numa_other":
+			vmStat.NumaOther = fv
+		case "nr_inactive_anon":
+			vmStat.NrInactiveAnon = fv
+		case "nr_active_anon":
+			vmStat.NrActiveAnon = fv
+		case "nr_inactive_file":
+			vmStat.NrInactiveFile = fv
+		case "nr_active_file":
+			vmStat.NrActiveFile = fv
+		case "nr_unevictable":
+			vmStat.NrUnevictable = fv
+		case "nr_slab_reclaimable":
+			vmStat.NrSlabReclaimable = fv
+		case "nr_slab_unreclaimable":
+			vmStat.NrSlabUnreclaimable = fv
+		case "nr_isolated_anon":
+			vmStat.NrIsolatedAnon = fv
+		case "nr_isolated_file":
+			vmStat.NrIsolatedFile = fv
+		case "workingset_nodes":
+			vmStat.WorkingsetNodes = fv
+		case "workingset_refault":
+			vmStat.WorkingsetRefault = fv
+		case "workingset_activate":
+			vmStat.WorkingsetActivate = fv
+		case "workingset_restore":
+			vmStat.WorkingsetRestore = fv
+		case "workingset_nodereclaim":
+			vmStat.WorkingsetNodereclaim = fv
+		case "nr_anon_pages":
+			vmStat.NrAnonPages = fv
+		case "nr_mapped":
+			vmStat.NrMapped = fv
+		case "nr_file_pages":
+			vmStat.NrFilePages = fv
+		case "nr_dirty":
+			vmStat.NrDirty = fv
+		case "nr_writeback":
+			vmStat.NrWriteback = fv
+		case "nr_writeback_temp":
+			vmStat.NrWritebackTemp = fv
+		case "nr_shmem":
+			vmStat.NrShmem = fv
+		case "nr_shmem_hugepages":
+			vmStat.NrShmemHugepages = fv
+		case "nr_shmem_pmdmapped":
+			vmStat.NrShmemPmdmapped = fv
+		case "nr_file_hugepages":
+			vmStat.NrFileHugepages = fv
+		case "nr_file_pmdmapped":
+			vmStat.NrFilePmdmapped = fv
+		case "nr_anon_transparent_hugepages":
+			vmStat.NrAnonTransparentHugepages = fv
+		case "nr_vmscan_write":
+			vmStat.NrVmscanWrite = fv
+		case "nr_vmscan_immediate_reclaim":
+			vmStat.NrVmscanImmediateReclaim = fv
+		case "nr_dirtied":
+			vmStat.NrDirtied = fv
+		case "nr_written":
+			vmStat.NrWritten = fv
+		case "nr_kernel_misc_reclaimable":
+			vmStat.NrKernelMiscReclaimable = fv
+		case "nr_foll_pin_acquired":
+			vmStat.NrFollPinAcquired = fv
+		case "nr_foll_pin_released":
+			vmStat.NrFollPinReleased = fv
+		}
+
+	}
+	return vmStat, scanner.Err()
+}
diff --git a/xfs/parse.go b/xfs/parse.go
index ba3afea..260501e 100644
--- a/xfs/parse.go
+++ b/xfs/parse.go
@@ -286,8 +286,8 @@ func readWriteStats(us []uint32) (ReadWriteStats, error) {
 	}
 
 	return ReadWriteStats{
-		Read:  us[0],
-		Write: us[1],
+		Write: us[0],
+		Read:  us[1],
 	}, nil
 }
 
diff --git a/xfs/xfs.go b/xfs/xfs.go
index cc8b36b..79bba65 100644
--- a/xfs/xfs.go
+++ b/xfs/xfs.go
@@ -124,8 +124,8 @@ type LogOperationStats struct {
 // ReadWriteStats contains statistics regarding the number of read and write
 // system calls for XFS filesystems.
 type ReadWriteStats struct {
-	Read  uint32
 	Write uint32
+	Read  uint32
 }
 
 // AttributeOperationStats contains statistics regarding manipulation of


Test patch:
diff --git a/mdstat_test.go b/mdstat_test.go
index 5404765..1b9a8ea 100644
--- a/mdstat_test.go
+++ b/mdstat_test.go
@@ -14,6 +14,7 @@
 package procfs
 
 import "testing"
+import "github.com/google/go-cmp/cmp"
 
 func TestFS_MDStat(t *testing.T) {
 	fs := getProcFixtures(t)
@@ -24,30 +25,30 @@ func TestFS_MDStat(t *testing.T) {
 	}
 
 	refs := map[string]MDStat{
-		"md127": {Name: "md127", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 312319552, BlocksSynced: 312319552},
-		"md0":   {Name: "md0", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 248896, BlocksSynced: 248896},
-		"md4":   {Name: "md4", ActivityState: "inactive", DisksActive: 0, DisksTotal: 0, DisksFailed: 1, DisksSpare: 1, BlocksTotal: 4883648, BlocksSynced: 4883648},
-		"md6":   {Name: "md6", ActivityState: "recovering", DisksActive: 1, DisksTotal: 2, DisksFailed: 1, DisksSpare: 1, BlocksTotal: 195310144, BlocksSynced: 16775552},
-		"md3":   {Name: "md3", ActivityState: "active", DisksActive: 8, DisksTotal: 8, DisksFailed: 0, DisksSpare: 2, BlocksTotal: 5853468288, BlocksSynced: 5853468288},
-		"md8":   {Name: "md8", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 2, BlocksTotal: 195310144, BlocksSynced: 16775552},
-		"md7":   {Name: "md7", ActivityState: "active", DisksActive: 3, DisksTotal: 4, DisksFailed: 1, DisksSpare: 0, BlocksTotal: 7813735424, BlocksSynced: 7813735424},
-		"md9":   {Name: "md9", ActivityState: "resyncing", DisksActive: 4, DisksTotal: 4, DisksSpare: 1, DisksFailed: 2, BlocksTotal: 523968, BlocksSynced: 0},
-		"md10":  {Name: "md10", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 314159265, BlocksSynced: 314159265},
-		"md11":  {Name: "md11", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, DisksFailed: 1, DisksSpare: 2, BlocksTotal: 4190208, BlocksSynced: 0},
-		"md12":  {Name: "md12", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksSpare: 0, DisksFailed: 0, BlocksTotal: 3886394368, BlocksSynced: 3886394368},
-		"md120": {Name: "md120", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 2095104, BlocksSynced: 2095104},
-		"md126": {Name: "md126", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 1855870976, BlocksSynced: 1855870976},
-		"md219": {Name: "md219", ActivityState: "inactive", DisksTotal: 0, DisksFailed: 0, DisksActive: 0, DisksSpare: 3, BlocksTotal: 7932, BlocksSynced: 7932},
-		"md00":  {Name: "md00", ActivityState: "active", DisksActive: 1, DisksTotal: 1, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 4186624, BlocksSynced: 4186624},
-		"md101": {Name: "md101", ActivityState: "active", DisksActive: 3, DisksTotal: 3, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 322560, BlocksSynced: 322560},
-		"md201": {Name: "md201", ActivityState: "checking", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 1993728, BlocksSynced: 114176},
+		"md127": {Name: "md127", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 312319552, BlocksSynced: 312319552, Devices: []string{"sdi2", "sdj2"}},
+		"md0":   {Name: "md0", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 248896, BlocksSynced: 248896, Devices: []string{"sdi1", "sdj1"}},
+		"md4":   {Name: "md4", ActivityState: "inactive", DisksActive: 0, DisksTotal: 0, DisksFailed: 1, DisksSpare: 1, BlocksTotal: 4883648, BlocksSynced: 4883648, Devices: []string{"sda3", "sdb3"}},
+		"md6":   {Name: "md6", ActivityState: "recovering", DisksActive: 1, DisksTotal: 2, DisksFailed: 1, DisksSpare: 1, BlocksTotal: 195310144, BlocksSynced: 16775552, Devices: []string{"sdb2", "sdc", "sda2"}},
+		"md3":   {Name: "md3", ActivityState: "active", DisksActive: 8, DisksTotal: 8, DisksFailed: 0, DisksSpare: 2, BlocksTotal: 5853468288, BlocksSynced: 5853468288, Devices: []string{"sda1", "sdh1", "sdg1", "sdf1", "sde1", "sdd1", "sdc1", "sdb1", "sdd1", "sdd2"}},
+		"md8":   {Name: "md8", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 2, BlocksTotal: 195310144, BlocksSynced: 16775552, Devices: []string{"sdb1", "sda1", "sdc", "sde"}},
+		"md7":   {Name: "md7", ActivityState: "active", DisksActive: 3, DisksTotal: 4, DisksFailed: 1, DisksSpare: 0, BlocksTotal: 7813735424, BlocksSynced: 7813735424, Devices: []string{"sdb1", "sde1", "sdd1", "sdc1"}},
+		"md9":   {Name: "md9", ActivityState: "resyncing", DisksActive: 4, DisksTotal: 4, DisksSpare: 1, DisksFailed: 2, BlocksTotal: 523968, BlocksSynced: 0, Devices: []string{"sdc2", "sdd2", "sdb2", "sda2", "sde", "sdf", "sdg"}},
+		"md10":  {Name: "md10", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 314159265, BlocksSynced: 314159265, Devices: []string{"sda1", "sdb1"}},
+		"md11":  {Name: "md11", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, DisksFailed: 1, DisksSpare: 2, BlocksTotal: 4190208, BlocksSynced: 0, Devices: []string{"sdb2", "sdc2", "sdc3", "hda", "ssdc2"}},
+		"md12":  {Name: "md12", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksSpare: 0, DisksFailed: 0, BlocksTotal: 3886394368, BlocksSynced: 3886394368, Devices: []string{"sdc2", "sdd2"}},
+		"md120": {Name: "md120", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 2095104, BlocksSynced: 2095104, Devices: []string{"sda1", "sdb1"}},
+		"md126": {Name: "md126", ActivityState: "active", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 1855870976, BlocksSynced: 1855870976, Devices: []string{"sdb", "sdc"}},
+		"md219": {Name: "md219", ActivityState: "inactive", DisksTotal: 0, DisksFailed: 0, DisksActive: 0, DisksSpare: 3, BlocksTotal: 7932, BlocksSynced: 7932, Devices: []string{"sdc", "sda"}},
+		"md00":  {Name: "md00", ActivityState: "active", DisksActive: 1, DisksTotal: 1, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 4186624, BlocksSynced: 4186624, Devices: []string{"xvdb"}},
+		"md101": {Name: "md101", ActivityState: "active", DisksActive: 3, DisksTotal: 3, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 322560, BlocksSynced: 322560, Devices: []string{"sdb", "sdd", "sdc"}},
+		"md201": {Name: "md201", ActivityState: "checking", DisksActive: 2, DisksTotal: 2, DisksFailed: 0, DisksSpare: 0, BlocksTotal: 1993728, BlocksSynced: 114176, Devices: []string{"sda3", "sdb3"}},
 	}
 
 	if want, have := len(refs), len(mdStats); want != have {
 		t.Errorf("want %d parsed md-devices, have %d", want, have)
 	}
 	for _, md := range mdStats {
-		if want, have := refs[md.Name], md; want != have {
+		if want, have := refs[md.Name], md; !cmp.Equal(want, have) {
 			t.Errorf("%s: want %v, have %v", md.Name, want, have)
 		}
 	}
diff --git a/mountstats_test.go b/mountstats_test.go
index 146df96..391d50c 100644
--- a/mountstats_test.go
+++ b/mountstats_test.go
@@ -64,7 +64,7 @@ func TestMountStats(t *testing.T) {
 		},
 		{
 			name:    "NFSv4 device with too little info",
-			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nhello",
+			s:       "device 192.168.1.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nopts:",
 			invalid: true,
 		},
 		{
@@ -253,6 +253,26 @@ func TestMountStats(t *testing.T) {
 				},
 			}},
 		},
+		{
+			name: "NFS4.1 device with multiline impl_id OK",
+			s:    "device 192.168.0.1:/srv mounted on /mnt/nfs with fstype nfs4 statvers=1.1\nopts: rw,vers=4.1,rsize=131072,wsize=131072,namlen=255,acregmin=3,acregmax=60,acdirmin=30,acdirmax=60,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.0.2,fsc,local_lock=none\nage: 1234567\nimpl_id: name='FreeBSD 11.2-STABLE #0 r325575+c9231c7d6bd(HEAD): Mon Nov 18 22:46:47 UTC 2019\nuser@host:/path/to/something'\n,domain='something.org',date='1293840000,0'",
+			mounts: []*Mount{{
+				Device: "192.168.0.1:/srv",
+				Mount:  "/mnt/nfs",
+				Type:   "nfs4",
+				Stats: &MountStatsNFS{
+					StatVersion: "1.1",
+					Opts: map[string]string{"rw": "", "vers": "4.1",
+						"rsize": "131072", "wsize": "131072", "namlen": "255", "acregmin": "3",
+						"acregmax": "60", "acdirmin": "30", "acdirmax": "60", "fsc": "", "hard": "",
+						"proto": "tcp", "timeo": "600", "retrans": "2",
+						"sec": "sys", "clientaddr": "192.168.0.2",
+						"local_lock": "none",
+					},
+					Age: 1234567 * time.Second,
+				},
+			}},
+		},
 		{
 			name: "fixtures/proc OK",
 			mounts: []*Mount{
diff --git a/net_protocols_test.go b/net_protocols_test.go
new file mode 100644
index 0000000..cabbd93
--- /dev/null
+++ b/net_protocols_test.go
@@ -0,0 +1,83 @@
+// Copyright 2020 The Prometheus Authors
+// 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.
+package procfs
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestParseCapabilities(t *testing.T) {
+	rawStr := "y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y\n"
+	have := NetProtocolCapabilities{}
+	err := have.parseCapabilities(strings.Fields(rawStr))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := NetProtocolCapabilities{true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true}
+	if want != have {
+		t.Errorf("want %+v\nhave %+v\n", want, have)
+	}
+}
+
+func TestProtocolsParseLine(t *testing.T) {
+	rawStr := "TCP       1984  93064  1225378   no     320   yes  kernel      y  y  y  y  y  y  y  y  y  y  y  y  y  n  y  y  y  y  y\n"
+	protocols := NetProtocolStats{}
+	have, err := protocols.parseLine(rawStr)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := NetProtocolStatLine{"TCP", 1984, 93064, 1225378, 0, 320, true, "kernel", NetProtocolCapabilities{true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true}}
+	if want != *have {
+		t.Errorf("want %+v\nhave %+v\n", want, have)
+	}
+}
+
+func TestProtocolsParseProtocols(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	protocolStats, err := fs.NetProtocols()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	lines := map[string]NetProtocolStatLine{
+		"PACKET":    {"PACKET", 1344, 2, -1, -1, 0, false, "kernel", NetProtocolCapabilities{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}},
+		"PINGv6":    {"PINGv6", 1112, 0, -1, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, false, true, false, false, true, true, true, true, false, true, true, true, true, true, false}},
+		"RAWv6":     {"RAWv6", 1112, 1, -1, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, false, true, true, true, true, false, false}},
+		"UDPLITEv6": {"UDPLITEv6", 1216, 0, 57, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, false, false, false, true, true, true, false}},
+		"UDPv6":     {"UDPv6", 1216, 10, 57, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, false, false, false, true, true, true, false}},
+		"TCPv6":     {"TCPv6", 2144, 1937, 1225378, 0, 320, true, "kernel", NetProtocolCapabilities{true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true}},
+		"UNIX":      {"UNIX", 1024, 120, -1, -1, 0, true, "kernel", NetProtocolCapabilities{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}},
+		"UDP-Lite":  {"UDP-Lite", 1024, 0, 57, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, true, false, false, true, true, true, false}},
+		"PING":      {"PING", 904, 0, -1, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, false, true, false, false, true, true, true, true, false, true, true, true, true, true, false}},
+		"RAW":       {"RAW", 912, 0, -1, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, false, true, true, true, true, false, false}},
+		"UDP":       {"UDP", 1024, 73, 57, -1, 0, true, "kernel", NetProtocolCapabilities{true, true, true, false, true, true, true, false, true, true, true, true, true, false, false, true, true, true, false}},
+		"TCP":       {"TCP", 1984, 93064, 1225378, 1, 320, true, "kernel", NetProtocolCapabilities{true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true}},
+		"NETLINK":   {"NETLINK", 1040, 16, -1, -1, 0, false, "kernel", NetProtocolCapabilities{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}},
+	}
+
+	if want, have := len(lines), len(protocolStats); want != have {
+		t.Errorf("want %d parsed net/protocols lines, have %d", want, have)
+	}
+	for _, line := range protocolStats {
+		if want, have := lines[line.Name], line; want != have {
+			t.Errorf("%s: want %v, have %v", line.Name, want, have)
+		}
+	}
+}
diff --git a/proc_limits_test.go b/proc_limits_test.go
index 9148e28..bb8ae61 100644
--- a/proc_limits_test.go
+++ b/proc_limits_test.go
@@ -28,10 +28,10 @@ func TestLimits(t *testing.T) {
 
 	for _, test := range []struct {
 		name string
-		want int64
-		have int64
+		want uint64
+		have uint64
 	}{
-		{name: "cpu time", want: -1, have: l.CPUTime},
+		{name: "cpu time", want: 18446744073709551615, have: l.CPUTime},
 		{name: "open files", want: 2048, have: l.OpenFiles},
 		{name: "msgqueue size", want: 819200, have: l.MsqqueueSize},
 		{name: "nice priority", want: 0, have: l.NicePriority},
diff --git a/sysfs/vmstat_numa_test.go b/sysfs/vmstat_numa_test.go
new file mode 100644
index 0000000..64977b9
--- /dev/null
+++ b/sysfs/vmstat_numa_test.go
@@ -0,0 +1,44 @@
+// Copyright 2020 The Prometheus Authors
+// 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.
+
+package sysfs
+
+import (
+	"testing"
+)
+
+func TestParseVMStatNUMA(t *testing.T) {
+	fs, err := NewFS(sysTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	vmstat, err := fs.VMStatNUMA()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if want, got := uint64(1), vmstat[1].NrFreePages; want != got {
+		t.Errorf("want vmstat stat nr_free_pages value %d, got %d", want, got)
+	}
+
+	if want, got := uint64(5), vmstat[1].NrZoneActiveFile; want != got {
+		t.Errorf("want numa stat nr_zone_active_file %d, got %d", want, got)
+	}
+	if want, got := uint64(7), vmstat[2].NrFreePages; want != got {
+		t.Errorf("want vmstat stat nr_free_pages value %d, got %d", want, got)
+	}
+
+	if want, got := uint64(11), vmstat[2].NrZoneActiveFile; want != got {
+		t.Errorf("want numa stat nr_zone_active_file %d, got %d", want, got)
+	}
+}
diff --git a/xfs/parse_test.go b/xfs/parse_test.go
index b799f4b..4989eca 100644
--- a/xfs/parse_test.go
+++ b/xfs/parse_test.go
@@ -199,8 +199,8 @@ func TestParseStats(t *testing.T) {
 			s:    "rw 1 2",
 			stats: &xfs.Stats{
 				ReadWrite: xfs.ReadWriteStats{
-					Read:  1,
-					Write: 2,
+					Write: 1,
+					Read:  2,
 				},
 			},
 		},
@@ -577,8 +577,8 @@ func TestParseStats(t *testing.T) {
 					ForceSleep:        739,
 				},
 				ReadWrite: xfs.ReadWriteStats{
-					Read:  107739,
-					Write: 94045,
+					Write: 107739,
+					Read:  94045,
 				},
 				AttributeOperation: xfs.AttributeOperationStats{
 					Get:    4,