Repository: prometheus/procfs
Pull request #486 (2022-12-26 19:16:12)

Enable oppportunistic fd counting fast path
Existing slow path takes ~725ms of CPU time on my laptopto count 1 million open files. Compare this to just 0.25ms for the baseline, when no extra files are open by a Go program:

```
Open files reported: 7
 Gathered metrics in 0.26ms
Open files reported: 1000007
 Gathered metrics in 724.50ms
```

Adding fastpath from Linux v6.2 makes it fast:

```
Open files reported: 6
 Gathered metrics in 0.29ms
Open files reported: 1000006
 Gathered metrics in 0.31ms
```

This is before taking in account any lock contention effects in the kernel if you try to count files from multiple threads concurrently, which makes the slow path even slower, burning a lot more CPU in the process. See:

* https://github.com/torvalds/linux/commit/f1f1f2569901

<details>
  <summary>The code I used</summary>

```go
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/prometheus/client_golang/prometheus"
)

func main() {
	run()

	openLotsOfFiles(1000000)

	run()
}

func run() {
	s := time.Now()

	metrics, err := prometheus.DefaultGatherer.Gather()
	if err != nil {
		panic(err)
	}

	for _, metric := range metrics {
		if metric.GetName() == "process_open_fds" {
			fmt.Printf("Open files reported: %.0f\n", *metric.Metric[0].Gauge.Value)
		}
	}

	fmt.Printf(" Gathered metrics in %.2fms\n", float64(time.Since(s).Microseconds())/1000)
}

func openLotsOfFiles(lots int) []*os.File {
	files := make([]*os.File, lots)

	for i := 0; i < lots; i++ {
		file, err := os.Open("/etc/hosts")
		if err != nil {
			panic(err)
		}

		files[i] = file
	}

	return files
}
```

```
go build -o /tmp/lol .
```

```
for i in $(seq 1 5); do GOMAXPROCS=1 /tmp/lol; done
```

</details>

bobrik: Tests are unhappy:

```
--- FAIL: TestFileDescriptorsLen (0.00s)
    proc_test.go:244: want fds 5, have 224
```

That's because they make `FileDescriptorsLen` look at the `Stat()` result for a real directory, which behaves differently from `/proc/pid/fd`. I'm open to suggestions on how to address this in tests.

```
ivan@vm:~/projects/prometheus-procfs$ stat testdata/fixtures/proc/26231/fd | head -2
  File: testdata/fixtures/proc/26231/fd
  Size: 224       	Blocks: 0          IO Block: 1048576 directory
```

```
ivan@vm:~/projects/prometheus-procfs$ ls testdata/fixtures/proc/26231/fd | wc -l
5
```
discordianfish: No clear recommendation for the tests.. Might need to restructure this to fix it.
bobrik: I added a commit with a possible solution. I'm not sure if it's a good one, but I can't think of anything better.

Initially I hoped for a `procfs` detection in `stat()` syscall result itself, but it doesn't seem possible. Adding another syscall to double check whether `stat()` result can be trusted doesn't seem right either, as we only need to learn this fact once per mountpoint.

Patch:
diff --git a/Makefile.common b/Makefile.common
index 1985fcd..6d8007c 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -61,7 +61,7 @@ PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_
 SKIP_GOLANGCI_LINT :=
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v1.50.1
+GOLANGCI_LINT_VERSION ?= v1.51.2
 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
diff --git a/fs.go b/fs.go
index 0102ab0..df9044e 100644
--- a/fs.go
+++ b/fs.go
@@ -14,6 +14,8 @@
 package procfs
 
 import (
+	"syscall"
+
 	"github.com/prometheus/procfs/internal/fs"
 )
 
@@ -21,6 +23,7 @@ import (
 // kernel data structures.
 type FS struct {
 	proc fs.FS
+	real bool
 }
 
 // DefaultMountPoint is the common mount point of the proc filesystem.
@@ -39,5 +42,23 @@ func NewFS(mountPoint string) (FS, error) {
 	if err != nil {
 		return FS{}, err
 	}
-	return FS{fs}, nil
+
+	real, err := isRealProc(mountPoint)
+	if err != nil {
+		return FS{}, err
+	}
+
+	return FS{fs, real}, nil
+}
+
+// isRealProc determines whether supplied mountpoint is really a proc filesystem.
+func isRealProc(mountPoint string) (bool, error) {
+	stat := syscall.Statfs_t{}
+	err := syscall.Statfs(mountPoint, &stat)
+	if err != nil {
+		return false, err
+	}
+
+	// 0x9fa0 is PROC_SUPER_MAGIC: https://elixir.bootlin.com/linux/v6.1/source/include/uapi/linux/magic.h#L87
+	return stat.Type == 0x9fa0, nil
 }
diff --git a/go.mod b/go.mod
index d88b439..7ae864d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,5 +5,5 @@ go 1.18
 require (
 	github.com/google/go-cmp v0.5.9
 	golang.org/x/sync v0.1.0
-	golang.org/x/sys v0.3.0
+	golang.org/x/sys v0.5.0
 )
diff --git a/go.sum b/go.sum
index f7fa868..b6a2cec 100644
--- a/go.sum
+++ b/go.sum
@@ -2,5 +2,5 @@ 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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/net_wireless.go b/net_wireless.go
new file mode 100644
index 0000000..c80fb15
--- /dev/null
+++ b/net_wireless.go
@@ -0,0 +1,182 @@
+// 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.
+
+package procfs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+// Wireless models the content of /proc/net/wireless.
+type Wireless struct {
+	Name string
+
+	// Status is the current 4-digit hex value status of the interface.
+	Status uint64
+
+	// QualityLink is the link quality.
+	QualityLink int
+
+	// QualityLevel is the signal gain (dBm).
+	QualityLevel int
+
+	// QualityNoise is the signal noise baseline (dBm).
+	QualityNoise int
+
+	// DiscardedNwid is the number of discarded packets with wrong nwid/essid.
+	DiscardedNwid int
+
+	// DiscardedCrypt is the number of discarded packets with wrong code/decode (WEP).
+	DiscardedCrypt int
+
+	// DiscardedFrag is the number of discarded packets that can't perform MAC reassembly.
+	DiscardedFrag int
+
+	// DiscardedRetry is the number of discarded packets that reached max MAC retries.
+	DiscardedRetry int
+
+	// DiscardedMisc is the number of discarded packets for other reasons.
+	DiscardedMisc int
+
+	// MissedBeacon is the number of missed beacons/superframe.
+	MissedBeacon int
+}
+
+// Wireless returns kernel wireless statistics.
+func (fs FS) Wireless() ([]*Wireless, error) {
+	b, err := util.ReadFileNoStat(fs.proc.Path("net/wireless"))
+	if err != nil {
+		return nil, err
+	}
+
+	m, err := parseWireless(bytes.NewReader(b))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse wireless: %w", err)
+	}
+
+	return m, nil
+}
+
+// parseWireless parses the contents of /proc/net/wireless.
+/*
+Inter-| sta-|   Quality        |   Discarded packets               | Missed | WE
+face | tus | link level noise |  nwid  crypt   frag  retry   misc | beacon | 22
+ eth1: 0000    5.  -256.  -10.       0      1      0     3      0        0
+ eth2: 0000    5.  -256.  -20.       0      2      0     4      0        0
+*/
+func parseWireless(r io.Reader) ([]*Wireless, error) {
+	var (
+		interfaces []*Wireless
+		scanner    = bufio.NewScanner(r)
+	)
+
+	for n := 0; scanner.Scan(); n++ {
+		// Skip the 2 header lines.
+		if n < 2 {
+			continue
+		}
+
+		line := scanner.Text()
+
+		parts := strings.Split(line, ":")
+		if len(parts) != 2 {
+			return nil, fmt.Errorf("expected 2 parts after splitting line by ':', got %d for line %q", len(parts), line)
+		}
+
+		name := strings.TrimSpace(parts[0])
+		stats := strings.Fields(parts[1])
+
+		if len(stats) < 10 {
+			return nil, fmt.Errorf("invalid number of fields in line %d, expected at least 10, got %d: %q", n, len(stats), line)
+		}
+
+		status, err := strconv.ParseUint(stats[0], 16, 16)
+		if err != nil {
+			return nil, fmt.Errorf("invalid status in line %d: %q", n, line)
+		}
+
+		qlink, err := strconv.Atoi(strings.TrimSuffix(stats[1], "."))
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Quality:link as integer %q: %w", qlink, err)
+		}
+
+		qlevel, err := strconv.Atoi(strings.TrimSuffix(stats[2], "."))
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Quality:level as integer %q: %w", qlevel, err)
+		}
+
+		qnoise, err := strconv.Atoi(strings.TrimSuffix(stats[3], "."))
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Quality:noise as integer %q: %w", qnoise, err)
+		}
+
+		dnwid, err := strconv.Atoi(stats[4])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:nwid as integer %q: %w", dnwid, err)
+		}
+
+		dcrypt, err := strconv.Atoi(stats[5])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:crypt as integer %q: %w", dcrypt, err)
+		}
+
+		dfrag, err := strconv.Atoi(stats[6])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:frag as integer %q: %w", dfrag, err)
+		}
+
+		dretry, err := strconv.Atoi(stats[7])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:retry as integer %q: %w", dretry, err)
+		}
+
+		dmisc, err := strconv.Atoi(stats[8])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Discarded:misc as integer %q: %w", dmisc, err)
+		}
+
+		mbeacon, err := strconv.Atoi(stats[9])
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse Missed:beacon as integer %q: %w", mbeacon, err)
+		}
+
+		w := &Wireless{
+			Name:           name,
+			Status:         status,
+			QualityLink:    qlink,
+			QualityLevel:   qlevel,
+			QualityNoise:   qnoise,
+			DiscardedNwid:  dnwid,
+			DiscardedCrypt: dcrypt,
+			DiscardedFrag:  dfrag,
+			DiscardedRetry: dretry,
+			DiscardedMisc:  dmisc,
+			MissedBeacon:   mbeacon,
+		}
+
+		interfaces = append(interfaces, w)
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, fmt.Errorf("failed to scan /proc/net/wireless: %w", err)
+	}
+
+	return interfaces, nil
+}
diff --git a/proc.go b/proc.go
index c30223a..48f39da 100644
--- a/proc.go
+++ b/proc.go
@@ -21,7 +21,6 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/prometheus/procfs/internal/fs"
 	"github.com/prometheus/procfs/internal/util"
 )
 
@@ -30,7 +29,7 @@ type Proc struct {
 	// The process ID.
 	PID int
 
-	fs fs.FS
+	fs FS
 }
 
 // Procs represents a list of Proc structs.
@@ -92,7 +91,7 @@ func (fs FS) Proc(pid int) (Proc, error) {
 	if _, err := os.Stat(fs.proc.Path(strconv.Itoa(pid))); err != nil {
 		return Proc{}, err
 	}
-	return Proc{PID: pid, fs: fs.proc}, nil
+	return Proc{PID: pid, fs: fs}, nil
 }
 
 // AllProcs returns a list of all currently available processes.
@@ -114,7 +113,7 @@ func (fs FS) AllProcs() (Procs, error) {
 		if err != nil {
 			continue
 		}
-		p = append(p, Proc{PID: int(pid), fs: fs.proc})
+		p = append(p, Proc{PID: int(pid), fs: fs})
 	}
 
 	return p, nil
@@ -237,6 +236,19 @@ func (p Proc) FileDescriptorTargets() ([]string, error) {
 // FileDescriptorsLen returns the number of currently open file descriptors of
 // a process.
 func (p Proc) FileDescriptorsLen() (int, error) {
+	// Use fast path if available (Linux v6.2): https://github.com/torvalds/linux/commit/f1f1f2569901
+	if p.fs.real {
+		stat, err := os.Stat(p.path("fd"))
+		if err != nil {
+			return 0, err
+		}
+
+		size := stat.Size()
+		if size > 0 {
+			return int(size), nil
+		}
+	}
+
 	fds, err := p.fileDescriptors()
 	if err != nil {
 		return 0, err
@@ -285,7 +297,7 @@ func (p Proc) fileDescriptors() ([]string, error) {
 }
 
 func (p Proc) path(pa ...string) string {
-	return p.fs.Path(append([]string{strconv.Itoa(p.PID)}, pa...)...)
+	return p.fs.proc.Path(append([]string{strconv.Itoa(p.PID)}, pa...)...)
 }
 
 // FileDescriptorsInfo retrieves information about all file descriptors of
diff --git a/proc_stat.go b/proc_stat.go
index b278eb2..14b249f 100644
--- a/proc_stat.go
+++ b/proc_stat.go
@@ -18,7 +18,6 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/prometheus/procfs/internal/fs"
 	"github.com/prometheus/procfs/internal/util"
 )
 
@@ -112,7 +111,7 @@ type ProcStat struct {
 	// Aggregated block I/O delays, measured in clock ticks (centiseconds).
 	DelayAcctBlkIOTicks uint64
 
-	proc fs.FS
+	proc FS
 }
 
 // NewStat returns the current status information of the process.
@@ -210,8 +209,7 @@ func (s ProcStat) ResidentMemory() int {
 
 // StartTime returns the unix timestamp of the process in seconds.
 func (s ProcStat) StartTime() (float64, error) {
-	fs := FS{proc: s.proc}
-	stat, err := fs.Stat()
+	stat, err := s.proc.Stat()
 	if err != nil {
 		return 0, err
 	}
diff --git a/sysfs/vmstat_numa.go b/sysfs/vmstat_numa.go
index 8a6d095..038a0ee 100644
--- a/sysfs/vmstat_numa.go
+++ b/sysfs/vmstat_numa.go
@@ -145,7 +145,7 @@ func parseVMStatNUMA(r []byte) (VMStat, error) {
 		case "nr_zone_active_anon":
 			vmStat.NrZoneActiveAnon = fv
 		case "nr_zone_inactive_file":
-			vmStat.NrZoneActiveFile = fv
+			vmStat.NrZoneInactiveFile = fv
 		case "nr_zone_active_file":
 			vmStat.NrZoneActiveFile = fv
 		case "nr_zone_unevictable":
diff --git a/thread.go b/thread.go
index f08bfc7..490c147 100644
--- a/thread.go
+++ b/thread.go
@@ -54,7 +54,8 @@ func (fs FS) AllThreads(pid int) (Procs, error) {
 		if err != nil {
 			continue
 		}
-		t = append(t, Proc{PID: int(tid), fs: fsi.FS(taskPath)})
+
+		t = append(t, Proc{PID: int(tid), fs: FS{fsi.FS(taskPath), fs.real}})
 	}
 
 	return t, nil
@@ -66,13 +67,13 @@ func (fs FS) Thread(pid, tid int) (Proc, error) {
 	if _, err := os.Stat(taskPath); err != nil {
 		return Proc{}, err
 	}
-	return Proc{PID: tid, fs: fsi.FS(taskPath)}, nil
+	return Proc{PID: tid, fs: FS{fsi.FS(taskPath), fs.real}}, nil
 }
 
 // Thread returns a process for a given TID of Proc.
 func (proc Proc) Thread(tid int) (Proc, error) {
-	tfs := fsi.FS(proc.path("task"))
-	if _, err := os.Stat(tfs.Path(strconv.Itoa(tid))); err != nil {
+	tfs := FS{fsi.FS(proc.path("task")), proc.fs.real}
+	if _, err := os.Stat(tfs.proc.Path(strconv.Itoa(tid))); err != nil {
 		return Proc{}, err
 	}
 	return Proc{PID: tid, fs: tfs}, nil


Test patch:
diff --git a/net_wireless_test.go b/net_wireless_test.go
new file mode 100644
index 0000000..25d6a65
--- /dev/null
+++ b/net_wireless_test.go
@@ -0,0 +1,70 @@
+// 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.
+
+package procfs
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestWireless(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatalf("failed to open procfs: %v", err)
+	}
+
+	got, err := fs.Wireless()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := []*Wireless{
+		{
+			Name:           "wlan0",
+			Status:         1,
+			QualityLink:    2,
+			QualityLevel:   3,
+			QualityNoise:   4,
+			DiscardedNwid:  5,
+			DiscardedCrypt: 6,
+			DiscardedFrag:  7,
+			DiscardedRetry: 8,
+			DiscardedMisc:  9,
+			MissedBeacon:   10,
+		},
+		{
+			Name:           "wlan1",
+			Status:         16,
+			QualityLink:    9,
+			QualityLevel:   8,
+			QualityNoise:   7,
+			DiscardedNwid:  6,
+			DiscardedCrypt: 5,
+			DiscardedFrag:  4,
+			DiscardedRetry: 3,
+			DiscardedMisc:  2,
+			MissedBeacon:   1,
+		},
+	}
+
+	if len(got) != len(expected) {
+		t.Fatalf("unexpected number of interfaces parsed %d, expected %d", len(got), len(expected))
+	}
+
+	for i, iface := range got {
+		if !reflect.DeepEqual(iface, expected[i]) {
+			t.Errorf("unexpected interface got %+v, expected %+v", iface, expected[i])
+		}
+	}
+}
diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar
index a965b11..e9a8cd0 100644
--- a/testdata/fixtures.ttar
+++ b/testdata/fixtures.ttar
@@ -2568,6 +2568,14 @@ Num       RefCount Protocol Flags    Type St Path
 0000000000000000: 00000003 00000000 00000000 0001 03
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/wireless
+Lines: 4
+Inter-| sta-|   Quality        |   Discarded packets               | Missed
+ face | tus | link level noise |  nwid  crypt   frag  retry   misc | beacon
+ wlan0: 0001    2.    3.    4.       5      6      7      8      9       10
+ wlan1: 0010    9     8.    7.       6      5      4      3      2        1
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/net/xfrm_stat
 Lines: 28
 XfrmInError                     1
diff --git a/thread_test.go b/thread_test.go
index 503be4f..1b83046 100644
--- a/thread_test.go
+++ b/thread_test.go
@@ -37,7 +37,7 @@ func TestAllThreads(t *testing.T) {
 			t.Errorf("want TID %d, have %d", wantTID, haveTID)
 		}
 		wantFS := fixFS.proc.Path(strconv.Itoa(testPID), "task")
-		haveFS := string(threads[i].fs)
+		haveFS := string(threads[i].fs.proc)
 		if wantFS != haveFS {
 			t.Errorf("want fs %q, have %q", wantFS, haveFS)
 		}