-
Notifications
You must be signed in to change notification settings - Fork 387
sysfs: add support for ACPI 4.0 power meters #860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+509
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| // 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 ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "github.com/prometheus/procfs/internal/parsers" | ||
| ) | ||
|
|
||
| // PowerMeter represents a single ACPI 4.0 power meter device, as exposed by | ||
| // the kernel driver drivers/hwmon/acpi_power_meter.c. | ||
| // | ||
| // All optional fields use pointer types and are nil when the firmware does | ||
| // not expose the corresponding sysfs attribute. Fields documented as RW in | ||
| // the kernel (AverageMin, AverageMax, AverageInterval, Cap) can be read but | ||
| // not written through this API; callers needing to set values must write to | ||
| // sysfs directly. | ||
| // | ||
| // Typical sysfs path: /sys/bus/acpi/drivers/power_meter/ACPI000D:XX/. | ||
|
dongjiang1989 marked this conversation as resolved.
|
||
| // | ||
| // See: https://docs.kernel.org/hwmon/acpi_power_meter.html | ||
| type PowerMeter struct { | ||
| // Standard hwmon attributes (always exposed by the driver). | ||
| Average *int64 // power1_average (microWatt) | ||
| AverageMin *int64 // power1_average_min (microWatt, RW) | ||
| AverageMax *int64 // power1_average_max (microWatt, RW) | ||
| AverageInterval *int64 // power1_average_interval (millisecond, RW) | ||
| AverageIntervalMin *int64 // power1_average_interval_min (millisecond) | ||
| AverageIntervalMax *int64 // power1_average_interval_max (millisecond) | ||
| Alarm *int64 // power1_alarm (0 or 1) | ||
|
|
||
| // Optional capping attributes (present only when the platform supports | ||
| // power capping; on non-IBM systems the kernel module must be loaded | ||
| // with force_cap_on=1 on kernel >= 4.14). | ||
| Cap *int64 // power1_cap (microWatt, RW) | ||
| CapMin *int64 // power1_cap_min (microWatt) | ||
| CapMax *int64 // power1_cap_max (microWatt) | ||
| CapHyst *int64 // power1_cap_hyst (microWatt) | ||
|
|
||
| // ACPI extension attributes (firmware-dependent; may not be present on | ||
| // all platforms). | ||
| // | ||
| // Note: the kernel emits power1_accuracy as a string with a percent | ||
| // suffix (e.g. "1.50%"), so it is preserved as-is rather than parsed | ||
| // to a number. | ||
| Accuracy string // power1_accuracy (e.g. "1.50%") | ||
| IsBattery *int64 // power1_is_battery (0 or 1) | ||
| ModelNumber string // power1_model_number | ||
| SerialNumber string // power1_serial_number | ||
| OEMInfo string // power1_oem_info | ||
|
|
||
| // Metadata. | ||
| Name string // device directory name (e.g. "ACPI000D:00") | ||
| Path string // full sysfs path | ||
| Measures []string // device names from the measures/ subdirectory | ||
| } | ||
|
|
||
| // PowerMeterClass is the collection of all ACPI power meters enumerated from | ||
| // /sys/bus/acpi/drivers/power_meter/. | ||
| type PowerMeterClass []PowerMeter | ||
|
|
||
| // GetPowerMeters returns a slice of PowerMeter, one for each ACPI 4.0 power | ||
| // meter discovered via /sys/bus/acpi/drivers/power_meter/. Returns nil, nil | ||
| // when the ACPI power meter bus path does not exist (i.e. the host has no | ||
| // ACPI power meter device). | ||
| func GetPowerMeters(fs FS) (PowerMeterClass, error) { | ||
| pattern := fs.sys.Path("bus/acpi/drivers/power_meter/ACPI000D:*") | ||
|
|
||
| dirs, err := filepath.Glob(pattern) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to glob power meter devices: %w", err) | ||
| } | ||
| if len(dirs) == 0 { | ||
| return PowerMeterClass{}, os.ErrNotExist | ||
| } | ||
|
|
||
| meters := make(PowerMeterClass, 0, len(dirs)) | ||
| for _, d := range dirs { | ||
| pm, err := parsePowerMeter(d) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to parse power meter %q: %w", d, err) | ||
| } | ||
| pm.Name = filepath.Base(d) | ||
| pm.Path = d | ||
| meters = append(meters, *pm) | ||
| } | ||
| return meters, nil | ||
| } | ||
|
|
||
| // parsePowerMeter reads every attribute file inside a single power meter | ||
| // sysfs directory and returns a populated PowerMeter. | ||
| func parsePowerMeter(path string) (*PowerMeter, error) { | ||
| files, err := os.ReadDir(path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| var pm PowerMeter | ||
| for _, f := range files { | ||
| // Skip subdirectories (measures/ is handled separately) and | ||
| // non-regular files. | ||
| if !f.Type().IsRegular() { | ||
| continue | ||
| } | ||
|
|
||
| name := filepath.Join(path, f.Name()) | ||
| value, err := parsers.SysReadFile(name) | ||
| if err != nil { | ||
| // Tolerate: device not ready / attribute unsupported / | ||
| // permission denied. Matches the strategy used by | ||
| // parsePowerSupply in class_power_supply.go. | ||
| if os.IsNotExist(err) || | ||
| err.Error() == "operation not supported" || | ||
| err.Error() == "no such device" || | ||
| errors.Is(err, os.ErrInvalid) { | ||
| continue | ||
| } | ||
| return nil, fmt.Errorf("failed to read file %q: %w", name, err) | ||
| } | ||
|
|
||
| vp := parsers.NewValueParser(value) | ||
|
|
||
| switch f.Name() { | ||
| case "power1_average": | ||
| pm.Average = vp.PInt64() | ||
| case "power1_average_min": | ||
| pm.AverageMin = vp.PInt64() | ||
| case "power1_average_max": | ||
| pm.AverageMax = vp.PInt64() | ||
| case "power1_average_interval": | ||
| pm.AverageInterval = vp.PInt64() | ||
| case "power1_average_interval_min": | ||
| pm.AverageIntervalMin = vp.PInt64() | ||
| case "power1_average_interval_max": | ||
| pm.AverageIntervalMax = vp.PInt64() | ||
| case "power1_alarm": | ||
| pm.Alarm = vp.PInt64() | ||
| case "power1_cap": | ||
| pm.Cap = vp.PInt64() | ||
| case "power1_cap_min": | ||
| pm.CapMin = vp.PInt64() | ||
| case "power1_cap_max": | ||
| pm.CapMax = vp.PInt64() | ||
| case "power1_cap_hyst": | ||
| pm.CapHyst = vp.PInt64() | ||
| case "power1_accuracy": | ||
| pm.Accuracy = value | ||
| case "power1_is_battery": | ||
| pm.IsBattery = vp.PInt64() | ||
| case "power1_model_number": | ||
| pm.ModelNumber = value | ||
| case "power1_serial_number": | ||
| pm.SerialNumber = value | ||
| case "power1_oem_info": | ||
| pm.OEMInfo = value | ||
| } | ||
|
|
||
| if err := vp.Err(); err != nil { | ||
| return nil, fmt.Errorf("failed to parse %q: %w", f.Name(), err) | ||
| } | ||
| } | ||
|
|
||
| // measures/ failure is non-fatal; the directory may be empty or absent. | ||
| pm.Measures, _ = parsePowerMeterMeasures(path) | ||
| return &pm, nil | ||
| } | ||
|
|
||
| // parsePowerMeterMeasures reads the measures/ subdirectory of a power meter | ||
| // and returns the basenames of every symlink target (i.e. the device names | ||
| // this meter measures). | ||
| func parsePowerMeterMeasures(meterPath string) ([]string, error) { | ||
| measureDir := filepath.Join(meterPath, "measures") | ||
| entries, err := os.ReadDir(measureDir) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| var devices []string | ||
| for _, e := range entries { | ||
| target, err := os.Readlink(filepath.Join(measureDir, e.Name())) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| devices = append(devices, filepath.Base(target)) | ||
| } | ||
| return devices, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| // 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 ( | ||
| "os" | ||
| "path/filepath" | ||
| "sort" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestGetPowerMeters(t *testing.T) { | ||
| fs, err := NewFS(sysTestFixtures) | ||
| if err != nil { | ||
| t.Fatalf("failed to open filesystem: %v", err) | ||
| } | ||
|
|
||
| meters, err := GetPowerMeters(fs) | ||
| if err != nil { | ||
| t.Fatalf("failed to get power meters: %v", err) | ||
| } | ||
| if len(meters) != 2 { | ||
| t.Fatalf("expected 2 power meters, got %d", len(meters)) | ||
| } | ||
|
|
||
| // Sort by name for deterministic ordering (glob order varies by OS). | ||
| sort.Slice(meters, func(i, j int) bool { return meters[i].Name < meters[j].Name }) | ||
|
|
||
| // --- ACPI000D:00: full-field meter --- | ||
| m0 := meters[0] | ||
| if m0.Name != "ACPI000D:00" { | ||
| t.Fatalf("expected Name=ACPI000D:00, got %q", m0.Name) | ||
| } | ||
|
|
||
| assertPInt64(t, "ACPI000D:00.Average", m0.Average, 15000000) | ||
| assertPInt64(t, "ACPI000D:00.AverageMin", m0.AverageMin, 0) | ||
| assertPInt64(t, "ACPI000D:00.AverageMax", m0.AverageMax, 60000000) | ||
| assertPInt64(t, "ACPI000D:00.AverageInterval", m0.AverageInterval, 1000) | ||
| assertPInt64(t, "ACPI000D:00.AverageIntervalMin", m0.AverageIntervalMin, 100) | ||
| assertPInt64(t, "ACPI000D:00.AverageIntervalMax", m0.AverageIntervalMax, 10000) | ||
| assertPInt64(t, "ACPI000D:00.Alarm", m0.Alarm, 0) | ||
| assertPInt64(t, "ACPI000D:00.Cap", m0.Cap, 25000000) | ||
| assertPInt64(t, "ACPI000D:00.CapMin", m0.CapMin, 1000000) | ||
| assertPInt64(t, "ACPI000D:00.CapMax", m0.CapMax, 100000000) | ||
| assertPInt64(t, "ACPI000D:00.CapHyst", m0.CapHyst, 500000) | ||
| assertPInt64(t, "ACPI000D:00.IsBattery", m0.IsBattery, 0) | ||
|
|
||
| if m0.Accuracy != "1.50%" { | ||
| t.Errorf("expected Accuracy=%q, got %q", "1.50%", m0.Accuracy) | ||
| } | ||
| if m0.ModelNumber != "ACME PM01" { | ||
| t.Errorf("expected ModelNumber=%q, got %q", "ACME PM01", m0.ModelNumber) | ||
| } | ||
| if m0.SerialNumber != "SN12345" { | ||
| t.Errorf("expected SerialNumber=%q, got %q", "SN12345", m0.SerialNumber) | ||
| } | ||
| if m0.OEMInfo != "ACME Corp" { | ||
| t.Errorf("expected OEMInfo=%q, got %q", "ACME Corp", m0.OEMInfo) | ||
| } | ||
|
|
||
| sort.Strings(m0.Measures) | ||
| if len(m0.Measures) != 2 || m0.Measures[0] != "LNXCPU:00" || m0.Measures[1] != "LNXMEM:00" { | ||
| t.Errorf("expected Measures=[LNXCPU:00, LNXMEM:00], got %v", m0.Measures) | ||
| } | ||
|
|
||
| // --- ACPI000D:01: minimal-field meter (verify optional fields are nil) --- | ||
| m1 := meters[1] | ||
| if m1.Name != "ACPI000D:01" { | ||
| t.Fatalf("expected Name=ACPI000D:01, got %q", m1.Name) | ||
| } | ||
|
|
||
| assertPInt64(t, "ACPI000D:01.Average", m1.Average, 5000000) | ||
| assertPInt64(t, "ACPI000D:01.AverageInterval", m1.AverageInterval, 500) | ||
| assertPInt64(t, "ACPI000D:01.Alarm", m1.Alarm, 0) | ||
|
|
||
| assertNilPInt64(t, "ACPI000D:01.AverageMin", m1.AverageMin) | ||
| assertNilPInt64(t, "ACPI000D:01.AverageMax", m1.AverageMax) | ||
| assertNilPInt64(t, "ACPI000D:01.AverageIntervalMin", m1.AverageIntervalMin) | ||
| assertNilPInt64(t, "ACPI000D:01.AverageIntervalMax", m1.AverageIntervalMax) | ||
| assertNilPInt64(t, "ACPI000D:01.Cap", m1.Cap) | ||
| assertNilPInt64(t, "ACPI000D:01.CapMin", m1.CapMin) | ||
| assertNilPInt64(t, "ACPI000D:01.CapMax", m1.CapMax) | ||
| assertNilPInt64(t, "ACPI000D:01.CapHyst", m1.CapHyst) | ||
| assertNilPInt64(t, "ACPI000D:01.IsBattery", m1.IsBattery) | ||
|
|
||
| if m1.Accuracy != "" { | ||
| t.Errorf("expected Accuracy=\"\" for ACPI000D:01, got %q", m1.Accuracy) | ||
| } | ||
| if m1.ModelNumber != "" { | ||
| t.Errorf("expected ModelNumber=\"\", got %q", m1.ModelNumber) | ||
| } | ||
| if m1.SerialNumber != "" { | ||
| t.Errorf("expected SerialNumber=\"\", got %q", m1.SerialNumber) | ||
| } | ||
| if m1.OEMInfo != "" { | ||
| t.Errorf("expected OEMInfo=\"\", got %q", m1.OEMInfo) | ||
| } | ||
| if len(m1.Measures) != 0 { | ||
| t.Errorf("expected empty Measures for ACPI000D:01, got %v", m1.Measures) | ||
| } | ||
| } | ||
|
|
||
| func TestGetPowerMeters_NoDevices(t *testing.T) { | ||
| fs, err := NewFS(t.TempDir()) | ||
| if err != nil { | ||
| t.Fatalf("failed to open filesystem: %v", err) | ||
| } | ||
|
|
||
| meters, err := GetPowerMeters(fs) | ||
| if !os.IsNotExist(err) { | ||
| t.Fatalf("expected os.ErrNotExist when no devices exist, got %v", err) | ||
| } | ||
| if len(meters) != 0 { | ||
| t.Fatalf("expected empty meters, got %v", meters) | ||
| } | ||
| } | ||
|
|
||
| func TestGetPowerMeters_SingleMeter(t *testing.T) { | ||
| // Verify parsing works when only a subset of attributes is present. | ||
| tmp := t.TempDir() | ||
| meterDir := filepath.Join(tmp, "bus", "acpi", "drivers", "power_meter", "ACPI000D:00") | ||
| if err := os.MkdirAll(filepath.Join(meterDir, "measures"), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if err := os.WriteFile(filepath.Join(meterDir, "power1_average"), []byte("1000"), 0o644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if err := os.WriteFile(filepath.Join(meterDir, "power1_alarm"), []byte("1"), 0o644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| fs, err := NewFS(tmp) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| meters, err := GetPowerMeters(fs) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if len(meters) != 1 { | ||
| t.Fatalf("expected 1 meter, got %d", len(meters)) | ||
| } | ||
| if meters[0].Name != "ACPI000D:00" { | ||
| t.Errorf("expected Name=ACPI000D:00, got %q", meters[0].Name) | ||
| } | ||
| assertPInt64(t, "Average", meters[0].Average, 1000) | ||
| assertPInt64(t, "Alarm", meters[0].Alarm, 1) | ||
| assertNilPInt64(t, "Cap", meters[0].Cap) | ||
| } | ||
|
|
||
| // assertPInt64 checks that a *int64 field is non-nil and holds the expected value. | ||
| func assertPInt64(t *testing.T, field string, got *int64, want int64) { | ||
| t.Helper() | ||
| if got == nil { | ||
| t.Errorf("%s: expected %d, got nil", field, want) | ||
| return | ||
| } | ||
| if *got != want { | ||
| t.Errorf("%s: expected %d, got %d", field, want, *got) | ||
| } | ||
| } | ||
|
|
||
| // assertNilPInt64 checks that a *int64 field is nil. | ||
| func assertNilPInt64(t *testing.T, field string, got *int64) { | ||
| t.Helper() | ||
| if got != nil { | ||
| t.Errorf("%s: expected nil, got %d", field, *got) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.