kubernetes/pkg/kubelet/winstats/cpu_topology_test.go

//go:build windows
// +build windows

/*
Copyright 2024 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 winstats

import (
	cadvisorapi "github.com/google/cadvisor/info/v1"
	"github.com/stretchr/testify/assert"
	"testing"
	"unsafe"
)

func TestGROUP_AFFINITY_Processors(t *testing.T) {
	tests := []struct {
		name  string
		Mask  uint64
		Group uint16
		want  []int
	}{
		{
			name:  "empty",
			Mask:  0,
			Group: 0,
			want:  []int{},
		},
		{
			name:  "empty group 2",
			Mask:  0,
			Group: 1,
			want:  []int{},
		},
		{
			name:  "cpu 1 Group 0",
			Mask:  1,
			Group: 0,
			want:  []int{0},
		},
		{
			name:  "cpu 64 Group 0",
			Mask:  1 << 63,
			Group: 0,
			want:  []int{63},
		},
		{
			name:  "cpu 128 Group 1",
			Mask:  1 << 63,
			Group: 1,
			want:  []int{127},
		},
		{
			name:  "cpu 128 (Group 1)",
			Mask:  1 << 63,
			Group: 1,
			want:  []int{127},
		},
		{
			name:  "Mask 1 Group 2",
			Mask:  1,
			Group: 2,
			want:  []int{128},
		},
		{
			name:  "64 cpus group 0",
			Mask:  0xffffffffffffffff,
			Group: 0,
			want:  makeRange(0, 63),
		},
		{
			name:  "64 cpus group 1",
			Mask:  0xffffffffffffffff,
			Group: 1,
			want:  makeRange(64, 127),
		},
		{
			name:  "64 cpus group 1",
			Mask:  0xffffffffffffffff,
			Group: 1,
			want:  makeRange(64, 127),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := GroupAffinity{
				Mask:  tt.Mask,
				Group: tt.Group,
			}
			assert.Equalf(t, tt.want, a.Processors(), "Processors()")
		})
	}
}

// https://stackoverflow.com/a/39868255/697126
func makeRange(min, max int) []int {
	a := make([]int, max-min+1)
	for i := range a {
		a[i] = min + i
	}
	return a
}

func TestCpusToGroupAffinity(t *testing.T) {
	tests := []struct {
		name string
		cpus []int
		want map[int]*GroupAffinity
	}{
		{
			name: "empty",
			want: map[int]*GroupAffinity{},
		},
		{
			name: "single cpu group 0",
			cpus: []int{0},
			want: map[int]*GroupAffinity{
				0: {
					Mask:  1,
					Group: 0,
				},
			},
		},
		{
			name: "single cpu group 0",
			cpus: []int{63},
			want: map[int]*GroupAffinity{
				0: {
					Mask:  1 << 63,
					Group: 0,
				},
			},
		},
		{
			name: "single cpu group 1",
			cpus: []int{64},
			want: map[int]*GroupAffinity{
				1: {
					Mask:  1,
					Group: 1,
				},
			},
		},
		{
			name: "multiple cpus same group",
			cpus: []int{0, 1, 2},
			want: map[int]*GroupAffinity{
				0: {
					Mask:  1 | 2 | 4, // Binary OR to combine the masks
					Group: 0,
				},
			},
		},
		{
			name: "multiple cpus different groups",
			cpus: []int{0, 64},
			want: map[int]*GroupAffinity{
				0: {
					Mask:  1,
					Group: 0,
				},
				1: {
					Mask:  1,
					Group: 1,
				},
			},
		},
		{
			name: "multiple cpus different groups",
			cpus: []int{0, 1, 2, 64, 65, 66},
			want: map[int]*GroupAffinity{
				0: {
					Mask:  1 | 2 | 4,
					Group: 0,
				},
				1: {
					Mask:  1 | 2 | 4,
					Group: 1,
				},
			},
		},
		{
			name: "64 cpus group 0",
			cpus: makeRange(0, 63),
			want: map[int]*GroupAffinity{
				0: {
					Mask:  0xffffffffffffffff, // All 64 bits set
					Group: 0,
				},
			},
		},
		{
			name: "64 cpus group 1",
			cpus: makeRange(64, 127),
			want: map[int]*GroupAffinity{
				1: {
					Mask:  0xffffffffffffffff, // All 64 bits set
					Group: 1,
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equalf(t, tt.want, CpusToGroupAffinity(tt.cpus), "CpusToGroupAffinity(%v)", tt.cpus)
		})
	}
}

func Test_convertWinApiToCadvisorApi(t *testing.T) {
	tests := []struct {
		name                 string
		buffer               []byte
		expectedNumOfCores   int
		expectedNumOfSockets int
		expectedNodes        []cadvisorapi.Node
		wantErr              bool
	}{
		{
			name:                 "empty",
			buffer:               []byte{},
			expectedNumOfCores:   0,
			expectedNumOfSockets: 0,
			expectedNodes:        []cadvisorapi.Node{},
			wantErr:              false,
		},
		{
			name:                 "single core",
			buffer:               createProcessorRelationships([]int{0}),
			expectedNumOfCores:   1,
			expectedNumOfSockets: 1,
			expectedNodes: []cadvisorapi.Node{
				{
					Id: 0,
					Cores: []cadvisorapi.Core{
						{
							Id:      1,
							Threads: []int{0},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name:                 "single core, multiple cpus",
			buffer:               createProcessorRelationships([]int{0, 1, 2}),
			expectedNumOfCores:   1,
			expectedNumOfSockets: 1,
			expectedNodes: []cadvisorapi.Node{
				{
					Id: 0,
					Cores: []cadvisorapi.Core{
						{
							Id:      1,
							Threads: []int{0, 1, 2},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name:                 "single core, multiple groups",
			buffer:               createProcessorRelationships([]int{0, 64}),
			expectedNumOfCores:   1,
			expectedNumOfSockets: 1,
			expectedNodes: []cadvisorapi.Node{
				{
					Id: 0,
					Cores: []cadvisorapi.Core{
						{
							Id:      1,
							Threads: []int{0, 64},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name:                 "buffer to small",
			buffer:               createProcessorRelationships([]int{0, 64})[:48],
			expectedNumOfCores:   1,
			expectedNumOfSockets: 1,
			expectedNodes: []cadvisorapi.Node{
				{
					Id: 0,
					Cores: []cadvisorapi.Core{
						{
							Id:      1,
							Threads: []int{0, 64},
						},
					},
				},
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			numOfCores, numOfSockets, nodes, err := convertWinApiToCadvisorApi(tt.buffer)
			if tt.wantErr {
				assert.Error(t, err)
				return
			}
			assert.Equalf(t, tt.expectedNumOfCores, numOfCores, "num of cores")
			assert.Equalf(t, tt.expectedNumOfSockets, numOfSockets, "num of sockets")
			for node := range nodes {
				assert.Equalf(t, tt.expectedNodes[node].Id, nodes[node].Id, "node id")
				for core := range nodes[node].Cores {
					assert.Equalf(t, tt.expectedNodes[node].Cores[core].Id, nodes[node].Cores[core].Id, "core id")
					assert.Equalf(t, len(tt.expectedNodes[node].Cores[core].Threads), len(nodes[node].Cores[core].Threads), "num of threads")
					for _, thread := range nodes[node].Cores[core].Threads {
						assert.Truef(t, containsThread(tt.expectedNodes[node].Cores[core].Threads, thread), "thread %d", thread)
					}
				}
			}
		})
	}
}

func containsThread(threads []int, thread int) bool {
	for _, t := range threads {
		if t == thread {
			return true
		}
	}
	return false
}

func genBuffer(infos ...systemLogicalProcessorInformationEx) []byte {
	var buffer []byte
	for _, info := range infos {
		buffer = append(buffer, structToBytes(info)...)
	}
	return buffer
}

func createProcessorRelationships(cpus []int) []byte {
	groups := CpusToGroupAffinity(cpus)
	grouplen := len(groups)
	groupAffinities := make([]GroupAffinity, 0, grouplen)
	for _, group := range groups {
		groupAffinities = append(groupAffinities, *group)
	}
	return genBuffer(systemLogicalProcessorInformationEx{
		Relationship: uint32(relationProcessorCore),
		Size:         uint32(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_SIZE + PROCESSOR_RELATIONSHIP_SIZE + (GROUP_AFFINITY_SIZE * grouplen)),
		data: processorRelationship{
			Flags:           0,
			EfficiencyClass: 0,
			Reserved:        [20]byte{},
			GroupCount:      uint16(grouplen),
			GroupMasks:      groupAffinities,
		},
	}, systemLogicalProcessorInformationEx{
		Relationship: uint32(relationNumaNode),
		Size:         uint32(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_SIZE + NUMA_NODE_RELATIONSHIP_SIZE + (GROUP_AFFINITY_SIZE * grouplen)),
		data: numaNodeRelationship{
			NodeNumber: 0,
			Reserved:   [18]byte{},
			GroupCount: uint16(grouplen),
			GroupMasks: groupAffinities,
		}}, systemLogicalProcessorInformationEx{
		Relationship: uint32(relationProcessorPackage),
		Size:         uint32(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_SIZE + PROCESSOR_RELATIONSHIP_SIZE + (GROUP_AFFINITY_SIZE * grouplen)),
		data: processorRelationship{
			Flags:           0,
			EfficiencyClass: 0,
			Reserved:        [20]byte{},
			GroupCount:      uint16(grouplen),
			GroupMasks:      groupAffinities,
		},
	})
}

const SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_SIZE = 8
const PROCESSOR_RELATIONSHIP_SIZE = 24
const NUMA_NODE_RELATIONSHIP_SIZE = 24
const GROUP_AFFINITY_SIZE = int(unsafe.Sizeof(GroupAffinity{})) // this one is known at compile time

func structToBytes(info systemLogicalProcessorInformationEx) []byte {
	var pri []byte = (*(*[SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_SIZE]byte)(unsafe.Pointer(&info)))[:SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_SIZE]

	switch info.data.(type) {
	case processorRelationship:
		rel := info.data.(processorRelationship)
		var prBytes []byte = (*(*[PROCESSOR_RELATIONSHIP_SIZE]byte)(unsafe.Pointer(&rel)))[:PROCESSOR_RELATIONSHIP_SIZE]
		pri = append(pri, prBytes...)

		groupAffinities := rel.GroupMasks.([]GroupAffinity)

		for _, groupAffinity := range groupAffinities {
			var groupByte []byte = (*(*[GROUP_AFFINITY_SIZE]byte)(unsafe.Pointer(&groupAffinity)))[:]
			pri = append(pri, groupByte...)
		}
	case numaNodeRelationship:
		numa := info.data.(numaNodeRelationship)
		var nameBytes []byte = (*(*[NUMA_NODE_RELATIONSHIP_SIZE]byte)(unsafe.Pointer(&numa)))[:NUMA_NODE_RELATIONSHIP_SIZE]
		pri = append(pri, nameBytes...)

		groupAffinities := numa.GroupMasks.([]GroupAffinity)

		for _, groupAffinity := range groupAffinities {
			var groupByte []byte = (*(*[GROUP_AFFINITY_SIZE]byte)(unsafe.Pointer(&groupAffinity)))[:]
			pri = append(pri, groupByte...)
		}
	}

	return pri
}