Repository: prometheus/procfs
Pull request #191 (2019-07-13 16:22:31)

Fix seconds reported by schedstat
As explained further in the code comments, the previous implementation
was based on incorrect documentation and reported values 10000000 times
too large for the values in seconds.

Signed-off-by: Phil Frost <phil@postmates.com>

bitglue: @discordianfish @pgier simple bugfix of recently added functionality, ready for review.
bitglue: @discordianfish @pgier bump
bitglue: @discordianfish @pgier rebump, easy bug fix, needs review.
bitglue: @discordianfish @pgier bump bump bump bump bump bump bump bump bump bump bump 
SuperQ: Hrm, I really think we need some additional testing here to verify what's correct, and when exactly things changed in the kernel. We still support very old kernels, like RHEL 6 contains 2.6.32.
SuperQ: I ran a quick test, booting up a CentOS 6 box. It looks like it's been in NS for a long time.

```console
[bjk@bjk-centos-6 ~]$ cat /proc/schedstat 
version 15
timestamp 4294786116
cpu0 0 0 65279 8841 37792 37792 80015632879 18335798709 56073
[bjk@bjk-centos-6 ~]$ uptime
 07:47:20 up 2 min,  1 user,  load average: 0.72, 0.38, 0.14
[bjk@bjk-centos-6 ~]$ uname -a
Linux bjk-centos-6 2.6.32-754.15.3.el6.x86_64 #1 SMP Tue Jun 18 16:25:32 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
```

Field 7 indicates ~80.015 seconds of CPU time used with NS, and an absurd number if it were in Jiffies.
bitglue: The stats went from jiffies to ns in https://github.com/torvalds/linux/commit/425e0968a25fa3f111f9919964cac079738140b5

The log for that commit says "no code changes are caused by this patch." but I don't think that's true. Look at the implementation of `sched_info_arrive()`; the old implementation has

    unsigned long now = jiffies, delta_jiffies = 0;

and the new implementation

    unsigned long long now = sched_clock(), delta = 0;

[`sched_clock()` returns time in nanoseconds](https://github.com/torvalds/linux/blob/425e0968a25fa3f111f9919964cac079738140b5/arch/i386/kernel/tsc.c#L100-L119).

This patch is part of the CFS implementation, which was first released in 2.6.23-rc1. Prior to that, stats were in jiffies. After that, stats were in ns.

Unfortunately, `SCHEDSTAT_VERSION` is set to 14 in either case. Between this, the commit message above ("no code changes"), and the documentation that was never updated, I guess this is a backwards-incompatible change which was introduced unintentionally.

So we don't have a straightforward way to know how to parse schedstats. But we could perhaps implement some heuristic, for example compare the counters to the `timestamp` field (which in all cases, does actually seem to be in jiffies) and if the counters are absurdly larger than `timestamp`, then assume they are in ns. If they are all less than `timestamp`, jiffies.

Or we can just forget about it, and people running kernels older than 2.6.23 can just have wrong metrics.
SuperQ: Well, good news, Go doesn't support older than 2.6.23, so we're safe to assume the nanosecond behavior. 😁

Patch:
diff --git a/fixtures.ttar b/fixtures.ttar
index b743744..75dd29d 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -909,6 +909,272 @@ Lines: 1
 0
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/zoneinfo
+Lines: 262
+Node 0, zone      DMA
+  per-node stats
+      nr_inactive_anon 230981
+      nr_active_anon 547580
+      nr_inactive_file 316904
+      nr_active_file 346282
+      nr_unevictable 115467
+      nr_slab_reclaimable 131220
+      nr_slab_unreclaimable 47320
+      nr_isolated_anon 0
+      nr_isolated_file 0
+      workingset_nodes 11627
+      workingset_refault 466886
+      workingset_activate 276925
+      workingset_restore 84055
+      workingset_nodereclaim 487
+      nr_anon_pages 795576
+      nr_mapped    215483
+      nr_file_pages 761874
+      nr_dirty     908
+      nr_writeback 0
+      nr_writeback_temp 0
+      nr_shmem     224925
+      nr_shmem_hugepages 0
+      nr_shmem_pmdmapped 0
+      nr_anon_transparent_hugepages 0
+      nr_unstable  0
+      nr_vmscan_write 12950
+      nr_vmscan_immediate_reclaim 3033
+      nr_dirtied   8007423
+      nr_written   7752121
+      nr_kernel_misc_reclaimable 0
+  pages free     3952
+        min      33
+        low      41
+        high     49
+        spanned  4095
+        present  3975
+        managed  3956
+        protection: (0, 2877, 7826, 7826, 7826)
+      nr_free_pages 3952
+      nr_zone_inactive_anon 0
+      nr_zone_active_anon 0
+      nr_zone_inactive_file 0
+      nr_zone_active_file 0
+      nr_zone_unevictable 0
+      nr_zone_write_pending 0
+      nr_mlock     0
+      nr_page_table_pages 0
+      nr_kernel_stack 0
+      nr_bounce    0
+      nr_zspages   0
+      nr_free_cma  0
+      numa_hit     1
+      numa_miss    0
+      numa_foreign 0
+      numa_interleave 0
+      numa_local   1
+      numa_other   0
+  pagesets
+    cpu: 0
+              count: 0
+              high:  0
+              batch: 1
+  vm stats threshold: 8
+    cpu: 1
+              count: 0
+              high:  0
+              batch: 1
+  vm stats threshold: 8
+    cpu: 2
+              count: 0
+              high:  0
+              batch: 1
+  vm stats threshold: 8
+    cpu: 3
+              count: 0
+              high:  0
+              batch: 1
+  vm stats threshold: 8
+    cpu: 4
+              count: 0
+              high:  0
+              batch: 1
+  vm stats threshold: 8
+    cpu: 5
+              count: 0
+              high:  0
+              batch: 1
+  vm stats threshold: 8
+    cpu: 6
+              count: 0
+              high:  0
+              batch: 1
+  vm stats threshold: 8
+    cpu: 7
+              count: 0
+              high:  0
+              batch: 1
+  vm stats threshold: 8
+  node_unreclaimable:  0
+  start_pfn:           1
+Node 0, zone    DMA32
+  pages free     204252
+        min      19510
+        low      21059
+        high     22608
+        spanned  1044480
+        present  759231
+        managed  742806
+        protection: (0, 0, 4949, 4949, 4949)
+      nr_free_pages 204252
+      nr_zone_inactive_anon 118558
+      nr_zone_active_anon 106598
+      nr_zone_inactive_file 75475
+      nr_zone_active_file 70293
+      nr_zone_unevictable 66195
+      nr_zone_write_pending 64
+      nr_mlock     4
+      nr_page_table_pages 1756
+      nr_kernel_stack 2208
+      nr_bounce    0
+      nr_zspages   0
+      nr_free_cma  0
+      numa_hit     113952967
+      numa_miss    0
+      numa_foreign 0
+      numa_interleave 0
+      numa_local   113952967
+      numa_other   0
+  pagesets
+    cpu: 0
+              count: 345
+              high:  378
+              batch: 63
+  vm stats threshold: 48
+    cpu: 1
+              count: 356
+              high:  378
+              batch: 63
+  vm stats threshold: 48
+    cpu: 2
+              count: 325
+              high:  378
+              batch: 63
+  vm stats threshold: 48
+    cpu: 3
+              count: 346
+              high:  378
+              batch: 63
+  vm stats threshold: 48
+    cpu: 4
+              count: 321
+              high:  378
+              batch: 63
+  vm stats threshold: 48
+    cpu: 5
+              count: 316
+              high:  378
+              batch: 63
+  vm stats threshold: 48
+    cpu: 6
+              count: 373
+              high:  378
+              batch: 63
+  vm stats threshold: 48
+    cpu: 7
+              count: 339
+              high:  378
+              batch: 63
+  vm stats threshold: 48
+  node_unreclaimable:  0
+  start_pfn:           4096
+Node 0, zone   Normal
+  pages free     18553
+        min      11176
+        low      13842
+        high     16508
+        spanned  1308160
+        present  1308160
+        managed  1268711
+        protection: (0, 0, 0, 0, 0)
+      nr_free_pages 18553
+      nr_zone_inactive_anon 112423
+      nr_zone_active_anon 440982
+      nr_zone_inactive_file 241429
+      nr_zone_active_file 275989
+      nr_zone_unevictable 49272
+      nr_zone_write_pending 844
+      nr_mlock     154
+      nr_page_table_pages 9750
+      nr_kernel_stack 15136
+      nr_bounce    0
+      nr_zspages   0
+      nr_free_cma  0
+      numa_hit     162718019
+      numa_miss    0
+      numa_foreign 0
+      numa_interleave 26812
+      numa_local   162718019
+      numa_other   0
+  pagesets
+    cpu: 0
+              count: 316
+              high:  378
+              batch: 63
+  vm stats threshold: 56
+    cpu: 1
+              count: 366
+              high:  378
+              batch: 63
+  vm stats threshold: 56
+    cpu: 2
+              count: 60
+              high:  378
+              batch: 63
+  vm stats threshold: 56
+    cpu: 3
+              count: 256
+              high:  378
+              batch: 63
+  vm stats threshold: 56
+    cpu: 4
+              count: 253
+              high:  378
+              batch: 63
+  vm stats threshold: 56
+    cpu: 5
+              count: 159
+              high:  378
+              batch: 63
+  vm stats threshold: 56
+    cpu: 6
+              count: 311
+              high:  378
+              batch: 63
+  vm stats threshold: 56
+    cpu: 7
+              count: 264
+              high:  378
+              batch: 63
+  vm stats threshold: 56
+  node_unreclaimable:  0
+  start_pfn:           1048576
+Node 0, zone  Movable
+  pages free     0
+        min      0
+        low      0
+        high     0
+        spanned  0
+        present  0
+        managed  0
+        protection: (0, 0, 0, 0, 0)
+Node 0, zone   Device
+  pages free     0
+        min      0
+        low      0
+        high     0
+        spanned  0
+        present  0
+        managed  0
+        protection: (0, 0, 0, 0, 0)
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys
 Mode: 755
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/internal/util/parse.go b/internal/util/parse.go
index ca74889..755591d 100644
--- a/internal/util/parse.go
+++ b/internal/util/parse.go
@@ -49,6 +49,21 @@ func ParseUint64s(ss []string) ([]uint64, error) {
 	return us, nil
 }
 
+// ParsePInt64s parses a slice of strings into a slice of int64 pointers.
+func ParsePInt64s(ss []string) ([]*int64, error) {
+	us := make([]*int64, 0, len(ss))
+	for _, s := range ss {
+		u, err := strconv.ParseInt(s, 10, 64)
+		if err != nil {
+			return nil, err
+		}
+
+		us = append(us, &u)
+	}
+
+	return us, nil
+}
+
 // ReadUintFromFile reads a file and attempts to parse a uint64 from it.
 func ReadUintFromFile(path string) (uint64, error) {
 	data, err := ioutil.ReadFile(path)
diff --git a/schedstat.go b/schedstat.go
index 86396c0..98221e3 100644
--- a/schedstat.go
+++ b/schedstat.go
@@ -31,6 +31,12 @@ var (
 // See
 // https://www.kernel.org/doc/Documentation/scheduler/sched-stats.txt
 // for a detailed description of what these numbers mean.
+//
+// Note the current kernel documentation claims some of the time units are in
+// jiffies when they are actually in nanoseconds since 2.6.23 with the
+// introduction of CFS. A fix to the documentation is pending. See
+// https://lore.kernel.org/patchwork/project/lkml/list/?series=403473
+
 type Schedstat struct {
 	CPUs []*SchedstatCPU
 }
@@ -39,16 +45,16 @@ type Schedstat struct {
 type SchedstatCPU struct {
 	CPUNum string
 
-	RunningJiffies uint64
-	WaitingJiffies uint64
-	RunTimeslices  uint64
+	RunningNanoseconds uint64
+	WaitingNanoseconds uint64
+	RunTimeslices      uint64
 }
 
 // ProcSchedstat contains the values from /proc/<pid>/schedstat
 type ProcSchedstat struct {
-	RunningJiffies uint64
-	WaitingJiffies uint64
-	RunTimeslices  uint64
+	RunningNanoseconds uint64
+	WaitingNanoseconds uint64
+	RunTimeslices      uint64
 }
 
 func (fs FS) Schedstat() (*Schedstat, error) {
@@ -67,12 +73,12 @@ func (fs FS) Schedstat() (*Schedstat, error) {
 			cpu := &SchedstatCPU{}
 			cpu.CPUNum = match[1]
 
-			cpu.RunningJiffies, err = strconv.ParseUint(match[8], 10, 64)
+			cpu.RunningNanoseconds, err = strconv.ParseUint(match[8], 10, 64)
 			if err != nil {
 				continue
 			}
 
-			cpu.WaitingJiffies, err = strconv.ParseUint(match[9], 10, 64)
+			cpu.WaitingNanoseconds, err = strconv.ParseUint(match[9], 10, 64)
 			if err != nil {
 				continue
 			}
@@ -93,12 +99,12 @@ func parseProcSchedstat(contents string) (stats ProcSchedstat, err error) {
 	match := procLineRE.FindStringSubmatch(contents)
 
 	if match != nil {
-		stats.RunningJiffies, err = strconv.ParseUint(match[1], 10, 64)
+		stats.RunningNanoseconds, err = strconv.ParseUint(match[1], 10, 64)
 		if err != nil {
 			return
 		}
 
-		stats.WaitingJiffies, err = strconv.ParseUint(match[2], 10, 64)
+		stats.WaitingNanoseconds, err = strconv.ParseUint(match[2], 10, 64)
 		if err != nil {
 			return
 		}
@@ -110,19 +116,3 @@ func parseProcSchedstat(contents string) (stats ProcSchedstat, err error) {
 	err = errors.New("could not parse schedstat")
 	return
 }
-
-func (stat *SchedstatCPU) RunningSeconds() float64 {
-	return float64(stat.RunningJiffies) / userHZ
-}
-
-func (stat *SchedstatCPU) WaitingSeconds() float64 {
-	return float64(stat.WaitingJiffies) / userHZ
-}
-
-func (stat *ProcSchedstat) RunningSeconds() float64 {
-	return float64(stat.RunningJiffies) / userHZ
-}
-
-func (stat *ProcSchedstat) WaitingSeconds() float64 {
-	return float64(stat.WaitingJiffies) / userHZ
-}
diff --git a/zoneinfo.go b/zoneinfo.go
new file mode 100644
index 0000000..e941503
--- /dev/null
+++ b/zoneinfo.go
@@ -0,0 +1,196 @@
+// Copyright 2019 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.
+
+// +build !windows
+
+package procfs
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"regexp"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// Zoneinfo holds info parsed from /proc/zoneinfo.
+type Zoneinfo struct {
+	Node                       string
+	Zone                       string
+	NrFreePages                *int64
+	Min                        *int64
+	Low                        *int64
+	High                       *int64
+	Scanned                    *int64
+	Spanned                    *int64
+	Present                    *int64
+	Managed                    *int64
+	NrActiveAnon               *int64
+	NrInactiveAnon             *int64
+	NrIsolatedAnon             *int64
+	NrAnonPages                *int64
+	NrAnonTransparentHugepages *int64
+	NrActiveFile               *int64
+	NrInactiveFile             *int64
+	NrIsolatedFile             *int64
+	NrFilePages                *int64
+	NrSlabReclaimable          *int64
+	NrSlabUnreclaimable        *int64
+	NrMlockStack               *int64
+	NrKernelStack              *int64
+	NrMapped                   *int64
+	NrDirty                    *int64
+	NrWriteback                *int64
+	NrUnevictable              *int64
+	NrShmem                    *int64
+	NrDirtied                  *int64
+	NrWritten                  *int64
+	NumaHit                    *int64
+	NumaMiss                   *int64
+	NumaForeign                *int64
+	NumaInterleave             *int64
+	NumaLocal                  *int64
+	NumaOther                  *int64
+	Protection                 []*int64
+}
+
+var nodeZoneRE = regexp.MustCompile(`(\d+), zone\s+(\w+)`)
+
+// Zoneinfo parses an zoneinfo-file (/proc/zoneinfo) and returns a slice of
+// structs containing the relevant info.  More information available here:
+// https://www.kernel.org/doc/Documentation/sysctl/vm.txt
+func (fs FS) Zoneinfo() ([]Zoneinfo, error) {
+	data, err := ioutil.ReadFile(fs.proc.Path("zoneinfo"))
+	if err != nil {
+		return nil, fmt.Errorf("error reading zoneinfo %s: %s", fs.proc.Path("zoneinfo"), err)
+	}
+	zoneinfo, err := parseZoneinfo(data)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing zoneinfo %s: %s", fs.proc.Path("zoneinfo"), err)
+	}
+	return zoneinfo, nil
+}
+
+func parseZoneinfo(zoneinfoData []byte) ([]Zoneinfo, error) {
+
+	zoneinfo := []Zoneinfo{}
+
+	zoneinfoBlocks := bytes.Split(zoneinfoData, []byte("\nNode"))
+	for _, block := range zoneinfoBlocks {
+		var zoneinfoElement Zoneinfo
+		lines := strings.Split(string(block), "\n")
+		for _, line := range lines {
+
+			if nodeZone := nodeZoneRE.FindStringSubmatch(line); nodeZone != nil {
+				zoneinfoElement.Node = nodeZone[1]
+				zoneinfoElement.Zone = nodeZone[2]
+				continue
+			}
+			if strings.HasPrefix(strings.TrimSpace(line), "per-node stats") {
+				zoneinfoElement.Zone = ""
+				continue
+			}
+			parts := strings.Fields(strings.TrimSpace(line))
+			if len(parts) < 2 {
+				continue
+			}
+			vp := util.NewValueParser(parts[1])
+			switch parts[0] {
+			case "nr_free_pages":
+				zoneinfoElement.NrFreePages = vp.PInt64()
+			case "min":
+				zoneinfoElement.Min = vp.PInt64()
+			case "low":
+				zoneinfoElement.Low = vp.PInt64()
+			case "high":
+				zoneinfoElement.High = vp.PInt64()
+			case "scanned":
+				zoneinfoElement.Scanned = vp.PInt64()
+			case "spanned":
+				zoneinfoElement.Spanned = vp.PInt64()
+			case "present":
+				zoneinfoElement.Present = vp.PInt64()
+			case "managed":
+				zoneinfoElement.Managed = vp.PInt64()
+			case "nr_active_anon":
+				zoneinfoElement.NrActiveAnon = vp.PInt64()
+			case "nr_inactive_anon":
+				zoneinfoElement.NrInactiveAnon = vp.PInt64()
+			case "nr_isolated_anon":
+				zoneinfoElement.NrIsolatedAnon = vp.PInt64()
+			case "nr_anon_pages":
+				zoneinfoElement.NrAnonPages = vp.PInt64()
+			case "nr_anon_transparent_hugepages":
+				zoneinfoElement.NrAnonTransparentHugepages = vp.PInt64()
+			case "nr_active_file":
+				zoneinfoElement.NrActiveFile = vp.PInt64()
+			case "nr_inactive_file":
+				zoneinfoElement.NrInactiveFile = vp.PInt64()
+			case "nr_isolated_file":
+				zoneinfoElement.NrIsolatedFile = vp.PInt64()
+			case "nr_file_pages":
+				zoneinfoElement.NrFilePages = vp.PInt64()
+			case "nr_slab_reclaimable":
+				zoneinfoElement.NrSlabReclaimable = vp.PInt64()
+			case "nr_slab_unreclaimable":
+				zoneinfoElement.NrSlabUnreclaimable = vp.PInt64()
+			case "nr_mlock_stack":
+				zoneinfoElement.NrMlockStack = vp.PInt64()
+			case "nr_kernel_stack":
+				zoneinfoElement.NrKernelStack = vp.PInt64()
+			case "nr_mapped":
+				zoneinfoElement.NrMapped = vp.PInt64()
+			case "nr_dirty":
+				zoneinfoElement.NrDirty = vp.PInt64()
+			case "nr_writeback":
+				zoneinfoElement.NrWriteback = vp.PInt64()
+			case "nr_unevictable":
+				zoneinfoElement.NrUnevictable = vp.PInt64()
+			case "nr_shmem":
+				zoneinfoElement.NrShmem = vp.PInt64()
+			case "nr_dirtied":
+				zoneinfoElement.NrDirtied = vp.PInt64()
+			case "nr_written":
+				zoneinfoElement.NrWritten = vp.PInt64()
+			case "numa_hit":
+				zoneinfoElement.NumaHit = vp.PInt64()
+			case "numa_miss":
+				zoneinfoElement.NumaMiss = vp.PInt64()
+			case "numa_foreign":
+				zoneinfoElement.NumaForeign = vp.PInt64()
+			case "numa_interleave":
+				zoneinfoElement.NumaInterleave = vp.PInt64()
+			case "numa_local":
+				zoneinfoElement.NumaLocal = vp.PInt64()
+			case "numa_other":
+				zoneinfoElement.NumaOther = vp.PInt64()
+			case "protection:":
+				protectionParts := strings.Split(line, ":")
+				protectionValues := strings.Replace(protectionParts[1], "(", "", 1)
+				protectionValues = strings.Replace(protectionValues, ")", "", 1)
+				protectionValues = strings.TrimSpace(protectionValues)
+				protectionStringMap := strings.Split(protectionValues, ", ")
+				val, err := util.ParsePInt64s(protectionStringMap)
+				if err == nil {
+					zoneinfoElement.Protection = val
+				}
+			}
+
+		}
+
+		zoneinfo = append(zoneinfo, zoneinfoElement)
+	}
+	return zoneinfo, nil
+}


Test patch:
diff --git a/schedstat_test.go b/schedstat_test.go
index 42e43f1..c6bee61 100644
--- a/schedstat_test.go
+++ b/schedstat_test.go
@@ -13,7 +13,9 @@
 
 package procfs
 
-import "testing"
+import (
+	"testing"
+)
 
 func TestSchedstat(t *testing.T) {
 	stats, err := getProcFixtures(t).Schedstat()
@@ -36,18 +38,12 @@ func TestSchedstat(t *testing.T) {
 		t.Error("could not find cpu0")
 	}
 
-	if want, have := uint64(2045936778163039), cpu.RunningJiffies; want != have {
-		t.Errorf("want RunningJiffies %v, have %v", want, have)
-	}
-	if want, have := float64(2045936778163039)/userHZ, cpu.RunningSeconds(); want != have {
-		t.Errorf("want RunningSeconds() %v, have %v", want, have)
+	if want, have := uint64(2045936778163039), cpu.RunningNanoseconds; want != have {
+		t.Errorf("want RunningNanoseconds %v, have %v", want, have)
 	}
 
-	if want, have := uint64(343796328169361), cpu.WaitingJiffies; want != have {
-		t.Errorf("want WaitingJiffies %v, have %v", want, have)
-	}
-	if want, have := float64(343796328169361)/userHZ, cpu.WaitingSeconds(); want != have {
-		t.Errorf("want WaitingSeconds() %v, have %v", want, have)
+	if want, have := uint64(343796328169361), cpu.WaitingNanoseconds; want != have {
+		t.Errorf("want WaitingNanoseconds %v, have %v", want, have)
 	}
 
 	if want, have := uint64(4767485306), cpu.RunTimeslices; want != have {
@@ -66,18 +62,12 @@ func TestProcSchedstat(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	if want, have := uint64(411605849), schedstat.RunningJiffies; want != have {
-		t.Errorf("want RunningJiffies %v, have %v", want, have)
-	}
-	if want, have := float64(411605849)/userHZ, schedstat.RunningSeconds(); want != have {
-		t.Errorf("want RunningSeconds() %v, have %v", want, have)
+	if want, have := uint64(411605849), schedstat.RunningNanoseconds; want != have {
+		t.Errorf("want RunningNanoseconds %v, have %v", want, have)
 	}
 
-	if want, have := uint64(93680043), schedstat.WaitingJiffies; want != have {
-		t.Errorf("want WaitingJiffies %v, have %v", want, have)
-	}
-	if want, have := float64(93680043)/userHZ, schedstat.WaitingSeconds(); want != have {
-		t.Errorf("want WaitingSeconds() %v, have %v", want, have)
+	if want, have := uint64(93680043), schedstat.WaitingNanoseconds; want != have {
+		t.Errorf("want WaitingNanoseconds %v, have %v", want, have)
 	}
 
 	if want, have := uint64(79), schedstat.RunTimeslices; want != have {
@@ -113,11 +103,11 @@ func TestProcSchedstatMultipleLines(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if want, have := uint64(123), schedstat.RunningJiffies; want != have {
-		t.Errorf("want RunningJiffies %v, have %v", want, have)
+	if want, have := uint64(123), schedstat.RunningNanoseconds; want != have {
+		t.Errorf("want RunningNanoseconds %v, have %v", want, have)
 	}
-	if want, have := uint64(456), schedstat.WaitingJiffies; want != have {
-		t.Errorf("want WaitingJiffies %v, have %v", want, have)
+	if want, have := uint64(456), schedstat.WaitingNanoseconds; want != have {
+		t.Errorf("want WaitingNanoseconds %v, have %v", want, have)
 	}
 	if want, have := uint64(789), schedstat.RunTimeslices; want != have {
 		t.Errorf("want RunTimeslices %v, have %v", want, have)
diff --git a/zoneinfo_test.go b/zoneinfo_test.go
new file mode 100644
index 0000000..618b9df
--- /dev/null
+++ b/zoneinfo_test.go
@@ -0,0 +1,47 @@
+// Copyright 2019 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.
+
+// +build !windows
+
+package procfs
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func newPInt64(i int64) *int64 {
+	return &i
+}
+func TestZoneinfo(t *testing.T) {
+	fs := getProcFixtures(t)
+	protection1 := []*int64{newPInt64(0), newPInt64(2877), newPInt64(7826), newPInt64(7826), newPInt64(7826)}
+	protection2 := []*int64{newPInt64(0), newPInt64(0), newPInt64(4949), newPInt64(4949), newPInt64(4949)}
+	refs := []Zoneinfo{
+		{Node: "0", Zone: "", NrFreePages: newPInt64(3952), Min: newPInt64(33), Low: newPInt64(41), High: newPInt64(49), Spanned: newPInt64(4095), Present: newPInt64(3975), Managed: newPInt64(3956), NrActiveAnon: newPInt64(547580), NrInactiveAnon: newPInt64(230981), NrIsolatedAnon: newPInt64(0), NrAnonPages: newPInt64(795576), NrAnonTransparentHugepages: newPInt64(0), NrActiveFile: newPInt64(346282), NrInactiveFile: newPInt64(316904), NrIsolatedFile: newPInt64(0), NrFilePages: newPInt64(761874), NrSlabReclaimable: newPInt64(131220), NrSlabUnreclaimable: newPInt64(47320), NrKernelStack: newPInt64(0), NrMapped: newPInt64(215483), NrDirty: newPInt64(908), NrWriteback: newPInt64(0), NrUnevictable: newPInt64(115467), NrShmem: newPInt64(224925), NrDirtied: newPInt64(8007423), NrWritten: newPInt64(7752121), NumaHit: newPInt64(1), NumaMiss: newPInt64(0), NumaForeign: newPInt64(0), NumaInterleave: newPInt64(0), NumaLocal: newPInt64(1), NumaOther: newPInt64(0), Protection: protection1},
+		{Node: "0", Zone: "DMA32", NrFreePages: newPInt64(204252), Min: newPInt64(19510), Low: newPInt64(21059), High: newPInt64(22608), Spanned: newPInt64(1044480), Present: newPInt64(759231), Managed: newPInt64(742806), NrKernelStack: newPInt64(2208), NumaHit: newPInt64(113952967), NumaMiss: newPInt64(0), NumaForeign: newPInt64(0), NumaInterleave: newPInt64(0), NumaLocal: newPInt64(113952967), NumaOther: newPInt64(0), Protection: protection2},
+	}
+	data, err := fs.Zoneinfo()
+	if err != nil {
+		t.Fatalf("failed to parse zoneinfo: %v", err)
+	}
+
+	for index, ref := range refs {
+		want, got := ref, data[index]
+		if diff := cmp.Diff(want, got); diff != "" {
+			t.Fatalf("unexpected crypto entry (-want +got):\n%s", diff)
+		}
+
+	}
+}