Repository: prometheus/procfs
Pull request #579 (2023-10-10 11:38:35)

Add support for for /proc/net/tls_data kTLS stats
Since Linux kernel 4.17 (I believe) there is support to move some TLS operations to kernel space, enabling things like TLS `sendfile` to improve performance. Statistics for this feature is exposed under `/proc/net/tls_stats`. This PR adds support to read these statistics. It is very much based on the support for `/proc/net/xfrm_stats` in `net_xfrm.go` as they both share the same format.

For more info on Linux kTLS see https://docs.kernel.org/networking/tls.html.

discordianfish: Would be nice if we could make the parsing for /proc/net/xfrm_stats more generic and re-use that here, but I think it's fine either way
defect: Yeah, i was thinking about refactoring that out but didn't really find a clean way to do it. Maybe using annotated structs a la `json` and `yaml` and friends? But that feels a bit overkill, at least for now.
discordianfish: @defect Fair enough, that might be better be part of a bigger refactoring

Patch:
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 15cf547..8f25279 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -12,21 +12,27 @@ on:
       - ".golangci.yml"
   pull_request:
 
+permissions:  # added using https://github.com/step-security/secure-repo
+  contents: read
+
 jobs:
   golangci:
+    permissions:
+      contents: read  # for actions/checkout to fetch code
+      pull-requests: read  # for golangci/golangci-lint-action to fetch pull requests
     name: lint
     runs-on: ubuntu-latest
     steps:
       - name: Checkout repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       - name: install Go
-        uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
+        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
         with:
-          go-version: 1.20.x
+          go-version: 1.21.x
       - name: Install snmp_exporter/generator dependencies
         run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
         if: github.repository == 'prometheus/snmp_exporter'
       - name: Lint
         uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0
         with:
-          version: v1.54.2
+          version: v1.55.2
diff --git a/Makefile.common b/Makefile.common
index 062a281..5fd1782 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -61,11 +61,11 @@ PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_
 SKIP_GOLANGCI_LINT :=
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v1.54.2
-# golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
+GOLANGCI_LINT_VERSION ?= v1.55.2
+# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
-	ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386))
+	ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))
 		# If we're in CI and there is an Actions file, that means the linter
 		# is being run in Actions, so we don't need to run it here.
 		ifneq (,$(SKIP_GOLANGCI_LINT))
diff --git a/btrfs/btrfs.go b/btrfs/btrfs.go
index 094c898..5b4ff35 100644
--- a/btrfs/btrfs.go
+++ b/btrfs/btrfs.go
@@ -25,6 +25,7 @@ type Stats struct {
 	NodeSize       uint64
 	QuotaOverride  uint64
 	SectorSize     uint64
+	CommitStats    CommitStats
 }
 
 // Allocation contains allocation statistics for data, metadata and system data.
@@ -65,3 +66,12 @@ type LayoutUsage struct {
 type Device struct {
 	Size uint64
 }
+
+// Number of commits and various time related statistics.
+// See Linux fs/btrfs/sysfs.c with 6.x version.
+type CommitStats struct {
+	Commits       uint64
+	LastCommitMs  uint64
+	MaxCommitMs   uint64
+	TotalCommitMs uint64
+}
diff --git a/btrfs/get.go b/btrfs/get.go
index 77c72e1..db0046b 100644
--- a/btrfs/get.go
+++ b/btrfs/get.go
@@ -14,6 +14,8 @@
 package btrfs
 
 import (
+	"bufio"
+	"fmt"
 	"os"
 	"path"
 	"path/filepath"
@@ -245,6 +247,62 @@ func (r *reader) readFilesystemStats() (s *Stats) {
 			Metadata:          r.readAllocationStats("allocation/metadata"),
 			System:            r.readAllocationStats("allocation/system"),
 		},
+
+		// Read commit stats data
+		CommitStats: r.readCommitStats("commit_stats"),
 	}
 	return
 }
+
+// readCommitStats returns the commit_stats information for commit stats metrics.
+func (r *reader) readCommitStats(p string) CommitStats {
+	stats := CommitStats{}
+
+	f, err := os.Open(path.Join(r.path, p))
+	if err != nil {
+		// if commit_stats not found. maybe btrfs version < 6.0
+		if !os.IsNotExist(err) {
+			r.err = err
+		}
+		return stats
+	}
+	defer f.Close()
+
+	scanner := bufio.NewScanner(f)
+
+	for scanner.Scan() {
+		line := scanner.Text()
+		parts := strings.Fields(scanner.Text())
+		// require  <key> <value>
+		if len(parts) != 2 {
+			r.err = fmt.Errorf("invalid commit_stats line %q", line)
+			return stats
+		}
+
+		value, err := strconv.ParseUint(parts[1], 10, 64)
+		if err != nil {
+			r.err = fmt.Errorf("error parsing commit_stats line: %w", err)
+			return stats
+		}
+
+		switch metricName := parts[0]; metricName {
+		case "commits":
+			stats.Commits = value
+		case "last_commit_ms":
+			stats.LastCommitMs = value
+		case "max_commit_ms":
+			stats.MaxCommitMs = value
+		case "total_commit_ms":
+			stats.TotalCommitMs = value
+		default:
+			continue
+		}
+	}
+
+	if err := scanner.Err(); err != nil {
+		r.err = fmt.Errorf("error scanning commit_stats file: %w", err)
+		return stats
+	}
+
+	return stats
+}
diff --git a/crypto.go b/crypto.go
index 9a73e26..13ee66f 100644
--- a/crypto.go
+++ b/crypto.go
@@ -84,7 +84,7 @@ func parseCrypto(r io.Reader) ([]Crypto, error) {
 
 		kv := strings.Split(text, ":")
 		if len(kv) != 2 {
-			return nil, fmt.Errorf("%w: Cannot parae line: %q", ErrFileParse, text)
+			return nil, fmt.Errorf("%w: Cannot parse line: %q", ErrFileParse, text)
 		}
 
 		k := strings.TrimSpace(kv[0])
diff --git a/go.mod b/go.mod
index 18971c8..1e58b3a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/prometheus/procfs
 go 1.19
 
 require (
-	github.com/google/go-cmp v0.5.9
-	golang.org/x/sync v0.3.0
-	golang.org/x/sys v0.12.0
+	github.com/google/go-cmp v0.6.0
+	golang.org/x/sync v0.6.0
+	golang.org/x/sys v0.16.0
 )
diff --git a/go.sum b/go.sum
index 1b5b75a..caa1dc9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/meminfo.go b/meminfo.go
index db1daea..73a03e8 100644
--- a/meminfo.go
+++ b/meminfo.go
@@ -126,6 +126,7 @@ type Meminfo struct {
 	VmallocUsed *uint64
 	// largest contiguous block of vmalloc area which is free
 	VmallocChunk      *uint64
+	Percpu            *uint64
 	HardwareCorrupted *uint64
 	AnonHugePages     *uint64
 	ShmemHugePages    *uint64
@@ -178,6 +179,7 @@ type Meminfo struct {
 	VmallocTotalBytes      *uint64
 	VmallocUsedBytes       *uint64
 	VmallocChunkBytes      *uint64
+	PercpuBytes            *uint64
 	HardwareCorruptedBytes *uint64
 	AnonHugePagesBytes     *uint64
 	ShmemHugePagesBytes    *uint64
@@ -339,6 +341,9 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
 		case "VmallocChunk:":
 			m.VmallocChunk = &val
 			m.VmallocChunkBytes = &valBytes
+		case "Percpu:":
+			m.Percpu = &val
+			m.PercpuBytes = &valBytes
 		case "HardwareCorrupted:":
 			m.HardwareCorrupted = &val
 			m.HardwareCorruptedBytes = &valBytes
diff --git a/net_ip_socket.go b/net_ip_socket.go
index 4da81ea..ba7d9ca 100644
--- a/net_ip_socket.go
+++ b/net_ip_socket.go
@@ -50,10 +50,13 @@ type (
 		// UsedSockets shows the total number of parsed lines representing the
 		// number of used sockets.
 		UsedSockets uint64
+		// Drops shows the total number of dropped packets of all UPD sockets.
+		Drops *uint64
 	}
 
 	// netIPSocketLine represents the fields parsed from a single line
 	// in /proc/net/{t,u}dp{,6}. Fields which are not used by IPSocket are skipped.
+	// Drops is non-nil for udp{,6}, but nil for tcp{,6}.
 	// For the proc file format details, see https://linux.die.net/man/5/proc.
 	netIPSocketLine struct {
 		Sl        uint64
@@ -66,6 +69,7 @@ type (
 		RxQueue   uint64
 		UID       uint64
 		Inode     uint64
+		Drops     *uint64
 	}
 )
 
@@ -77,13 +81,14 @@ func newNetIPSocket(file string) (NetIPSocket, error) {
 	defer f.Close()
 
 	var netIPSocket NetIPSocket
+	isUDP := strings.Contains(file, "udp")
 
 	lr := io.LimitReader(f, readLimit)
 	s := bufio.NewScanner(lr)
 	s.Scan() // skip first line with headers
 	for s.Scan() {
 		fields := strings.Fields(s.Text())
-		line, err := parseNetIPSocketLine(fields)
+		line, err := parseNetIPSocketLine(fields, isUDP)
 		if err != nil {
 			return nil, err
 		}
@@ -104,19 +109,25 @@ func newNetIPSocketSummary(file string) (*NetIPSocketSummary, error) {
 	defer f.Close()
 
 	var netIPSocketSummary NetIPSocketSummary
+	var udpPacketDrops uint64
+	isUDP := strings.Contains(file, "udp")
 
 	lr := io.LimitReader(f, readLimit)
 	s := bufio.NewScanner(lr)
 	s.Scan() // skip first line with headers
 	for s.Scan() {
 		fields := strings.Fields(s.Text())
-		line, err := parseNetIPSocketLine(fields)
+		line, err := parseNetIPSocketLine(fields, isUDP)
 		if err != nil {
 			return nil, err
 		}
 		netIPSocketSummary.TxQueueLength += line.TxQueue
 		netIPSocketSummary.RxQueueLength += line.RxQueue
 		netIPSocketSummary.UsedSockets++
+		if isUDP {
+			udpPacketDrops += *line.Drops
+			netIPSocketSummary.Drops = &udpPacketDrops
+		}
 	}
 	if err := s.Err(); err != nil {
 		return nil, err
@@ -149,7 +160,7 @@ func parseIP(hexIP string) (net.IP, error) {
 }
 
 // parseNetIPSocketLine parses a single line, represented by a list of fields.
-func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
+func parseNetIPSocketLine(fields []string, isUDP bool) (*netIPSocketLine, error) {
 	line := &netIPSocketLine{}
 	if len(fields) < 10 {
 		return nil, fmt.Errorf(
@@ -224,5 +235,14 @@ func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
 		return nil, fmt.Errorf("%s: Cannot parse inode value in %q: %w", ErrFileParse, line.Inode, err)
 	}
 
+	// drops
+	if isUDP {
+		drops, err := strconv.ParseUint(fields[12], 0, 64)
+		if err != nil {
+			return nil, fmt.Errorf("%s: Cannot parse drops value in %q: %w", ErrFileParse, drops, err)
+		}
+		line.Drops = &drops
+	}
+
 	return line, nil
 }
diff --git a/net_tls_stat.go b/net_tls_stat.go
new file mode 100644
index 0000000..13994c1
--- /dev/null
+++ b/net_tls_stat.go
@@ -0,0 +1,119 @@
+// Copyright 2023 Prometheus Team
+// 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"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+)
+
+// TLSStat struct represents data in /proc/net/tls_stat.
+// See https://docs.kernel.org/networking/tls.html#statistics
+type TLSStat struct {
+	// number of TX sessions currently installed where host handles cryptography
+	TLSCurrTxSw int
+	// number of RX sessions currently installed where host handles cryptography
+	TLSCurrRxSw int
+	// number of TX sessions currently installed where NIC handles cryptography
+	TLSCurrTxDevice int
+	// number of RX sessions currently installed where NIC handles cryptography
+	TLSCurrRxDevice int
+	//number of TX sessions opened with host cryptography
+	TLSTxSw int
+	//number of RX sessions opened with host cryptography
+	TLSRxSw int
+	// number of TX sessions opened with NIC cryptography
+	TLSTxDevice int
+	// number of RX sessions opened with NIC cryptography
+	TLSRxDevice int
+	// record decryption failed (e.g. due to incorrect authentication tag)
+	TLSDecryptError int
+	//  number of RX resyncs sent to NICs handling cryptography
+	TLSRxDeviceResync int
+	// number of RX records which had to be re-decrypted due to TLS_RX_EXPECT_NO_PAD mis-prediction. Note that this counter will also increment for non-data records.
+	TLSDecryptRetry int
+	// number of data RX records which had to be re-decrypted due to TLS_RX_EXPECT_NO_PAD mis-prediction.
+	TLSRxNoPadViolation int
+}
+
+// NewTLSStat reads the tls_stat statistics.
+func NewTLSStat() (TLSStat, error) {
+	fs, err := NewFS(DefaultMountPoint)
+	if err != nil {
+		return TLSStat{}, err
+	}
+
+	return fs.NewTLSStat()
+}
+
+// NewTLSStat reads the tls_stat statistics.
+func (fs FS) NewTLSStat() (TLSStat, error) {
+	file, err := os.Open(fs.proc.Path("net/tls_stat"))
+	if err != nil {
+		return TLSStat{}, err
+	}
+	defer file.Close()
+
+	var (
+		tlsstat = TLSStat{}
+		s       = bufio.NewScanner(file)
+	)
+
+	for s.Scan() {
+		fields := strings.Fields(s.Text())
+
+		if len(fields) != 2 {
+			return TLSStat{}, fmt.Errorf("%w: %q line %q", ErrFileParse, file.Name(), s.Text())
+		}
+
+		name := fields[0]
+		value, err := strconv.Atoi(fields[1])
+		if err != nil {
+			return TLSStat{}, err
+		}
+
+		switch name {
+		case "TlsCurrTxSw":
+			tlsstat.TLSCurrTxSw = value
+		case "TlsCurrRxSw":
+			tlsstat.TLSCurrRxSw = value
+		case "TlsCurrTxDevice":
+			tlsstat.TLSCurrTxDevice = value
+		case "TlsCurrRxDevice":
+			tlsstat.TLSCurrRxDevice = value
+		case "TlsTxSw":
+			tlsstat.TLSTxSw = value
+		case "TlsRxSw":
+			tlsstat.TLSRxSw = value
+		case "TlsTxDevice":
+			tlsstat.TLSTxDevice = value
+		case "TlsRxDevice":
+			tlsstat.TLSRxDevice = value
+		case "TlsDecryptError":
+			tlsstat.TLSDecryptError = value
+		case "TlsRxDeviceResync":
+			tlsstat.TLSRxDeviceResync = value
+		case "TlsDecryptRetry":
+			tlsstat.TLSDecryptRetry = value
+		case "TlsRxNoPadViolation":
+			tlsstat.TLSRxNoPadViolation = value
+		}
+
+	}
+
+	return tlsstat, s.Err()
+}
diff --git a/proc_stat.go b/proc_stat.go
index 923e550..06a8d93 100644
--- a/proc_stat.go
+++ b/proc_stat.go
@@ -110,6 +110,11 @@ type ProcStat struct {
 	Policy uint
 	// Aggregated block I/O delays, measured in clock ticks (centiseconds).
 	DelayAcctBlkIOTicks uint64
+	// Guest time of the process (time spent running a virtual CPU for a guest
+	// operating system), measured in clock ticks.
+	GuestTime int
+	// Guest time of the process's children, measured in clock ticks.
+	CGuestTime int
 
 	proc FS
 }
@@ -189,6 +194,8 @@ func (p Proc) Stat() (ProcStat, error) {
 		&s.RTPriority,
 		&s.Policy,
 		&s.DelayAcctBlkIOTicks,
+		&s.GuestTime,
+		&s.CGuestTime,
 	)
 	if err != nil {
 		return ProcStat{}, err
diff --git a/sysfs/class_infiniband.go b/sysfs/class_infiniband.go
index 90dc0d0..7892c69 100644
--- a/sysfs/class_infiniband.go
+++ b/sysfs/class_infiniband.go
@@ -279,13 +279,16 @@ func (fs FS) parseInfiniBandPort(name string, port string) (*InfiniBandPort, err
 		return nil, fmt.Errorf("could not parse rate file in %q: %w", portPath, err)
 	}
 
-	counters, err := parseInfiniBandCounters(portPath)
-	if err != nil {
-		return nil, err
+	// Intel irdma module does not expose /sys/class/infiniband/<device>/ports/<port-num>/counters
+	if !strings.HasPrefix(ibp.Name, "irdma") {
+		counters, err := parseInfiniBandCounters(portPath)
+		if err != nil {
+			return nil, err
+		}
+		ibp.Counters = *counters
 	}
-	ibp.Counters = *counters
 
-	if strings.Contains(ibp.Name, "mlx5") {
+	if strings.HasPrefix(ibp.Name, "irdma") || strings.HasPrefix(ibp.Name, "mlx5_") {
 		hwCounters, err := parseInfiniBandHwCounters(portPath)
 		if err != nil {
 			return nil, err
@@ -296,6 +299,9 @@ func (fs FS) parseInfiniBandPort(name string, port string) (*InfiniBandPort, err
 	return &ibp, nil
 }
 
+// parseInfiniBandCounters parses the counters exposed under
+// /sys/class/infiniband/<device>/ports/<port-num>/counters, which first appeared in kernel v2.6.12.
+// Prior to kernel v4.5, 64-bit counters were exposed separately under the "counters_ext" directory.
 func parseInfiniBandCounters(portPath string) (*InfiniBandCounters, error) {
 	var counters InfiniBandCounters
 
@@ -393,7 +399,7 @@ func parseInfiniBandCounters(portPath string) (*InfiniBandCounters, error) {
 		}
 	}
 
-	// Parse legacy counters
+	// Parse pre-kernel-v4.5 64-bit counters.
 	path = filepath.Join(portPath, "counters_ext")
 	files, err = os.ReadDir(path)
 	if err != nil && !os.IsNotExist(err) {
@@ -457,6 +463,8 @@ func parseInfiniBandCounters(portPath string) (*InfiniBandCounters, error) {
 	return &counters, nil
 }
 
+// parseInfiniBandHwCounters parses the optional counters exposed under
+// /sys/class/infiniband/<device>/ports/<port-num>/hw_counters, which first appeared in kernel v4.6.
 func parseInfiniBandHwCounters(portPath string) (*InfiniBandHwCounters, error) {
 	var hwCounters InfiniBandHwCounters
 
diff --git a/sysfs/class_watchdog.go b/sysfs/class_watchdog.go
new file mode 100644
index 0000000..bcc2a67
--- /dev/null
+++ b/sysfs/class_watchdog.go
@@ -0,0 +1,123 @@
+// Copyright 2023 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.
+
+//go:build linux
+// +build linux
+
+package sysfs
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+const watchdogClassPath = "class/watchdog"
+
+// WatchdogStats contains info from files in /sys/class/watchdog for a single watchdog device.
+// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-watchdog
+type WatchdogStats struct {
+	Name               string
+	Bootstatus         *int64  // /sys/class/watchdog/<Name>/bootstatus
+	Options            *string // /sys/class/watchdog/<Name>/options
+	FwVersion          *int64  // /sys/class/watchdog/<Name>/fw_version
+	Identity           *string // /sys/class/watchdog/<Name>/identity
+	Nowayout           *int64  // /sys/class/watchdog/<Name>/nowayout
+	State              *string // /sys/class/watchdog/<Name>/state
+	Status             *string // /sys/class/watchdog/<Name>/status
+	Timeleft           *int64  // /sys/class/watchdog/<Name>/timeleft
+	Timeout            *int64  // /sys/class/watchdog/<Name>/timeout
+	Pretimeout         *int64  // /sys/class/watchdog/<Name>/pretimeout
+	PretimeoutGovernor *string // /sys/class/watchdog/<Name>/pretimeout_governor
+	AccessCs0          *int64  // /sys/class/watchdog/<Name>/access_cs0
+}
+
+// WatchdogClass is a collection of statistics for every watchdog device in /sys/class/watchdog.
+//
+// The map keys are the names of the watchdog devices.
+type WatchdogClass map[string]WatchdogStats
+
+// WatchdogClass returns info for all watchdog devices read from /sys/class/watchdog.
+func (fs FS) WatchdogClass() (WatchdogClass, error) {
+	path := fs.sys.Path(watchdogClassPath)
+
+	dirs, err := os.ReadDir(path)
+	if err != nil {
+		return nil, fmt.Errorf("failed to list watchdog devices at %q: %w", path, err)
+	}
+
+	wds := make(WatchdogClass, len(dirs))
+	for _, d := range dirs {
+		stats, err := fs.parseWatchdog(d.Name())
+		if err != nil {
+			return nil, err
+		}
+
+		wds[stats.Name] = *stats
+	}
+
+	return wds, nil
+}
+
+func (fs FS) parseWatchdog(wdName string) (*WatchdogStats, error) {
+	path := fs.sys.Path(watchdogClassPath, wdName)
+	wd := WatchdogStats{Name: wdName}
+
+	for _, f := range [...]string{"bootstatus", "options", "fw_version", "identity", "nowayout", "state", "status", "timeleft", "timeout", "pretimeout", "pretimeout_governor", "access_cs0"} {
+		name := filepath.Join(path, f)
+		value, err := util.SysReadFile(name)
+		if err != nil {
+			if os.IsNotExist(err) {
+				continue
+			}
+			return nil, fmt.Errorf("failed to read file %q: %w", name, err)
+		}
+
+		vp := util.NewValueParser(value)
+
+		switch f {
+		case "bootstatus":
+			wd.Bootstatus = vp.PInt64()
+		case "options":
+			wd.Options = &value
+		case "fw_version":
+			wd.FwVersion = vp.PInt64()
+		case "identity":
+			wd.Identity = &value
+		case "nowayout":
+			wd.Nowayout = vp.PInt64()
+		case "state":
+			wd.State = &value
+		case "status":
+			wd.Status = &value
+		case "timeleft":
+			wd.Timeleft = vp.PInt64()
+		case "timeout":
+			wd.Timeout = vp.PInt64()
+		case "pretimeout":
+			wd.Pretimeout = vp.PInt64()
+		case "pretimeout_governor":
+			wd.PretimeoutGovernor = &value
+		case "access_cs0":
+			wd.AccessCs0 = vp.PInt64()
+		}
+
+		if err := vp.Err(); err != nil {
+			return nil, err
+		}
+	}
+
+	return &wd, nil
+}


Test patch:
diff --git a/btrfs/get_test.go b/btrfs/get_test.go
index 23059c6..e2d09e1 100644
--- a/btrfs/get_test.go
+++ b/btrfs/get_test.go
@@ -19,6 +19,7 @@ type testVector struct {
 	uuid, label        string
 	devices, features  int
 	data, meta, system alloc
+	commitstats        commit
 }
 
 type alloc struct {
@@ -27,6 +28,13 @@ type alloc struct {
 	ratio  float64
 }
 
+type commit struct {
+	commits       uint64
+	lastCommitMs  uint64
+	maxCommitMs   uint64
+	totalCommitMs uint64
+}
+
 func TestFSBtrfsStats(t *testing.T) {
 	btrfs, err := NewFS("testdata/fixtures/sys")
 	if err != nil {
@@ -39,22 +47,24 @@ func TestFSBtrfsStats(t *testing.T) {
 
 	tests := []testVector{
 		{
-			uuid:     "0abb23a9-579b-43e6-ad30-227ef47fcb9d",
-			label:    "fixture",
-			devices:  2,
-			features: 4,
-			data:     alloc{"raid0", 2147483648, 1},
-			meta:     alloc{"raid1", 1073741824, 2},
-			system:   alloc{"raid1", 8388608, 2},
+			uuid:        "0abb23a9-579b-43e6-ad30-227ef47fcb9d",
+			label:       "fixture",
+			devices:     2,
+			features:    4,
+			data:        alloc{"raid0", 2147483648, 1},
+			meta:        alloc{"raid1", 1073741824, 2},
+			system:      alloc{"raid1", 8388608, 2},
+			commitstats: commit{258051, 1000, 51462, 47836090},
 		},
 		{
-			uuid:     "7f07c59f-6136-449c-ab87-e1cf2328731b",
-			label:    "",
-			devices:  4,
-			features: 5,
-			data:     alloc{"raid5", 644087808, 4. / 3.},
-			meta:     alloc{"raid6", 429391872, 4. / 2.},
-			system:   alloc{"raid6", 16777216, 4. / 2.},
+			uuid:        "7f07c59f-6136-449c-ab87-e1cf2328731b",
+			label:       "",
+			devices:     4,
+			features:    5,
+			data:        alloc{"raid5", 644087808, 4. / 3.},
+			meta:        alloc{"raid6", 429391872, 4. / 2.},
+			system:      alloc{"raid6", 16777216, 4. / 2.},
+			commitstats: commit{0, 0, 0, 0},
 		},
 	}
 
@@ -98,5 +108,21 @@ func TestFSBtrfsStats(t *testing.T) {
 		if want, got := tt.system.ratio, stats[i].Allocation.System.Layouts[tt.system.layout].Ratio; want != got {
 			t.Errorf("fs %q unexpected system ratio:\nwant: %f\nhave: %f", tt.uuid, want, got)
 		}
+
+		if want, got := tt.commitstats.commits, stats[i].CommitStats.Commits; want != got {
+			t.Errorf("fs %q unexpected commit stats commits:\nwant: %d\nhave: %d", tt.uuid, want, got)
+		}
+
+		if want, got := tt.commitstats.lastCommitMs, stats[i].CommitStats.LastCommitMs; want != got {
+			t.Errorf("fs %q unexpected commit stats last_commit_ms:\nwant: %d\nhave: %d", tt.uuid, want, got)
+		}
+
+		if want, got := tt.commitstats.maxCommitMs, stats[i].CommitStats.MaxCommitMs; want != got {
+			t.Errorf("fs %q unexpected commit stats max_commit_ms:\nwant: %d\nhave: %d", tt.uuid, want, got)
+		}
+
+		if want, got := tt.commitstats.totalCommitMs, stats[i].CommitStats.TotalCommitMs; want != got {
+			t.Errorf("fs %q unexpected commit stats total_commit_ms:\nwant: %d\nhave: %d", tt.uuid, want, got)
+		}
 	}
 }
diff --git a/meminfo_test.go b/meminfo_test.go
index 2d2b238..3d3b409 100644
--- a/meminfo_test.go
+++ b/meminfo_test.go
@@ -53,6 +53,7 @@ func TestMeminfo(t *testing.T) {
 		VmallocTotal:      newuint64(34359738367),
 		VmallocUsed:       newuint64(36596),
 		VmallocChunk:      newuint64(34359637840),
+		Percpu:            newuint64(26176),
 		HardwareCorrupted: newuint64(0),
 		AnonHugePages:     newuint64(12288),
 		HugePagesTotal:    newuint64(0),
@@ -96,6 +97,7 @@ func TestMeminfo(t *testing.T) {
 		VmallocTotalBytes:      newuint64(35184372087808),
 		VmallocUsedBytes:       newuint64(37474304),
 		VmallocChunkBytes:      newuint64(35184269148160),
+		PercpuBytes:            newuint64(26804224),
 		HardwareCorruptedBytes: newuint64(0),
 		AnonHugePagesBytes:     newuint64(12582912),
 		HugepagesizeBytes:      newuint64(2097152),
diff --git a/net_ip_socket_test.go b/net_ip_socket_test.go
index 9bbad87..3c057fb 100644
--- a/net_ip_socket_test.go
+++ b/net_ip_socket_test.go
@@ -25,6 +25,7 @@ func Test_parseNetIPSocketLine(t *testing.T) {
 		name    string
 		want    *netIPSocketLine
 		wantErr bool
+		isUDP   bool
 	}{
 		{
 			name:   "reading valid lines, no issue should happened",
@@ -96,10 +97,17 @@ func Test_parseNetIPSocketLine(t *testing.T) {
 			want:    nil,
 			wantErr: true,
 		},
+		{
+			name:    "error case - parse Drops - not a valid uint",
+			fields:  []string{"1:", "00000000:0000", "00000000:0000", "07", "00000000:00000001", "0:0", "0", "10", "0", "39309", "2", "000000009bd60d72", "-5"},
+			want:    nil,
+			wantErr: true,
+			isUDP:   true,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := parseNetIPSocketLine(tt.fields)
+			got, err := parseNetIPSocketLine(tt.fields, tt.isUDP)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("parseNetIPSocketLine() error = %v, wantErr %v", err, tt.wantErr)
 				return
diff --git a/net_tcp_test.go b/net_tcp_test.go
index a32e2fb..5ff33ef 100644
--- a/net_tcp_test.go
+++ b/net_tcp_test.go
@@ -30,7 +30,7 @@ func Test_newNetTCP(t *testing.T) {
 			name: "tcp file found, no error should come up",
 			file: "testdata/fixtures/proc/net/tcp",
 			want: []*netIPSocketLine{
-				&netIPSocketLine{
+				{
 					Sl:        0,
 					LocalAddr: net.IP{10, 0, 0, 5},
 					LocalPort: 22,
@@ -42,7 +42,7 @@ func Test_newNetTCP(t *testing.T) {
 					UID:       0,
 					Inode:     2740,
 				},
-				&netIPSocketLine{
+				{
 					Sl:        1,
 					LocalAddr: net.IP{0, 0, 0, 0},
 					LocalPort: 22,
@@ -54,7 +54,7 @@ func Test_newNetTCP(t *testing.T) {
 					UID:       0,
 					Inode:     2740,
 				},
-				&netIPSocketLine{
+				{
 					Sl:        2,
 					LocalAddr: net.IP{0, 0, 0, 0},
 					LocalPort: 22,
@@ -73,7 +73,7 @@ func Test_newNetTCP(t *testing.T) {
 			name: "tcp6 file found, no error should come up",
 			file: "testdata/fixtures/proc/net/tcp6",
 			want: []*netIPSocketLine{
-				&netIPSocketLine{
+				{
 					Sl:        1315,
 					LocalAddr: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
 					LocalPort: 5355,
@@ -85,7 +85,7 @@ func Test_newNetTCP(t *testing.T) {
 					UID:       981,
 					Inode:     21040,
 				},
-				&netIPSocketLine{
+				{
 					Sl:        6073,
 					LocalAddr: net.IP{254, 128, 0, 0, 0, 0, 0, 0, 86, 225, 173, 255, 254, 124, 102, 9},
 					LocalPort: 51073,
diff --git a/net_tls_stat_test.go b/net_tls_stat_test.go
new file mode 100644
index 0000000..8bc10e4
--- /dev/null
+++ b/net_tls_stat_test.go
@@ -0,0 +1,48 @@
+// Copyright 2023 Prometheus Team
+// 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 (
+	"testing"
+)
+
+func TestTLSStat(t *testing.T) {
+	tlsStats, err := getProcFixtures(t).NewTLSStat()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for _, test := range []struct {
+		name string
+		want int
+		got  int
+	}{
+		{name: "TLSCurrTxSw", want: 5, got: tlsStats.TLSCurrTxSw},
+		{name: "TLSCurrRxSw", want: 5, got: tlsStats.TLSCurrRxSw},
+		{name: "TLSCurrTxDevice", want: 0, got: tlsStats.TLSCurrTxDevice},
+		{name: "TLSCurrRxDevice", want: 0, got: tlsStats.TLSCurrRxDevice},
+		{name: "TLSTxSw", want: 8711, got: tlsStats.TLSTxSw},
+		{name: "TLSTxSw", want: 8711, got: tlsStats.TLSRxSw},
+		{name: "TLSTxDevice", want: 0, got: tlsStats.TLSTxDevice},
+		{name: "TLSRxDevice", want: 0, got: tlsStats.TLSRxDevice},
+		{name: "TLSDecryptError", want: 13, got: tlsStats.TLSDecryptError},
+		{name: "TLSRxDeviceResync", want: 0, got: tlsStats.TLSRxDeviceResync},
+		{name: "TLSDecryptRetry", want: 0, got: tlsStats.TLSDecryptRetry},
+		{name: "TLSRxNoPadViolation", want: 0, got: tlsStats.TLSRxNoPadViolation},
+	} {
+		if test.want != test.got {
+			t.Errorf("Want %s %d, have %d", test.name, test.want, test.got)
+		}
+	}
+}
diff --git a/net_udp_test.go b/net_udp_test.go
index 9abb64d..6576161 100644
--- a/net_udp_test.go
+++ b/net_udp_test.go
@@ -41,6 +41,7 @@ func Test_newNetUDP(t *testing.T) {
 					RxQueue:   1,
 					UID:       0,
 					Inode:     2740,
+					Drops:     intToU64(100),
 				},
 				&netIPSocketLine{
 					Sl:        1,
@@ -53,6 +54,7 @@ func Test_newNetUDP(t *testing.T) {
 					RxQueue:   0,
 					UID:       0,
 					Inode:     2740,
+					Drops:     intToU64(100),
 				},
 				&netIPSocketLine{
 					Sl:        2,
@@ -65,6 +67,7 @@ func Test_newNetUDP(t *testing.T) {
 					RxQueue:   1,
 					UID:       0,
 					Inode:     2740,
+					Drops:     intToU64(100),
 				},
 			},
 			wantErr: false,
@@ -84,6 +87,7 @@ func Test_newNetUDP(t *testing.T) {
 					RxQueue:   0,
 					UID:       981,
 					Inode:     21040,
+					Drops:     intToU64(0),
 				},
 				&netIPSocketLine{
 					Sl:        6073,
@@ -96,6 +100,7 @@ func Test_newNetUDP(t *testing.T) {
 					RxQueue:   0,
 					UID:       1000,
 					Inode:     11337031,
+					Drops:     intToU64(0),
 				},
 			},
 			wantErr: false,
@@ -137,13 +142,13 @@ func Test_newNetUDPSummary(t *testing.T) {
 		{
 			name:    "udp file found, no error should come up",
 			file:    "testdata/fixtures/proc/net/udp",
-			want:    &NetUDPSummary{TxQueueLength: 2, RxQueueLength: 2, UsedSockets: 3},
+			want:    &NetUDPSummary{TxQueueLength: 2, RxQueueLength: 2, UsedSockets: 3, Drops: intToU64(300)},
 			wantErr: false,
 		},
 		{
 			name:    "udp6 file found, no error should come up",
 			file:    "testdata/fixtures/proc/net/udp6",
-			want:    &NetUDPSummary{TxQueueLength: 0, RxQueueLength: 0, UsedSockets: 2},
+			want:    &NetUDPSummary{TxQueueLength: 0, RxQueueLength: 0, UsedSockets: 2, Drops: intToU64(0)},
 			wantErr: false,
 		},
 		{
@@ -172,3 +177,9 @@ func Test_newNetUDPSummary(t *testing.T) {
 		})
 	}
 }
+
+// intToU64 convert int to uint64 and return it pointer.
+func intToU64(i int) *uint64 {
+	cast := uint64(i)
+	return &cast
+}
diff --git a/sysfs/class_watchdog_test.go b/sysfs/class_watchdog_test.go
new file mode 100644
index 0000000..ad9d63a
--- /dev/null
+++ b/sysfs/class_watchdog_test.go
@@ -0,0 +1,76 @@
+// Copyright 2023 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.
+
+//go:build linux
+// +build linux
+
+package sysfs
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestWatchdogClass(t *testing.T) {
+	fs, err := NewFS(sysTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	got, err := fs.WatchdogClass()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var (
+		bootstatus int64 = 1
+		fwVersion  int64 = 2
+		nowayout   int64
+		timeleft   int64 = 300
+		timeout    int64 = 60
+		pretimeout int64 = 120
+		accessCs0  int64
+
+		options            = "0x8380"
+		identity           = "Software Watchdog"
+		state              = "active"
+		status             = "0x8000"
+		pretimeoutGovernor = "noop"
+	)
+
+	want := WatchdogClass{
+		"watchdog0": {
+			Name:               "watchdog0",
+			Bootstatus:         &bootstatus,
+			Options:            &options,
+			FwVersion:          &fwVersion,
+			Identity:           &identity,
+			Nowayout:           &nowayout,
+			State:              &state,
+			Status:             &status,
+			Timeleft:           &timeleft,
+			Timeout:            &timeout,
+			Pretimeout:         &pretimeout,
+			PretimeoutGovernor: &pretimeoutGovernor,
+			AccessCs0:          &accessCs0,
+		},
+		"watchdog1": {
+			Name: "watchdog1",
+		},
+	}
+
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Fatalf("unexpected watchdog class (-want +got):\n%s", diff)
+	}
+}
diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar
index 92f72fa..3fdfc5b 100644
--- a/testdata/fixtures.ttar
+++ b/testdata/fixtures.ttar
@@ -2317,7 +2317,7 @@ unused devices: <none>
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/meminfo
-Lines: 42
+Lines: 43
 MemTotal:       15666184 kB
 MemFree:          440324 kB
 Buffers:         1020128 kB
@@ -2351,6 +2351,7 @@ Committed_AS:     530844 kB
 VmallocTotal:   34359738367 kB
 VmallocUsed:       36596 kB
 VmallocChunk:   34359637840 kB
+Percpu:            26176 kB
 HardwareCorrupted:     0 kB
 AnonHugePages:     12288 kB
 HugePages_Total:       0
@@ -2527,6 +2528,22 @@ Lines: 3
  6073: 000080FE00000000FFADE15609667CFE:C781 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000  1000        0 11337031 2 00000000b9256fdd 0
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/tls_stat
+Lines: 12
+TlsCurrTxSw                     	5
+TlsCurrRxSw                     	5
+TlsCurrTxDevice                 	0
+TlsCurrRxDevice                 	0
+TlsTxSw                         	8711
+TlsRxSw                         	8711
+TlsTxDevice                     	0
+TlsRxDevice                     	0
+TlsDecryptError                 	13
+TlsRxDeviceResync               	0
+TlsDecryptRetry                 	0
+TlsRxNoPadViolation             	0
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/net/udp
 Lines: 4
   sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
@@ -6269,6 +6286,75 @@ Lines: 1
 acpitz
 Mode: 664
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/class/watchdog
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/class/watchdog/watchdog0
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/access_cs0
+Lines: 1
+0EOF
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/bootstatus
+Lines: 1
+1EOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/fw_version
+Lines: 1
+2EOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/identity
+Lines: 1
+Software WatchdogEOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/nowayout
+Lines: 1
+0EOF
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/options
+Lines: 1
+0x8380EOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/pretimeout
+Lines: 1
+120EOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/pretimeout_governor
+Lines: 1
+noopEOF
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/state
+Lines: 1
+activeEOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/status
+Lines: 1
+0x8000EOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/timeleft
+Lines: 1
+300EOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/class/watchdog/watchdog0/timeout
+Lines: 1
+60EOF
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/class/watchdog/watchdog1
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/devices
 Mode: 755
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -14229,6 +14315,14 @@ Lines: 1
 4096
 Mode: 444
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/fs/btrfs/0abb23a9-579b-43e6-ad30-227ef47fcb9d/commit_stats
+Lines: 4
+commits 258051
+last_commit_ms 1000
+max_commit_ms 51462
+total_commit_ms 47836090EOF
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/fs/btrfs/0abb23a9-579b-43e6-ad30-227ef47fcb9d/devices
 Mode: 755
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -