diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index ad946803..1050b332 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -453,11 +453,25 @@ func hyperthreadingFromOutput(outputs map[string]script.ScriptOutput) string { sockets := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Socket\(s\):\s*(.+)$`) coresPerSocket := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Core\(s\) per socket:\s*(.+)$`) cpus := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^CPU\(.*:\s*(.+?)$`) + onlineCpus := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^On-line CPU\(s\) list:\s*(.+)$`) + threadsPerCore := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Thread\(s\) per core:\s*(.+)$`) + numCPUs, err := strconv.Atoi(cpus) // logical CPUs if err != nil { slog.Error("error parsing cpus from lscpu") return "" } + onlineCpusList, err := util.SelectiveIntRangeToIntList(onlineCpus) // logical online CPUs + numOnlineCpus := len(onlineCpusList) + if err != nil { + slog.Error("error parsing online cpus from lscpu") + numOnlineCpus = 0 // set to 0 to indicate parsing failed, will use numCPUs instead + } + numThreadsPerCore, err := strconv.Atoi(threadsPerCore) // logical threads per core + if err != nil { + slog.Error("error parsing threads per core from lscpu") + numThreadsPerCore = 0 + } numSockets, err := strconv.Atoi(sockets) if err != nil { slog.Error("error parsing sockets from lscpu") @@ -472,9 +486,22 @@ func hyperthreadingFromOutput(outputs map[string]script.ScriptOutput) string { if err != nil { return "" } + if numOnlineCpus > 0 && numOnlineCpus < numCPUs { + // if online CPUs list is available, use it to determine the number of CPUs + // supersedes lscpu output of numCPUs which counts CPUs on the system, not online CPUs + numCPUs = numOnlineCpus + } if cpu.LogicalThreadCount < 2 { return "N/A" + } else if numThreadsPerCore == 1 { + // if threads per core is 1, hyperthreading is disabled + return "Disabled" + } else if numThreadsPerCore >= 2 { + // if threads per core is greater than or equal to 2, hyperthreading is enabled + return "Enabled" } else if numCPUs > numCoresPerSocket*numSockets { + // if the threads per core attribute is not available, we can still check if hyperthreading is enabled + // by checking if the number of logical CPUs is greater than the number of physical cores return "Enabled" } else { return "Disabled" diff --git a/internal/report/table_helpers_test.go b/internal/report/table_helpers_test.go index f7648851..57563db3 100644 --- a/internal/report/table_helpers_test.go +++ b/internal/report/table_helpers_test.go @@ -4,10 +4,269 @@ package report // SPDX-License-Identifier: BSD-3-Clause import ( + "perfspect/internal/script" "reflect" "testing" ) +func TestHyperthreadingFromOutput(t *testing.T) { + tests := []struct { + name string + lscpuOutput string + wantResult string + }{ + { + name: "Hyperthreading enabled - 2 threads per core", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 16 +Thread(s) per core: 2 +On-line CPU(s) list: 0-15 +`, + wantResult: "Enabled", + }, + { + name: "Hyperthreading disabled - 1 thread per core", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 8 +Thread(s) per core: 1 +On-line CPU(s) list: 0-7 +`, + wantResult: "Disabled", + }, + { + name: "Hyperthreading enabled - detected by CPU count vs core count", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 2 +Core(s) per socket: 8 +CPU(s): 32 +On-line CPU(s) list: 0-31 +`, + wantResult: "Enabled", + }, + { + name: "Hyperthreading disabled - CPU count equals core count", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 2 +Core(s) per socket: 8 +CPU(s): 16 +On-line CPU(s) list: 0-15 +`, + wantResult: "Disabled", + }, + { + name: "Online CPUs less than total CPUs - use online count", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 16 +Thread(s) per core: 2 +On-line CPU(s) list: 0-7 +`, + wantResult: "Enabled", + }, + { + name: "Missing threads per core - fallback to CPU vs core comparison", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 16 +On-line CPU(s) list: 0-15 +`, + wantResult: "Enabled", + }, + { + name: "Error parsing CPU count", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): invalid +Thread(s) per core: 2 +On-line CPU(s) list: 0-15 +`, + wantResult: "", + }, + { + name: "Error parsing socket count", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): invalid +Core(s) per socket: 8 +CPU(s): 16 +Thread(s) per core: 2 +On-line CPU(s) list: 0-15 +`, + wantResult: "", + }, + { + name: "Error parsing cores per socket", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: invalid +CPU(s): 16 +Thread(s) per core: 2 +On-line CPU(s) list: 0-15 +`, + wantResult: "", + }, + { + name: "Invalid online CPU list - should continue with total CPU count", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 16 +Thread(s) per core: 2 +On-line CPU(s) list: invalid-range +`, + wantResult: "Enabled", + }, + { + name: "Single core CPU - disabled result", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 1 +CPU(s): 1 +Thread(s) per core: 1 +On-line CPU(s) list: 0 +`, + wantResult: "Disabled", + }, + { + name: "4 threads per core - enabled", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 32 +Thread(s) per core: 4 +On-line CPU(s) list: 0-31 +`, + wantResult: "Enabled", + }, + { + name: "Missing CPU family - getCPUExtended will fail", + lscpuOutput: ` +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 16 +Thread(s) per core: 2 +On-line CPU(s) list: 0-15 +`, + wantResult: "", + }, + { + name: "Dual socket system with hyperthreading", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 2 +Core(s) per socket: 16 +CPU(s): 64 +Thread(s) per core: 2 +On-line CPU(s) list: 0-63 +`, + wantResult: "Enabled", + }, + { + name: "Quad socket system without hyperthreading", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 4 +Core(s) per socket: 12 +CPU(s): 48 +Thread(s) per core: 1 +On-line CPU(s) list: 0-47 +`, + wantResult: "Disabled", + }, + { + name: "Offlined cores with hyperthreading disabled and no threads per core", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 64 +On-line CPU(s) list: 0-7 +`, + wantResult: "Disabled", + }, + { + name: "Offlined cores with hyperthreading enabled and no threads per core", + lscpuOutput: ` +CPU family: 6 +Model: 143 +Stepping: 8 +Socket(s): 1 +Core(s) per socket: 8 +CPU(s): 64 +On-line CPU(s) list: 0-7,32-39 +`, + wantResult: "Enabled", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outputs := map[string]script.ScriptOutput{ + script.LscpuScriptName: { + Stdout: tt.lscpuOutput, + Stderr: "", + Exitcode: 0, + }, + } + + result := hyperthreadingFromOutput(outputs) + if result != tt.wantResult { + t.Errorf("hyperthreadingFromOutput() = %q, want %q", result, tt.wantResult) + } + }) + } +} + func TestGetFrequenciesFromMSR(t *testing.T) { tests := []struct { name string