Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add CPU micro architecture support #1628

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions pkg/analyze/host_cpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package analyzer

import (
"encoding/json"
"slices"
"strconv"
"strings"

Expand All @@ -10,6 +11,18 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/collect"
)

// microarchs holds a list of features present in each microarchitecture.
// ref: https://gitlab.com/x86-psABIs/x86-64-ABI
// ref: https://developers.redhat.com/blog/2021/01/05/building-red-hat-enterprise-linux-9-for-the-x86-64-v2-microarchitecture-level
var microarchs = map[string][]string{
"x86-64-v2": {"cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
"x86-64-v3": {"avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "lzcnt", "movbe", "xsave"},
"x86-64-v4": {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
}

// x8664BaseFeatures are the features that are present in all x86-64 microarchitectures.
var x8664BaseFeatures = []string{"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2"}

type AnalyzeHostCPU struct {
hostAnalyzer *troubleshootv1beta2.CPUAnalyze
}
Expand Down Expand Up @@ -52,7 +65,7 @@ func (a *AnalyzeHostCPU) Analyze(
return []*AnalyzeResult{&result}, nil
}

isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags)
if err != nil {
return nil, errors.Wrap(err, "failed to compare")
}
Expand All @@ -73,7 +86,7 @@ func (a *AnalyzeHostCPU) Analyze(
return []*AnalyzeResult{&result}, nil
}

isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags)
if err != nil {
return nil, errors.Wrap(err, "failed to compare")
}
Expand All @@ -94,7 +107,7 @@ func (a *AnalyzeHostCPU) Analyze(
return []*AnalyzeResult{&result}, nil
}

isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount)
isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags)
if err != nil {
return nil, errors.Wrap(err, "failed to compare")
}
Expand All @@ -112,7 +125,25 @@ func (a *AnalyzeHostCPU) Analyze(
return []*AnalyzeResult{&result}, nil
}

func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int) (res bool, err error) {
func doCompareHostCPUMicroArchitecture(microarch string, flags []string) (res bool, err error) {
specifics, ok := microarchs[microarch]
if !ok && microarch != "x86-64" {
return false, errors.Errorf("troubleshoot does not yet support microarchitecture %q", microarch)
}
expectedFlags := x8664BaseFeatures
if len(specifics) > 0 {
expectedFlags = append(expectedFlags, specifics...)
}
for _, flag := range expectedFlags {
if slices.Contains(flags, flag) {
continue
}
return false, nil
}
return true, nil
}

func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int, flags []string) (res bool, err error) {
compareLogical := false
comparePhysical := false
compareUnspecified := false
Expand All @@ -137,6 +168,11 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy
desired = parts[1]
}

// analyze if the cpu supports a specific set of features, aka as micrarchitecture.
if strings.ToLower(comparator) == "supports" {
return doCompareHostCPUMicroArchitecture(desired, flags)
}

if !compareLogical && !comparePhysical && !compareUnspecified {
return false, errors.New("unable to parse conditional")
}
Expand Down
15 changes: 14 additions & 1 deletion pkg/analyze/host_cpu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func Test_compareHostCPUConditionalToActual(t *testing.T) {
when string
logicalCount int
physicalCount int
flags []string
expected bool
}{
{
Expand Down Expand Up @@ -139,12 +140,24 @@ func Test_compareHostCPUConditionalToActual(t *testing.T) {
physicalCount: 4,
expected: true,
},
{
name: "supports x86-64-v2 microarchitecture",
when: "supports x86-64-v2",
flags: []string{""},
expected: false,
},
{
name: "supports x86-64-v2 microarchitecture",
when: "supports x86-64-v2",
flags: []string{"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2", "cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)

actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount)
actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount, test.flags)
req.NoError(err)

assert.Equal(t, test.expected, actual)
Expand Down
24 changes: 22 additions & 2 deletions pkg/collect/host_cpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
)

type CPUInfo struct {
LogicalCount int `json:"logicalCount"`
PhysicalCount int `json:"physicalCount"`
LogicalCount int `json:"logicalCount"`
PhysicalCount int `json:"physicalCount"`
Flags []string `json:"flags"`
}

const HostCPUPath = `host-collectors/system/cpu.json`
Expand Down Expand Up @@ -44,6 +45,25 @@ func (c *CollectHostCPU) Collect(progressChan chan<- interface{}) (map[string][]
}
cpuInfo.PhysicalCount = physicalCount

// XXX even though the cpu.Info() returns a slice per CPU it is way
// common to have the same flags for all CPUs. We consolidate them here
// so the output is a list of all different flags present in all CPUs.
info, err := cpu.Info()
if err != nil {
return nil, errors.Wrap(err, "failed to get cpu info")
}

seen := make(map[string]bool)
for _, infoForCPU := range info {
for _, flag := range infoForCPU.Flags {
if seen[flag] {
continue
}
seen[flag] = true
cpuInfo.Flags = append(cpuInfo.Flags, flag)
}
}

b, err := json.Marshal(cpuInfo)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal cpu info")
Expand Down
5 changes: 3 additions & 2 deletions pkg/collect/host_cpu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ func TestCollectHostCPU_Collect(t *testing.T) {
require.Contains(t, got, "host-collectors/system/cpu.json")
values := got["host-collectors/system/cpu.json"]

var m map[string]int
var m map[string]interface{}
err = json.Unmarshal(values, &m)
require.NoError(t, err)

// Check if values exist. They will be different on different machines.
assert.Equal(t, 2, len(m))
assert.Equal(t, 3, len(m))
assert.Contains(t, m, "logicalCount")
assert.Contains(t, m, "physicalCount")
assert.Contains(t, m, "flags")
}
Loading