Skip to content

Commit

Permalink
Implement List{Offline,Online,Possible,Present} functions on Linux
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tklauser committed May 22, 2024
1 parent 769bdb3 commit 9ff7f79
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 59 deletions.
23 changes: 23 additions & 0 deletions numcpus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
51 changes: 51 additions & 0 deletions numcpus_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
159 changes: 127 additions & 32 deletions numcpus_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions numcpus_list_unsupported.go
Original file line number Diff line number Diff line change
@@ -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
}
57 changes: 30 additions & 27 deletions numcpus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 9ff7f79

Please sign in to comment.