| // 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 sysfs |
| |
| import ( |
| "fmt" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "golang.org/x/sync/errgroup" |
| |
| "github.com/prometheus/procfs/internal/util" |
| ) |
| |
| // CPU represents a path to a CPU located in `/sys/devices/system/cpu/cpu[0-9]*`. |
| type CPU string |
| |
| // Number returns the ID number of the given CPU. |
| func (c CPU) Number() string { |
| return strings.TrimPrefix(filepath.Base(string(c)), "cpu") |
| } |
| |
| // CPUTopology contains data located in `/sys/devices/system/cpu/cpu[0-9]*/topology`. |
| type CPUTopology struct { |
| CoreID string |
| CoreSiblingsList string |
| PhysicalPackageID string |
| ThreadSiblingsList string |
| } |
| |
| // CPUThermalThrottle contains data from `/sys/devices/system/cpu/cpu[0-9]*/thermal_throttle`. |
| type CPUThermalThrottle struct { |
| CoreThrottleCount uint64 |
| PackageThrottleCount uint64 |
| } |
| |
| // SystemCPUCpufreqStats contains stats from `/sys/devices/system/cpu/cpu[0-9]*/cpufreq/...`. |
| type SystemCPUCpufreqStats struct { |
| Name string |
| CpuinfoCurrentFrequency *uint64 |
| CpuinfoMinimumFrequency *uint64 |
| CpuinfoMaximumFrequency *uint64 |
| CpuinfoTransitionLatency *uint64 |
| ScalingCurrentFrequency *uint64 |
| ScalingMinimumFrequency *uint64 |
| ScalingMaximumFrequency *uint64 |
| AvailableGovernors string |
| Driver string |
| Governor string |
| RelatedCpus string |
| SetSpeed string |
| // Refer `CONFIG_CPU_FREQ_STAT`: https://www.kernel.org/doc/html/latest/cpu-freq/cpufreq-stats.html#configuring-cpufreq-stats |
| CpuinfoFrequencyDuration *map[uint64]uint64 |
| CpuinfoFrequencyTransitionsTotal *uint64 |
| CpuinfoTransitionTable *[][]uint64 |
| } |
| |
| // CPUs returns a slice of all CPUs in `/sys/devices/system/cpu`. |
| func (fs FS) CPUs() ([]CPU, error) { |
| cpuPaths, err := filepath.Glob(fs.sys.Path("devices/system/cpu/cpu[0-9]*")) |
| if err != nil { |
| return nil, err |
| } |
| cpus := make([]CPU, len(cpuPaths)) |
| for i, cpu := range cpuPaths { |
| cpus[i] = CPU(cpu) |
| } |
| return cpus, nil |
| } |
| |
| // Topology gets the topology information for a single CPU from `/sys/devices/system/cpu/cpuN/topology`. |
| func (c CPU) Topology() (*CPUTopology, error) { |
| cpuTopologyPath := filepath.Join(string(c), "topology") |
| if _, err := os.Stat(cpuTopologyPath); err != nil { |
| return nil, err |
| } |
| t, err := parseCPUTopology(cpuTopologyPath) |
| if err != nil { |
| return nil, err |
| } |
| return t, nil |
| } |
| |
| func parseCPUTopology(cpuPath string) (*CPUTopology, error) { |
| t := CPUTopology{} |
| var err error |
| t.CoreID, err = util.SysReadFile(filepath.Join(cpuPath, "core_id")) |
| if err != nil { |
| return nil, err |
| } |
| t.PhysicalPackageID, err = util.SysReadFile(filepath.Join(cpuPath, "physical_package_id")) |
| if err != nil { |
| return nil, err |
| } |
| t.CoreSiblingsList, err = util.SysReadFile(filepath.Join(cpuPath, "core_siblings_list")) |
| if err != nil { |
| return nil, err |
| } |
| t.ThreadSiblingsList, err = util.SysReadFile(filepath.Join(cpuPath, "thread_siblings_list")) |
| if err != nil { |
| return nil, err |
| } |
| return &t, nil |
| } |
| |
| // ThermalThrottle gets the cpu throttle count information for a single CPU from `/sys/devices/system/cpu/cpuN/thermal_throttle`. |
| func (c CPU) ThermalThrottle() (*CPUThermalThrottle, error) { |
| cpuPath := filepath.Join(string(c), "thermal_throttle") |
| if _, err := os.Stat(cpuPath); err != nil { |
| return nil, err |
| } |
| t, err := parseCPUThermalThrottle(cpuPath) |
| if err != nil { |
| return nil, err |
| } |
| return t, nil |
| } |
| |
| // Online returns the online status of a CPU from `/sys/devices/system/cpu/cpuN/online`. |
| func (c CPU) Online() (bool, error) { |
| cpuPath := filepath.Join(string(c), "online") |
| str, err := util.SysReadFile(cpuPath) |
| if err != nil { |
| return false, err |
| } |
| return str == "1", nil |
| } |
| |
| func parseCPUThermalThrottle(cpuPath string) (*CPUThermalThrottle, error) { |
| t := CPUThermalThrottle{} |
| var err error |
| t.PackageThrottleCount, err = util.ReadUintFromFile(filepath.Join(cpuPath, "package_throttle_count")) |
| if err != nil { |
| return nil, err |
| } |
| t.CoreThrottleCount, err = util.ReadUintFromFile(filepath.Join(cpuPath, "core_throttle_count")) |
| if err != nil { |
| return nil, err |
| } |
| return &t, nil |
| } |
| |
| func binSearch(elem uint16, elemSlice *[]uint16) bool { |
| if len(*elemSlice) == 0 { |
| return false |
| } |
| |
| if len(*elemSlice) == 1 { |
| return elem == (*elemSlice)[0] |
| } |
| |
| start := 0 |
| end := len(*elemSlice) - 1 |
| |
| var mid int |
| |
| for start <= end { |
| mid = (start + end) / 2 |
| switch { |
| case (*elemSlice)[mid] == elem: |
| return true |
| case (*elemSlice)[mid] > elem: |
| end = mid - 1 |
| case (*elemSlice)[mid] < elem: |
| start = mid + 1 |
| } |
| } |
| |
| return false |
| } |
| |
| func filterOfflineCPUs(offlineCpus *[]uint16, cpus *[]string) ([]string, error) { |
| var filteredCPUs []string |
| for _, cpu := range *cpus { |
| cpuName := strings.TrimPrefix(filepath.Base(cpu), "cpu") |
| cpuNameUint16, err := strconv.Atoi(cpuName) |
| if err != nil { |
| return nil, err |
| } |
| if !binSearch(uint16(cpuNameUint16), offlineCpus) { |
| filteredCPUs = append(filteredCPUs, cpu) |
| } |
| } |
| |
| return filteredCPUs, nil |
| } |
| |
| // SystemCpufreq returns CPU frequency metrics for all CPUs. |
| func (fs FS) SystemCpufreq() ([]SystemCPUCpufreqStats, error) { |
| var g errgroup.Group |
| |
| cpus, err := filepath.Glob(fs.sys.Path("devices/system/cpu/cpu[0-9]*")) |
| if err != nil { |
| return nil, err |
| } |
| |
| line, err := util.ReadFileNoStat(fs.sys.Path("devices/system/cpu/offline")) |
| if err != nil { |
| return nil, err |
| } |
| |
| if strings.TrimSpace(string(line)) != "" { |
| offlineCPUs, err := parseCPURange(line) |
| if err != nil { |
| return nil, err |
| } |
| |
| if len(offlineCPUs) > 0 { |
| cpus, err = filterOfflineCPUs(&offlineCPUs, &cpus) |
| if err != nil { |
| return nil, err |
| } |
| } |
| } |
| |
| systemCpufreq := make([]SystemCPUCpufreqStats, len(cpus)) |
| for i, cpu := range cpus { |
| cpuName := strings.TrimPrefix(filepath.Base(cpu), "cpu") |
| |
| cpuCpufreqPath := filepath.Join(cpu, "cpufreq") |
| _, err = os.Stat(cpuCpufreqPath) |
| if os.IsNotExist(err) { |
| continue |
| } else if err != nil { |
| return nil, err |
| } |
| |
| // Execute the parsing of each CPU in parallel. |
| // This is done because the kernel intentionally delays access to each CPU by |
| // 50 milliseconds to avoid DDoSing possibly expensive functions. |
| i := i // https://golang.org/doc/faq#closures_and_goroutines |
| g.Go(func() error { |
| cpufreq, err := parseCpufreqCpuinfo(cpuCpufreqPath) |
| if err == nil { |
| cpufreq.Name = cpuName |
| systemCpufreq[i] = *cpufreq |
| } |
| return err |
| }) |
| } |
| |
| err = g.Wait() |
| if err != nil { |
| return nil, err |
| } |
| |
| if len(systemCpufreq) == 0 { |
| return nil, fmt.Errorf("could not find any cpufreq files: %w", os.ErrNotExist) |
| } |
| |
| return systemCpufreq, nil |
| } |
| |
| func parseCpufreqCpuinfo(cpuPath string) (*SystemCPUCpufreqStats, error) { |
| uintFiles := []string{ |
| "cpuinfo_cur_freq", |
| "cpuinfo_max_freq", |
| "cpuinfo_min_freq", |
| "cpuinfo_transition_latency", |
| "scaling_cur_freq", |
| "scaling_max_freq", |
| "scaling_min_freq", |
| } |
| uintOut := make([]*uint64, len(uintFiles)) |
| |
| for i, f := range uintFiles { |
| v, err := util.ReadUintFromFile(filepath.Join(cpuPath, f)) |
| if err != nil { |
| if os.IsNotExist(err) || os.IsPermission(err) { |
| continue |
| } |
| return &SystemCPUCpufreqStats{}, err |
| } |
| |
| uintOut[i] = &v |
| } |
| |
| stringFiles := []string{ |
| "scaling_available_governors", |
| "scaling_driver", |
| "scaling_governor", |
| "related_cpus", |
| "scaling_setspeed", |
| } |
| stringOut := make([]string, len(stringFiles)) |
| var err error |
| |
| for i, f := range stringFiles { |
| stringOut[i], err = util.SysReadFile(filepath.Join(cpuPath, f)) |
| if err != nil { |
| return &SystemCPUCpufreqStats{}, err |
| } |
| } |
| |
| // "total_trans" is the total number of times the CPU has changed frequency. |
| var cpuinfoFrequencyTransitionsTotal *uint64 |
| cpuinfoFrequencyTransitionsTotalUint, err := util.ReadUintFromFile(filepath.Join(cpuPath, "stats", "total_trans")) |
| if err != nil { |
| if !os.IsNotExist(err) && !os.IsPermission(err) { |
| return &SystemCPUCpufreqStats{}, err |
| } |
| } else { |
| cpuinfoFrequencyTransitionsTotal = &cpuinfoFrequencyTransitionsTotalUint |
| } |
| |
| // "time_in_state" is the total time spent at each frequency. |
| var cpuinfoFrequencyDuration *map[uint64]uint64 |
| cpuinfoFrequencyDurationString, err := util.ReadFileNoStat(filepath.Join(cpuPath, "stats", "time_in_state")) |
| if err != nil { |
| if !os.IsNotExist(err) && !os.IsPermission(err) { |
| return &SystemCPUCpufreqStats{}, err |
| } |
| } else { |
| cpuinfoFrequencyDuration = &map[uint64]uint64{} |
| for line := range strings.SplitSeq(string(cpuinfoFrequencyDurationString), "\n") { |
| if line == "" { |
| continue |
| } |
| fields := strings.Fields(line) |
| if len(fields) != 2 { |
| return &SystemCPUCpufreqStats{}, fmt.Errorf("unexpected number of fields in time_in_state: %v", fields) |
| } |
| freq, err := strconv.ParseUint(fields[0], 10, 64) |
| if err != nil { |
| return &SystemCPUCpufreqStats{}, err |
| } |
| duration, err := strconv.ParseUint(fields[1], 10, 64) |
| if err != nil { |
| return &SystemCPUCpufreqStats{}, err |
| } |
| (*cpuinfoFrequencyDuration)[freq] = duration |
| } |
| } |
| |
| // "trans_table" contains information about all the CPU frequency transitions. |
| var cpuinfoTransitionTable *[][]uint64 |
| cpuinfoTransitionTableString, err := util.ReadFileNoStat(filepath.Join(cpuPath, "stats", "trans_table")) |
| if err != nil { |
| if !os.IsNotExist(err) && !os.IsPermission(err) { |
| return &SystemCPUCpufreqStats{}, err |
| } |
| } else { |
| cpuinfoTransitionTable = &[][]uint64{} |
| for i, line := range strings.Split(string(cpuinfoTransitionTableString), "\n") { |
| // Skip the "From: To" header. |
| if i == 0 || line == "" { |
| continue |
| } |
| fields := strings.Fields(line) |
| fields[0] = strings.TrimSuffix(fields[0], ":") |
| cpuinfoTransitionTableRow := make([]uint64, len(fields)) |
| for i := range fields { |
| if fields[i] == "" { |
| continue |
| } |
| f, err := strconv.ParseUint(fields[i], 10, 64) |
| if err != nil { |
| return &SystemCPUCpufreqStats{}, err |
| } |
| cpuinfoTransitionTableRow[i] = f |
| } |
| *cpuinfoTransitionTable = append(*cpuinfoTransitionTable, cpuinfoTransitionTableRow) |
| } |
| } |
| |
| return &SystemCPUCpufreqStats{ |
| CpuinfoCurrentFrequency: uintOut[0], |
| CpuinfoMaximumFrequency: uintOut[1], |
| CpuinfoMinimumFrequency: uintOut[2], |
| CpuinfoTransitionLatency: uintOut[3], |
| ScalingCurrentFrequency: uintOut[4], |
| ScalingMaximumFrequency: uintOut[5], |
| ScalingMinimumFrequency: uintOut[6], |
| AvailableGovernors: stringOut[0], |
| Driver: stringOut[1], |
| Governor: stringOut[2], |
| RelatedCpus: stringOut[3], |
| SetSpeed: stringOut[4], |
| CpuinfoFrequencyDuration: cpuinfoFrequencyDuration, |
| CpuinfoFrequencyTransitionsTotal: cpuinfoFrequencyTransitionsTotal, |
| CpuinfoTransitionTable: cpuinfoTransitionTable, |
| }, nil |
| } |
| |
| func (fs FS) IsolatedCPUs() ([]uint16, error) { |
| isolcpus, err := os.ReadFile(fs.sys.Path("devices/system/cpu/isolated")) |
| if err != nil { |
| return nil, err |
| } |
| |
| return parseCPURange(isolcpus) |
| } |
| |
| func parseCPURange(data []byte) ([]uint16, error) { |
| |
| var cpusInt = []uint16{} |
| |
| for cpu := range strings.SplitSeq(strings.TrimSuffix(string(data), "\n"), ",") { |
| if cpu == "" { |
| continue |
| } |
| if strings.Contains(cpu, "-") { |
| ranges := strings.Split(cpu, "-") |
| if len(ranges) != 2 { |
| return nil, fmt.Errorf("invalid cpu range: %s", cpu) |
| } |
| startRange, err := strconv.Atoi(ranges[0]) |
| if err != nil { |
| return nil, fmt.Errorf("invalid cpu start range: %w", err) |
| } |
| endRange, err := strconv.Atoi(ranges[1]) |
| if err != nil { |
| return nil, fmt.Errorf("invalid cpu end range: %w", err) |
| } |
| |
| for i := startRange; i <= endRange; i++ { |
| cpusInt = append(cpusInt, uint16(i)) |
| } |
| continue |
| } |
| |
| cpuN, err := strconv.Atoi(cpu) |
| if err != nil { |
| return nil, err |
| } |
| cpusInt = append(cpusInt, uint16(cpuN)) |
| } |
| return cpusInt, nil |
| } |