From 9ff7f7923f3087bc93960658e0d68ad187e46dcd Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Wed, 22 May 2024 12:50:12 +0200 Subject: [PATCH] Implement List{Offline,Online,Possible,Present} functions on Linux These return a list of offline, online, possible or present CPUs rather than just the number of CPUs. These are implemented on Linux only for now as other platforms don't seem to expose that information in detail. --- numcpus.go | 23 ++++++ numcpus_linux.go | 51 ++++++++++++ numcpus_linux_test.go | 159 ++++++++++++++++++++++++++++-------- numcpus_list_unsupported.go | 33 ++++++++ numcpus_test.go | 57 +++++++------ 5 files changed, 264 insertions(+), 59 deletions(-) create mode 100644 numcpus_list_unsupported.go diff --git a/numcpus.go b/numcpus.go index af59983..de206f0 100644 --- a/numcpus.go +++ b/numcpus.go @@ -73,3 +73,26 @@ func GetPossible() (int, error) { func GetPresent() (int, error) { return getPresent() } + +// ListOffline returns the list of offline CPUs. See [GetOffline] for details on +// when a CPU is considered offline. +func ListOffline() ([]int, error) { + return listOffline() +} + +// ListOnline returns the list of CPUs that are online and being scheduled. +func ListOnline() ([]int, error) { + return listOnline() +} + +// ListPossible returns the list of possible CPUs. See [GetPossible] for +// details on when a CPU is considered possible. +func ListPossible() ([]int, error) { + return listPossible() +} + +// ListPresent returns the list of present CPUs. See [GetPresent] for +// details on when a CPU is considered present. +func ListPresent() ([]int, error) { + return listPresent() +} diff --git a/numcpus_linux.go b/numcpus_linux.go index af02a4f..c4b225d 100644 --- a/numcpus_linux.go +++ b/numcpus_linux.go @@ -83,6 +83,41 @@ func countCPURange(cpus string) (int, error) { return n, nil } +func listCPURange(cpus string) ([]int, error) { + // See comment in countCPURange. + if cpus == "" { + return []int{}, nil + } + + list := []int{} + for _, cpuRange := range strings.Split(cpus, ",") { + if cpuRange == "" { + return nil, fmt.Errorf("empty CPU range in CPU string %q", cpus) + } + from, to, found := strings.Cut(cpuRange, "-") + first, err := strconv.ParseUint(from, 10, 32) + if err != nil { + return nil, err + } + var last uint64 + if found { + last, err = strconv.ParseUint(to, 10, 32) + if err != nil { + return nil, err + } + } else { + last = first + } + if last < first { + return nil, fmt.Errorf("last CPU in range (%d) less than first (%d)", last, first) + } + for cpu := int(first); cpu <= int(last); cpu++ { + list = append(list, cpu) + } + } + return list, nil +} + func getConfigured() (int, error) { d, err := os.Open(sysfsCPUBasePath) if err != nil { @@ -135,3 +170,19 @@ func getPossible() (int, error) { func getPresent() (int, error) { return readCPURangeWith(present, countCPURange) } + +func listOffline() ([]int, error) { + return readCPURangeWith(offline, listCPURange) +} + +func listOnline() ([]int, error) { + return readCPURangeWith(online, listCPURange) +} + +func listPossible() ([]int, error) { + return readCPURangeWith(possible, listCPURange) +} + +func listPresent() ([]int, error) { + return readCPURangeWith(present, listCPURange) +} diff --git a/numcpus_linux_test.go b/numcpus_linux_test.go index e9624fa..f7cc9cd 100644 --- a/numcpus_linux_test.go +++ b/numcpus_linux_test.go @@ -14,50 +14,145 @@ package numcpus -import "testing" +import ( + "reflect" + "testing" +) -func TestCountCPURange(t *testing.T) { +func TestCPURange(t *testing.T) { testCases := []struct { - str string - n int - wantErr bool + str string + wantCount int + wantList []int + wantErr bool }{ - {str: "", n: 0}, - {str: "0", n: 1}, - {str: "0-1", n: 2}, - {str: "1-1", n: 1}, - {str: "0-7", n: 8}, - {str: "1-7", n: 7}, - {str: "1-15", n: 15}, - {str: "0-3,7", n: 5}, - {str: "0,2-4", n: 4}, - {str: "0,2-4,7", n: 5}, - {str: "0,2-4,7-15", n: 13}, - {str: "0,2-4,6,8-10", n: 8}, - {str: ",", wantErr: true}, - {str: "invalid", n: 0, wantErr: true}, - {str: "-", wantErr: true}, - {str: ",", wantErr: true}, - {str: ",1", wantErr: true}, - {str: "0,", wantErr: true}, - {str: "0-", wantErr: true}, - {str: "0,2-", wantErr: true}, - {str: "0-,1", wantErr: true}, - {str: "0,-3,5", wantErr: true}, - {str: "42-0", wantErr: true}, - {str: "0,5-3", wantErr: true}, + { + str: "", + wantCount: 0, + wantList: []int{}, + }, + { + str: "0", + wantCount: 1, + wantList: []int{0}, + }, + { + str: "0-1", + wantCount: 2, + wantList: []int{0, 1}, + }, + { + str: "1-1", + wantCount: 1, + wantList: []int{1}, + }, + { + str: "0-7", + wantCount: 8, + wantList: []int{0, 1, 2, 3, 4, 5, 6, 7}, + }, + { + str: "1-7", + wantCount: 7, + wantList: []int{1, 2, 3, 4, 5, 6, 7}, + }, + { + str: "1-15", + wantCount: 15, + wantList: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + }, + { + str: "0-3,7", + wantCount: 5, + wantList: []int{0, 1, 2, 3, 7}, + }, + { + str: "0,2-4", + wantCount: 4, + wantList: []int{0, 2, 3, 4}, + }, + { + str: "0,2-4,7", + wantCount: 5, + wantList: []int{0, 2, 3, 4, 7}, + }, + { + str: "0,2-4,7-15", + wantCount: 13, + wantList: []int{0, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + }, + { + str: "0,2-4,6,8-10,31", + wantCount: 9, + wantList: []int{0, 2, 3, 4, 6, 8, 9, 10, 31}, + }, + { + str: "invalid", + wantErr: true, + }, + { + str: "-", + wantErr: true, + }, + { + str: ",", + wantErr: true, + }, + { + str: ",1", + wantErr: true, + }, + { + str: "0,", + wantErr: true, + }, + { + str: "0-", + wantErr: true, + }, + { + str: "-15", + wantErr: true, + }, + { + str: "0-,1", + wantErr: true, + }, + { + str: "0,-3,5", + wantErr: true, + }, + { + str: "42-0", + wantErr: true, + }, + { + str: "0,5-3", + wantErr: true, + }, } for _, tc := range testCases { - n, err := countCPURange(tc.str) + count, err := countCPURange(tc.str) if !tc.wantErr && err != nil { t.Errorf("countCPURange(%q) = %v, expected no error", tc.str, err) } else if tc.wantErr && err == nil { t.Errorf("countCPURange(%q) expected error", tc.str) } - if n != tc.n { - t.Errorf("countCPURange(%q) = %d, expected %d", tc.str, n, tc.n) + if count != tc.wantCount { + t.Errorf("countCPURange(%q) = %d, expected %d", tc.str, count, tc.wantCount) + } + + list, err := listCPURange(tc.str) + if !tc.wantErr && err != nil { + t.Errorf("listCPURange(%q) = %v, expected no error", tc.str, err) + } else if tc.wantErr && err == nil { + t.Errorf("listCPURange(%q) expected error", tc.str) + } + + if !reflect.DeepEqual(list, tc.wantList) { + t.Errorf("listCPURange(%q) = %d, expected %d", tc.str, list, tc.wantList) } } } diff --git a/numcpus_list_unsupported.go b/numcpus_list_unsupported.go new file mode 100644 index 0000000..af4efea --- /dev/null +++ b/numcpus_list_unsupported.go @@ -0,0 +1,33 @@ +// Copyright 2024 Tobias Klauser +// +// 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 numcpus + +func listOffline() ([]int, error) { + return nil, ErrNotSupported +} + +func listOnline() ([]int, error) { + return nil, ErrNotSupported +} + +func listPossible() ([]int, error) { + return nil, ErrNotSupported +} + +func listPresent() ([]int, error) { + return nil, ErrNotSupported +} diff --git a/numcpus_test.go b/numcpus_test.go index 2b68d5a..236a836 100644 --- a/numcpus_test.go +++ b/numcpus_test.go @@ -82,44 +82,47 @@ func TestGetKernelMax(t *testing.T) { t.Logf("KernelMax = %v", n) } -func TestGetOffline(t *testing.T) { - n, err := numcpus.GetOffline() +func testNumAndList(t *testing.T, name string, get func() (int, error), list func() ([]int, error)) int { + t.Helper() + + n, err := get() if errors.Is(err, numcpus.ErrNotSupported) { - t.Skipf("GetOffline not supported on %s", runtime.GOOS) + t.Skipf("Get%s not supported on %s", name, runtime.GOOS) } else if err != nil { - t.Fatalf("GetOffline: %v", err) + t.Fatalf("Get%s: %v", name, err) } - t.Logf("Offline = %v", n) -} + t.Logf("%s = %v", name, n) -func TestGetOnline(t *testing.T) { - n, err := numcpus.GetOnline() + l, err := list() if errors.Is(err, numcpus.ErrNotSupported) { - t.Skipf("GetOnline not supported on %s", runtime.GOOS) + t.Skipf("List%s not supported on %s", name, runtime.GOOS) } else if err != nil { - t.Fatalf("GetOnline: %v", err) + t.Fatalf("List%s: %v", name, err) + } + t.Logf("List%s = %v", name, l) + + if len(l) != n { + t.Errorf("number of online CPUs in list %v doesn't match expected number of CPUs %d", l, n) } - t.Logf("Online = %v", n) + + return n +} + +func TestOffline(t *testing.T) { + testNumAndList(t, "Offline", numcpus.GetOffline, numcpus.ListOffline) +} + +func TestOnline(t *testing.T) { + n := testNumAndList(t, "Online", numcpus.GetOnline, numcpus.ListOnline) testGetconf(t, n, "GetOnline", confName("_NPROCESSORS_ONLN")) + } -func TestGetPossible(t *testing.T) { - n, err := numcpus.GetPossible() - if errors.Is(err, numcpus.ErrNotSupported) { - t.Skipf("GetPossible not supported on %s", runtime.GOOS) - } else if err != nil { - t.Fatalf("GetPossible: %v", err) - } - t.Logf("Possible = %v", n) +func TestPossible(t *testing.T) { + testNumAndList(t, "Possible", numcpus.GetPossible, numcpus.ListPossible) } -func TestGetPresent(t *testing.T) { - n, err := numcpus.GetPresent() - if errors.Is(err, numcpus.ErrNotSupported) { - t.Skipf("GetPresent not supported on %s", runtime.GOOS) - } else if err != nil { - t.Fatalf("GetPresent: %v", err) - } - t.Logf("Present = %v", n) +func TestPresent(t *testing.T) { + testNumAndList(t, "Present", numcpus.GetPresent, numcpus.ListPresent) }