mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 23:56:57 +02:00
172 lines
5.1 KiB
Go
172 lines
5.1 KiB
Go
package cpupick
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
// pairTopo builds a topology where consecutive candidate pairs are SMT
|
|
// siblings: (cpus[0],cpus[1]) share a core, (cpus[2],cpus[3]) the next, ...
|
|
// All CPUs land on node 0.
|
|
func pairTopo(cpus []int) topology {
|
|
t := topology{
|
|
nodeOf: make(map[int]int, len(cpus)),
|
|
coreOf: make(map[int]int, len(cpus)),
|
|
zeroCore: -1,
|
|
}
|
|
for i, c := range cpus {
|
|
t.nodeOf[c] = 0
|
|
t.coreOf[c] = i / 2
|
|
if c == 0 {
|
|
t.zeroCore = i / 2
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func TestArrangeDemotesZeroForEveryKey(t *testing.T) {
|
|
candidates := []int{0, 1, 2, 3, 4, 5, 6, 7}
|
|
for key := range uint64(64) {
|
|
got := arrange(candidates, flatTopology(candidates), 4, splitmix64(key))
|
|
if len(got) != len(candidates) {
|
|
t.Fatalf("key %d: len=%d want %d", key, len(got), len(candidates))
|
|
}
|
|
if got[0] == 0 {
|
|
t.Errorf("key %d: CPU 0 at the front: %v", key, got)
|
|
}
|
|
if got[len(got)-1] != 0 {
|
|
t.Errorf("key %d: CPU 0 not demoted to last: %v", key, got)
|
|
}
|
|
sorted := slices.Clone(got)
|
|
slices.Sort(sorted)
|
|
if !slices.Equal(sorted, candidates) {
|
|
t.Errorf("key %d: not a permutation: %v", key, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestArrangeDemotesZeroSiblings(t *testing.T) {
|
|
// Pairs (0,1),(2,3),(4,5),(6,7): CPU 0's core — 0 and its sibling 1 —
|
|
// must tail the list, sibling ahead of 0 itself.
|
|
candidates := []int{0, 1, 2, 3, 4, 5, 6, 7}
|
|
for key := range uint64(64) {
|
|
got := arrange(candidates, pairTopo(candidates), 2, splitmix64(key))
|
|
n := len(got)
|
|
if got[n-1] != 0 || got[n-2] != 1 {
|
|
t.Fatalf("key %d: tail = %v, want [... 1 0]", key, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestArrangeZeroSiblingWithoutZero(t *testing.T) {
|
|
// CPU 0 excluded (cpuset) but its sibling 1 remains: the sibling still
|
|
// tails the list when the topology knows which core CPU 0 lives on.
|
|
candidates := []int{1, 2, 3, 4, 5}
|
|
topo := pairTopo([]int{0, 1, 2, 3, 4, 5})
|
|
got := arrange(candidates, topo, 2, splitmix64(7))
|
|
if got[len(got)-1] != 1 {
|
|
t.Errorf("CPU 0's sibling not demoted: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestArrangeRotatesByKey(t *testing.T) {
|
|
candidates := []int{1, 2, 3, 4, 5, 6, 7, 8}
|
|
seen := map[int]bool{}
|
|
for key := range uint64(64) {
|
|
seen[arrange(candidates, flatTopology(candidates), 4, splitmix64(key))[0]] = true
|
|
}
|
|
// 64 hashed keys over 8 slots must hit more than one starting CPU, or
|
|
// co-located instances would all stack again.
|
|
if len(seen) < 2 {
|
|
t.Errorf("rotation never varied across keys: %v", seen)
|
|
}
|
|
}
|
|
|
|
func TestArrangeStableForSameKey(t *testing.T) {
|
|
candidates := []int{0, 2, 4, 6}
|
|
topo := flatTopology(candidates)
|
|
a := arrange(candidates, topo, 2, splitmix64(4242))
|
|
b := arrange(candidates, topo, 2, splitmix64(4242))
|
|
if !slices.Equal(a, b) {
|
|
t.Errorf("same key ordered differently: %v vs %v", a, b)
|
|
}
|
|
}
|
|
|
|
func TestArrangeZeroOnly(t *testing.T) {
|
|
if got := arrange([]int{0}, flatTopology([]int{0}), 1, splitmix64(7)); !slices.Equal(got, []int{0}) {
|
|
t.Errorf("sole CPU 0 must survive: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestArrangeSMTSiblingsLast(t *testing.T) {
|
|
// Pairs (1,2),(3,4),(5,6),(7,8): the first four picks must cover four
|
|
// distinct physical cores before any sibling repeats.
|
|
candidates := []int{1, 2, 3, 4, 5, 6, 7, 8}
|
|
topo := pairTopo(candidates)
|
|
for key := range uint64(16) {
|
|
got := arrange(candidates, topo, 4, splitmix64(key))
|
|
seen := map[int]bool{}
|
|
for _, c := range got[:4] {
|
|
g := topo.coreOf[c]
|
|
if seen[g] {
|
|
t.Fatalf("key %d: sibling before all cores covered: %v", key, got)
|
|
}
|
|
seen[g] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestArrangeNUMAConfinesToOneNode(t *testing.T) {
|
|
// Two nodes of four; both fit routines=3, so the result must sit
|
|
// entirely inside one of them, and the hash must pick both across keys.
|
|
candidates := []int{1, 2, 3, 4, 10, 11, 12, 13}
|
|
topo := flatTopology(candidates)
|
|
for _, c := range []int{10, 11, 12, 13} {
|
|
topo.nodeOf[c] = 1
|
|
}
|
|
nodesSeen := map[int]bool{}
|
|
for key := range uint64(32) {
|
|
got := arrange(candidates, topo, 3, splitmix64(key))
|
|
if len(got) != 4 {
|
|
t.Fatalf("key %d: not confined to one node: %v", key, got)
|
|
}
|
|
n := topo.nodeOf[got[0]]
|
|
for _, c := range got {
|
|
if topo.nodeOf[c] != n {
|
|
t.Fatalf("key %d: spans nodes: %v", key, got)
|
|
}
|
|
}
|
|
nodesSeen[n] = true
|
|
}
|
|
if len(nodesSeen) != 2 {
|
|
t.Errorf("hash never spread instances across nodes: %v", nodesSeen)
|
|
}
|
|
}
|
|
|
|
func TestArrangeNUMASpansWhenNoNodeFits(t *testing.T) {
|
|
candidates := []int{1, 2, 3, 4, 10, 11, 12, 13}
|
|
topo := flatTopology(candidates)
|
|
for _, c := range []int{10, 11, 12, 13} {
|
|
topo.nodeOf[c] = 1
|
|
}
|
|
got := arrange(candidates, topo, 6, splitmix64(1))
|
|
if len(got) != len(candidates) {
|
|
t.Errorf("undersized nodes must span, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestPickCandidates(t *testing.T) {
|
|
allowed := []int{0, 1, 2, 3, 4, 5, 6, 7}
|
|
perf := []int{4, 5}
|
|
|
|
// Enough perf cores for every routine: only they are used.
|
|
if got := pickCandidates(allowed, perf, 2); !slices.Equal(got, perf) {
|
|
t.Errorf("perf filter not applied: %v", got)
|
|
}
|
|
// Perf filter too small for the routine count: discarded, everyone
|
|
// gets their own core from the full allowed set.
|
|
if got := pickCandidates(allowed, perf, 4); !slices.Equal(got, allowed) {
|
|
t.Errorf("undersized perf filter not discarded: %v", got)
|
|
}
|
|
}
|