Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (

// DefaultSelinuxMountPoint is the common mount point of the selinuxfs.
DefaultSelinuxMountPoint = "/sys/fs/selinux"

// DefaultResctrlMountPoint is the common mount point of the resctrl filesystem.
DefaultResctrlMountPoint = "/sys/fs/resctrl"
)

// FS represents a pseudo-filesystem, normally /proc or /sys, which provides an
Expand Down
63 changes: 63 additions & 0 deletions resctrlfs/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 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

// Package resctrlfs provides access to the monitoring data of the resctrl
// filesystem.
//
// resctrl is the kernel interface to the cache and memory bandwidth resource
// control of the CPU: Intel calls it RDT (Resource Director Technology), AMD
// calls it PQoS (Platform Quality of Service), Arm calls it MPAM (Memory System
// Resource Partitioning and Monitoring). All vendors are served by the same
// kernel driver, so the file layout is identical.
//
// The filesystem is not mounted by default. Mount it with:
//
// mount -t resctrl resctrl /sys/fs/resctrl
//
// This package only reads monitoring data. It does not configure allocation.
//
// The filesystem is documented in the kernel tree:
// - https://docs.kernel.org/filesystems/resctrl.html
// - https://docs.kernel.org/arch/arm64/mpam.html
package resctrlfs

import (
"github.com/prometheus/procfs/internal/fs"
)

// FS represents the pseudo-filesystem resctrl, which provides an interface to
// the cache and memory bandwidth monitoring of the CPU.
type FS struct {
resctrl fs.FS
}

// DefaultMountPoint is the common mount point of the resctrl filesystem.
const DefaultMountPoint = fs.DefaultResctrlMountPoint

// NewDefaultFS returns a new FS mounted under the default mountPoint. It will error
// if the mount point can't be read.
func NewDefaultFS() (FS, error) {
return NewFS(DefaultMountPoint)
}

// NewFS returns a new FS mounted under the given mountPoint. It will error
// if the mount point can't be read.
func NewFS(mountPoint string) (FS, error) {
fs, err := fs.NewFS(mountPoint)
if err != nil {
return FS{}, err
}
return FS{fs}, nil
}
36 changes: 36 additions & 0 deletions resctrlfs/fs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 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

package resctrlfs

import "testing"

const (
resctrlTestFixtures = "testdata/fixtures" + DefaultMountPoint
)

func TestNewFS(t *testing.T) {
if _, err := NewFS("foobar"); err == nil {
t.Error("want NewFS to fail for non-existing mount point")
}

if _, err := NewFS("fs.go"); err == nil {
t.Error("want NewFS to fail if mount point is not a directory")
}

if _, err := NewFS(resctrlTestFixtures); err != nil {
t.Error("want NewFS to succeed if mount point exists")
}
}
66 changes: 66 additions & 0 deletions resctrlfs/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 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

package resctrlfs

import (
"strings"

"github.com/prometheus/procfs/internal/util"
)

const l3MonInfoPath = "info/L3_MON"

// L3MonInfo describes the L3 monitoring capabilities from info/L3_MON.
type L3MonInfo struct {
// NumRMIDs is the number of resource monitoring ids the hardware offers.
// One RMID is used per monitoring group, so this is the upper limit of
// groups that can be monitored at the same time.
NumRMIDs uint64
// MonFeatures lists the counters the hardware supports, in the order the
// kernel reports them, e.g. "llc_occupancy", "mbm_total_bytes",
// "mbm_local_bytes".
MonFeatures []string
}

// L3MonInfo returns the L3 monitoring capabilities of the CPU.
//
// It errors if info/L3_MON is missing, which means the CPU or the kernel does
// not support L3 monitoring. The error wraps the underlying os error, so it can
// be tested with os.IsNotExist.
func (fs FS) L3MonInfo() (L3MonInfo, error) {
var info L3MonInfo

numRMIDs, err := util.ReadUintFromFile(fs.resctrl.Path(l3MonInfoPath, "num_rmids"))
if err != nil {
return info, err
}
info.NumRMIDs = numRMIDs

data, err := util.ReadFileNoStat(fs.resctrl.Path(l3MonInfoPath, "mon_features"))
if err != nil {
return info, err
}

for _, line := range strings.Split(string(data), "\n") {
feature := strings.TrimSpace(line)
if feature == "" {
continue
}
info.MonFeatures = append(info.MonFeatures, feature)
}

return info, nil
}
63 changes: 63 additions & 0 deletions resctrlfs/info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 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

package resctrlfs

import (
"os"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestL3MonInfo(t *testing.T) {
fs, err := NewFS(resctrlTestFixtures)
if err != nil {
t.Fatalf("failed to open filesystem: %v", err)
}

got, err := fs.L3MonInfo()
if err != nil {
t.Fatalf("failed to read info/L3_MON: %v", err)
}

want := L3MonInfo{
NumRMIDs: 256,
MonFeatures: []string{
"llc_occupancy",
"mbm_total_bytes",
"mbm_local_bytes",
},
}

if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("unexpected L3 monitoring info (-want +got):\n%s", diff)
}
}

func TestL3MonInfoWithoutMonitoring(t *testing.T) {
fs, err := NewFS(t.TempDir())
if err != nil {
t.Fatalf("failed to open filesystem: %v", err)
}

_, err = fs.L3MonInfo()
if err == nil {
t.Fatal("want L3MonInfo to fail if info/L3_MON is missing")
}
if !os.IsNotExist(err) {
t.Errorf("want a not exist error, got %v", err)
}
}
134 changes: 134 additions & 0 deletions resctrlfs/mon_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 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

package resctrlfs

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/prometheus/procfs/internal/util"
)

const monDataPath = "mon_data"

// MonData holds the monitoring counters of one domain of one resource,
// read from mon_data/mon_<resource>_<id>.
type MonData struct {
Resource string // e.g. "L3"
ID string // domain id with leading zeros stripped, e.g. "0"
// Counters are nil when the file is absent (feature not supported)
// or reads "Unavailable" (no sample available right now). For example
// Arm MPAM never offers MBMLocalBytes, because the hardware can not
// tell local from remote traffic.
LLCOccupancy *uint64
MBMTotalBytes *uint64
MBMLocalBytes *uint64
}

// MonData returns the monitoring counters of the root control group, one entry
// per monitoring domain. A domain is a socket, or a CCX on AMD.
//
// It errors only if the mon_data directory itself can't be read. A domain
// counter that is missing, unreadable or unavailable stays nil, because the
// hardware may support only some of the features and samples are not always
// available. A mount without any monitoring domain returns an empty slice.
func (fs FS) MonData() ([]MonData, error) {
path := fs.resctrl.Path(monDataPath)

dirs, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("failed to list monitoring domains at %q: %w", path, err)
}

// os.ReadDir sorts by filename, so the result is ordered by domain.
mons := make([]MonData, 0, len(dirs))
for _, d := range dirs {
resource, id, ok := parseMonDirName(d.Name())
if !ok {
continue
}

domain := fs.resctrl.Path(monDataPath, d.Name())
mons = append(mons, MonData{
Resource: resource,
ID: id,
LLCOccupancy: readCounter(domain, "llc_occupancy"),
// The mbm counters are hardware registers of limited width. They
// are free running and wrap around; this library reports the raw
// value and leaves the wrap handling to the caller.
MBMTotalBytes: readCounter(domain, "mbm_total_bytes"),
MBMLocalBytes: readCounter(domain, "mbm_local_bytes"),
})
}

return mons, nil
}

// readCounter reads a single counter file of a monitoring domain. It returns
// nil if the file does not exist, can not be read, or does not hold a number.
// The kernel writes the literal "Unavailable" when the hardware can not
// deliver a sample, which is a normal and transient state.
func readCounter(domain, name string) *uint64 {
value, err := util.ReadUintFromFile(filepath.Join(domain, name))
if err != nil {
return nil
}
return &value
}

// parseMonDirName splits a mon_data directory name into resource and domain id.
// The name is "mon_<resource>_<id>", for example "mon_L3_00". The resource name
// may itself contain underscores, so the split is at the last one. The id is
// zero padded by the kernel; the padding is stripped, so "00" becomes "0".
// The second return value is false for a name that is not a monitoring domain.
func parseMonDirName(name string) (string, string, bool) {
rest, ok := strings.CutPrefix(name, "mon_")
if !ok {
return "", "", false
}

i := strings.LastIndex(rest, "_")
if i <= 0 {
return "", "", false
}

resource, id := rest[:i], rest[i+1:]
if !isDigits(id) {
return "", "", false
}

id = strings.TrimLeft(id, "0")
if id == "" {
id = "0"
}

return resource, id, true
}

// isDigits reports whether s is a non-empty run of decimal digits.
func isDigits(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
Loading