diff --git a/cpuset/OWNERS b/cpuset/OWNERS new file mode 100644 index 00000000..0ec2b085 --- /dev/null +++ b/cpuset/OWNERS @@ -0,0 +1,8 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +approvers: + - dchen1107 + - derekwaynecarr + - ffromani + - klueska + - SergeyKanzhelev diff --git a/cpuset/cpuset.go b/cpuset/cpuset.go new file mode 100644 index 00000000..52912d95 --- /dev/null +++ b/cpuset/cpuset.go @@ -0,0 +1,256 @@ +/* +Copyright 2017 The Kubernetes 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. +*/ + +// Package cpuset represents a collection of CPUs in a 'set' data structure. +// +// It can be used to represent core IDs, hyper thread siblings, CPU nodes, or processor IDs. +// +// The only special thing about this package is that +// methods are provided to convert back and forth from Linux 'list' syntax. +// See http://man7.org/linux/man-pages/man7/cpuset.7.html#FORMATS for details. +// +// Future work can migrate this to use a 'set' library, and relax the dubious 'immutable' property. +// +// This package was originally developed in the 'kubernetes' repository. +package cpuset + +import ( + "bytes" + "fmt" + "reflect" + "sort" + "strconv" + "strings" +) + +// CPUSet is a thread-safe, immutable set-like data structure for CPU IDs. +type CPUSet struct { + elems map[int]struct{} +} + +// New returns a new CPUSet containing the supplied elements. +func New(cpus ...int) CPUSet { + s := CPUSet{ + elems: map[int]struct{}{}, + } + for _, c := range cpus { + s.add(c) + } + return s +} + +// add adds the supplied elements to the CPUSet. +// It is intended for internal use only, since it mutates the CPUSet. +func (s CPUSet) add(elems ...int) { + for _, elem := range elems { + s.elems[elem] = struct{}{} + } +} + +// Size returns the number of elements in this set. +func (s CPUSet) Size() int { + return len(s.elems) +} + +// IsEmpty returns true if there are zero elements in this set. +func (s CPUSet) IsEmpty() bool { + return s.Size() == 0 +} + +// Contains returns true if the supplied element is present in this set. +func (s CPUSet) Contains(cpu int) bool { + _, found := s.elems[cpu] + return found +} + +// Equals returns true if the supplied set contains exactly the same elements +// as this set (s IsSubsetOf s2 and s2 IsSubsetOf s). +func (s CPUSet) Equals(s2 CPUSet) bool { + return reflect.DeepEqual(s.elems, s2.elems) +} + +// filter returns a new CPU set that contains all of the elements from this +// set that match the supplied predicate, without mutating the source set. +func (s CPUSet) filter(predicate func(int) bool) CPUSet { + r := New() + for cpu := range s.elems { + if predicate(cpu) { + r.add(cpu) + } + } + return r +} + +// IsSubsetOf returns true if the supplied set contains all the elements +func (s CPUSet) IsSubsetOf(s2 CPUSet) bool { + result := true + for cpu := range s.elems { + if !s2.Contains(cpu) { + result = false + break + } + } + return result +} + +// Union returns a new CPU set that contains all of the elements from this +// set and all of the elements from the supplied sets, without mutating +// either source set. +func (s CPUSet) Union(s2 ...CPUSet) CPUSet { + r := New() + for cpu := range s.elems { + r.add(cpu) + } + for _, cs := range s2 { + for cpu := range cs.elems { + r.add(cpu) + } + } + return r +} + +// Intersection returns a new CPU set that contains all of the elements +// that are present in both this set and the supplied set, without mutating +// either source set. +func (s CPUSet) Intersection(s2 CPUSet) CPUSet { + return s.filter(func(cpu int) bool { return s2.Contains(cpu) }) +} + +// Difference returns a new CPU set that contains all of the elements that +// are present in this set and not the supplied set, without mutating either +// source set. +func (s CPUSet) Difference(s2 CPUSet) CPUSet { + return s.filter(func(cpu int) bool { return !s2.Contains(cpu) }) +} + +// List returns a slice of integers that contains all elements from +// this set. The list is sorted. +func (s CPUSet) List() []int { + result := s.UnsortedList() + sort.Ints(result) + return result +} + +// UnsortedList returns a slice of integers that contains all elements from +// this set. +func (s CPUSet) UnsortedList() []int { + result := make([]int, 0, len(s.elems)) + for cpu := range s.elems { + result = append(result, cpu) + } + return result +} + +// String returns a new string representation of the elements in this CPU set +// in canonical linux CPU list format. +// +// See: http://man7.org/linux/man-pages/man7/cpuset.7.html#FORMATS +func (s CPUSet) String() string { + if s.IsEmpty() { + return "" + } + + elems := s.List() + + type rng struct { + start int + end int + } + + ranges := []rng{{elems[0], elems[0]}} + + for i := 1; i < len(elems); i++ { + lastRange := &ranges[len(ranges)-1] + // if this element is adjacent to the high end of the last range + if elems[i] == lastRange.end+1 { + // then extend the last range to include this element + lastRange.end = elems[i] + continue + } + // otherwise, start a new range beginning with this element + ranges = append(ranges, rng{elems[i], elems[i]}) + } + + // construct string from ranges + var result bytes.Buffer + for _, r := range ranges { + if r.start == r.end { + result.WriteString(strconv.Itoa(r.start)) + } else { + result.WriteString(fmt.Sprintf("%d-%d", r.start, r.end)) + } + result.WriteString(",") + } + return strings.TrimRight(result.String(), ",") +} + +// Parse CPUSet constructs a new CPU set from a Linux CPU list formatted string. +// +// See: http://man7.org/linux/man-pages/man7/cpuset.7.html#FORMATS +func Parse(s string) (CPUSet, error) { + // Handle empty string. + if s == "" { + return New(), nil + } + + result := New() + + // Split CPU list string: + // "0-5,34,46-48" => ["0-5", "34", "46-48"] + ranges := strings.Split(s, ",") + + for _, r := range ranges { + boundaries := strings.SplitN(r, "-", 2) + if len(boundaries) == 1 { + // Handle ranges that consist of only one element like "34". + elem, err := strconv.Atoi(boundaries[0]) + if err != nil { + return New(), err + } + result.add(elem) + } else if len(boundaries) == 2 { + // Handle multi-element ranges like "0-5". + start, err := strconv.Atoi(boundaries[0]) + if err != nil { + return New(), err + } + end, err := strconv.Atoi(boundaries[1]) + if err != nil { + return New(), err + } + if start > end { + return New(), fmt.Errorf("invalid range %q (%d > %d)", r, start, end) + } + // start == end is acceptable (1-1 -> 1) + + // Add all elements to the result. + // e.g. "0-5", "46-48" => [0, 1, 2, 3, 4, 5, 46, 47, 48]. + for e := start; e <= end; e++ { + result.add(e) + } + } + } + return result, nil +} + +// Clone returns a copy of this CPU set. +func (s CPUSet) Clone() CPUSet { + r := New() + for elem := range s.elems { + r.add(elem) + } + return r +} diff --git a/cpuset/cpuset_test.go b/cpuset/cpuset_test.go new file mode 100644 index 00000000..275cc9e6 --- /dev/null +++ b/cpuset/cpuset_test.go @@ -0,0 +1,358 @@ +/* +Copyright 2017 The Kubernetes 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. +*/ + +package cpuset + +import ( + "reflect" + "sort" + "testing" +) + +func TestCPUSetSize(t *testing.T) { + testCases := []struct { + cpuset CPUSet + expected int + }{ + {New(), 0}, + {New(5), 1}, + {New(1, 2, 3, 4, 5), 5}, + } + + for _, c := range testCases { + actual := c.cpuset.Size() + if actual != c.expected { + t.Errorf("expected: %d, actual: %d, cpuset: [%v]", c.expected, actual, c.cpuset) + } + } +} + +func TestCPUSetIsEmpty(t *testing.T) { + testCases := []struct { + cpuset CPUSet + expected bool + }{ + {New(), true}, + {New(5), false}, + {New(1, 2, 3, 4, 5), false}, + } + + for _, c := range testCases { + actual := c.cpuset.IsEmpty() + if actual != c.expected { + t.Errorf("expected: %t, IsEmpty() returned: %t, cpuset: [%v]", c.expected, actual, c.cpuset) + } + } +} + +func TestCPUSetContains(t *testing.T) { + testCases := []struct { + cpuset CPUSet + mustContain []int + mustNotContain []int + }{ + {New(), []int{}, []int{1, 2, 3, 4, 5}}, + {New(5), []int{5}, []int{1, 2, 3, 4}}, + {New(1, 2, 4, 5), []int{1, 2, 4, 5}, []int{0, 3, 6}}, + } + + for _, c := range testCases { + for _, elem := range c.mustContain { + if !c.cpuset.Contains(elem) { + t.Errorf("expected cpuset to contain element %d: [%v]", elem, c.cpuset) + } + } + for _, elem := range c.mustNotContain { + if c.cpuset.Contains(elem) { + t.Errorf("expected cpuset not to contain element %d: [%v]", elem, c.cpuset) + } + } + } +} + +func TestCPUSetEqual(t *testing.T) { + shouldEqual := []struct { + s1 CPUSet + s2 CPUSet + }{ + {New(), New()}, + {New(5), New(5)}, + {New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5)}, + {New(5, 4, 3, 2, 1), New(1, 2, 3, 4, 5)}, + } + + shouldNotEqual := []struct { + s1 CPUSet + s2 CPUSet + }{ + {New(), New(5)}, + {New(5), New()}, + {New(), New(1, 2, 3, 4, 5)}, + {New(1, 2, 3, 4, 5), New()}, + {New(5), New(1, 2, 3, 4, 5)}, + {New(1, 2, 3, 4, 5), New(5)}, + } + + for _, c := range shouldEqual { + if !c.s1.Equals(c.s2) { + t.Errorf("expected cpusets to be equal: s1: [%v], s2: [%v]", c.s1, c.s2) + } + } + for _, c := range shouldNotEqual { + if c.s1.Equals(c.s2) { + t.Errorf("expected cpusets to not be equal: s1: [%v], s2: [%v]", c.s1, c.s2) + } + } +} + +func TestCPUSetIsSubsetOf(t *testing.T) { + shouldBeSubset := []struct { + s1 CPUSet + s2 CPUSet + }{ + // A set is a subset of itself + {New(), New()}, + {New(5), New(5)}, + {New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5)}, + + // Empty set is a subset of every set + {New(), New(5)}, + {New(), New(1, 2, 3, 4, 5)}, + + {New(5), New(1, 2, 3, 4, 5)}, + {New(1, 2, 3), New(1, 2, 3, 4, 5)}, + {New(4, 5), New(1, 2, 3, 4, 5)}, + {New(2, 3), New(1, 2, 3, 4, 5)}, + } + + shouldNotBeSubset := []struct { + s1 CPUSet + s2 CPUSet + }{ + // A set with more elements is not a subset. + {New(5), New()}, + + // Disjoint set is not a subset. + {New(6), New(5)}, + } + + for _, c := range shouldBeSubset { + if !c.s1.IsSubsetOf(c.s2) { + t.Errorf("expected s1 to be a subset of s2: s1: [%v], s2: [%v]", c.s1, c.s2) + } + } + for _, c := range shouldNotBeSubset { + if c.s1.IsSubsetOf(c.s2) { + t.Errorf("expected s1 to not be a subset of s2: s1: [%v], s2: [%v]", c.s1, c.s2) + } + } +} + +func TestCPUSetUnion(t *testing.T) { + testCases := []struct { + s1 CPUSet + others []CPUSet + expected CPUSet + }{ + {New(5), []CPUSet{}, New(5)}, + + {New(), []CPUSet{New()}, New()}, + + {New(), []CPUSet{New(5)}, New(5)}, + {New(5), []CPUSet{New()}, New(5)}, + {New(5), []CPUSet{New(5)}, New(5)}, + + {New(), []CPUSet{New(1, 2, 3, 4, 5)}, New(1, 2, 3, 4, 5)}, + {New(1, 2, 3, 4, 5), []CPUSet{New()}, New(1, 2, 3, 4, 5)}, + {New(1, 2, 3, 4, 5), []CPUSet{New(1, 2, 3, 4, 5)}, New(1, 2, 3, 4, 5)}, + + {New(5), []CPUSet{New(1, 2, 3, 4, 5)}, New(1, 2, 3, 4, 5)}, + {New(1, 2, 3, 4, 5), []CPUSet{New(5)}, New(1, 2, 3, 4, 5)}, + + {New(1, 2), []CPUSet{New(3, 4, 5)}, New(1, 2, 3, 4, 5)}, + {New(1, 2, 3), []CPUSet{New(3, 4, 5)}, New(1, 2, 3, 4, 5)}, + + {New(), []CPUSet{New(1, 2, 3, 4, 5), New(4, 5)}, New(1, 2, 3, 4, 5)}, + {New(1, 2, 3, 4, 5), []CPUSet{New(), New(4)}, New(1, 2, 3, 4, 5)}, + {New(1, 2, 3, 4, 5), []CPUSet{New(1, 2, 3, 4, 5), New(1, 5)}, New(1, 2, 3, 4, 5)}, + } + + for _, c := range testCases { + result := c.s1.Union(c.others...) + if !result.Equals(c.expected) { + t.Errorf("expected the union of s1 and s2 to be [%v] (got [%v]), others: [%v]", c.expected, result, c.others) + } + } +} + +func TestCPUSetIntersection(t *testing.T) { + testCases := []struct { + s1 CPUSet + s2 CPUSet + expected CPUSet + }{ + {New(), New(), New()}, + + {New(), New(5), New()}, + {New(5), New(), New()}, + {New(5), New(5), New(5)}, + + {New(), New(1, 2, 3, 4, 5), New()}, + {New(1, 2, 3, 4, 5), New(), New()}, + {New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5)}, + + {New(5), New(1, 2, 3, 4, 5), New(5)}, + {New(1, 2, 3, 4, 5), New(5), New(5)}, + + {New(1, 2), New(3, 4, 5), New()}, + {New(1, 2, 3), New(3, 4, 5), New(3)}, + } + + for _, c := range testCases { + result := c.s1.Intersection(c.s2) + if !result.Equals(c.expected) { + t.Errorf("expected the intersection of s1 and s2 to be [%v] (got [%v]), s1: [%v], s2: [%v]", c.expected, result, c.s1, c.s2) + } + } +} + +func TestCPUSetDifference(t *testing.T) { + testCases := []struct { + s1 CPUSet + s2 CPUSet + expected CPUSet + }{ + {New(), New(), New()}, + + {New(), New(5), New()}, + {New(5), New(), New(5)}, + {New(5), New(5), New()}, + + {New(), New(1, 2, 3, 4, 5), New()}, + {New(1, 2, 3, 4, 5), New(), New(1, 2, 3, 4, 5)}, + {New(1, 2, 3, 4, 5), New(1, 2, 3, 4, 5), New()}, + + {New(5), New(1, 2, 3, 4, 5), New()}, + {New(1, 2, 3, 4, 5), New(5), New(1, 2, 3, 4)}, + + {New(1, 2), New(3, 4, 5), New(1, 2)}, + {New(1, 2, 3), New(3, 4, 5), New(1, 2)}, + } + + for _, c := range testCases { + result := c.s1.Difference(c.s2) + if !result.Equals(c.expected) { + t.Errorf("expected the difference of s1 and s2 to be [%v] (got [%v]), s1: [%v], s2: [%v]", c.expected, result, c.s1, c.s2) + } + } +} + +func TestCPUSetList(t *testing.T) { + testCases := []struct { + set CPUSet + expected []int // must be sorted + }{ + {New(), []int{}}, + {New(5), []int{5}}, + {New(1, 2, 3, 4, 5), []int{1, 2, 3, 4, 5}}, + {New(5, 4, 3, 2, 1), []int{1, 2, 3, 4, 5}}, + } + + for _, c := range testCases { + result := c.set.List() + if !reflect.DeepEqual(result, c.expected) { + t.Errorf("unexpected List() contents. got [%v] want [%v] (set: [%v])", result, c.expected, c.set) + } + + // We cannot rely on internal storage order details for a unit test. + // The best we can do is to sort the output of 'UnsortedList'. + result = c.set.UnsortedList() + sort.Ints(result) + if !reflect.DeepEqual(result, c.expected) { + t.Errorf("unexpected UnsortedList() contents. got [%v] want [%v] (set: [%v])", result, c.expected, c.set) + } + } +} + +func TestCPUSetString(t *testing.T) { + testCases := []struct { + set CPUSet + expected string + }{ + {New(), ""}, + {New(5), "5"}, + {New(1, 2, 3, 4, 5), "1-5"}, + {New(1, 2, 3, 5, 6, 8), "1-3,5-6,8"}, + } + + for _, c := range testCases { + result := c.set.String() + if result != c.expected { + t.Errorf("expected set as string to be %s (got \"%s\"), s: [%v]", c.expected, result, c.set) + } + } +} + +func TestParse(t *testing.T) { + positiveTestCases := []struct { + cpusetString string + expected CPUSet + }{ + {"", New()}, + {"5", New(5)}, + {"1,2,3,4,5", New(1, 2, 3, 4, 5)}, + {"1-5", New(1, 2, 3, 4, 5)}, + {"1-2,3-5", New(1, 2, 3, 4, 5)}, + {"5,4,3,2,1", New(1, 2, 3, 4, 5)}, // Range ordering + {"3-6,1-5", New(1, 2, 3, 4, 5, 6)}, // Overlapping ranges + {"3-3,5-5", New(3, 5)}, // Very short ranges + } + + for _, c := range positiveTestCases { + result, err := Parse(c.cpusetString) + if err != nil { + t.Errorf("expected error not to have occurred: %v", err) + } + if !result.Equals(c.expected) { + t.Errorf("expected string \"%s\" to parse as [%v] (got [%v])", c.cpusetString, c.expected, result) + } + } + + negativeTestCases := []string{ + // Non-numeric entries + "nonnumeric", "non-numeric", "no,numbers", "0-a", "a-0", "0,a", "a,0", "1-2,a,3-5", + // Incomplete sequences + "0,", "0,,", ",3", ",,3", "0,,3", + // Incomplete ranges and/or negative numbers + "-1", "1-", "1,2-,3", "1,-2,3", "-1--2", "--1", "1--", + // Reversed ranges + "3-0", "0--3"} + for _, c := range negativeTestCases { + result, err := Parse(c) + if err == nil { + t.Errorf("expected parse failure of \"%s\", but it succeeded as \"%s\"", c, result.String()) + } + } +} + +func TestClone(t *testing.T) { + original := New(1, 2, 3, 4, 5) + clone := original.Clone() + + if !original.Equals(clone) { + t.Errorf("expected clone [%v] to equal original [%v]", clone, original) + } +}