diff --git a/README.md b/README.md
index 4b891459..7e312dfd 100644
--- a/README.md
+++ b/README.md
@@ -129,6 +129,38 @@ $ ./perfspect config --cores 24 --llc 2.0 --uncore-max 1.8
...
+##### Recording Configuration
+The current configuration can, optionally, be saved to a file using the `--record` flag. This creates a human-readable configuration file that can be used to restore settings later.
+
+Example:
+
+$ ./perfspect config --tdp 300 --record
+Configuration recorded to: perfspect_2025-12-01_14-30-45/gnr_config.txt
+
+
+##### Restoring Configuration
+The `config restore` subcommand restores configuration from a previously recorded file. This is useful for reverting changes or applying a known-good configuration across multiple systems.
+
+Example:
+
+$ ./perfspect config restore perfspect_2025-12-01_14-30-45/gnr_config.txt
+Configuration settings to restore from perfspect_2025-12-01_14-30-45/gnr_config.txt:
+ Cores per Socket : 86
+ L3 Cache : 336
+ Package Power / TDP : 350
+ ...
+Apply these configuration changes? [y/N]: y
+...
+
+
+Use the `--yes` flag to skip the confirmation prompt:
+
+$ ./perfspect config restore perfspect_2025-12-01_14-30-45/gnr_config.txt --yes
+
+
+> [!TIP]
+> The restore command works with remote targets too. Use `--target` or `--targets` to restore configuration on remote systems.
+
### Common Command Options
#### Local vs. Remote Targets
diff --git a/builder/build.Dockerfile b/builder/build.Dockerfile
index 2e9e0722..da46ad0e 100644
--- a/builder/build.Dockerfile
+++ b/builder/build.Dockerfile
@@ -14,7 +14,7 @@ ARG TAG=
FROM ${REGISTRY}${PREFIX}perfspect-tools:${TAG} AS tools
# STAGE 2 - image contains perfspect's Go components build environment
-FROM golang:1.25.4@sha256:6ca9eb0b32a4bd4e8c98a4a2edf2d7c96f3ea6db6eb4fc254eef6c067cf73bb4
+FROM golang:1.25.5@sha256:20b91eda7a9627c127c0225b0d4e8ec927b476fa4130c6760928b849d769c149
# copy the tools binaries and source from the previous stage
RUN mkdir /prebuilt
RUN mkdir /prebuilt/tools
diff --git a/cmd/config/config.go b/cmd/config/config.go
index 6c08f2f0..bd1a2b03 100644
--- a/cmd/config/config.go
+++ b/cmd/config/config.go
@@ -14,6 +14,7 @@ import (
"perfspect/internal/report"
"perfspect/internal/script"
"perfspect/internal/target"
+ "perfspect/internal/util"
"slices"
"strings"
@@ -25,6 +26,8 @@ const cmdName = "config"
var examples = []string{
fmt.Sprintf(" Set core count on local host: $ %s %s --cores 32", common.AppName, cmdName),
fmt.Sprintf(" Set multiple config items on local host: $ %s %s --core-max 3.0 --uncore-max 2.1 --tdp 120", common.AppName, cmdName),
+ fmt.Sprintf(" Record config to file before changes: $ %s %s --c6 disable --epb 0 --record", common.AppName, cmdName),
+ fmt.Sprintf(" Restore config from file: $ %s %s restore gnr_config.txt", common.AppName, cmdName),
fmt.Sprintf(" Set core count on remote target: $ %s %s --cores 32 --target 192.168.1.1 --user fred --key fred_key", common.AppName, cmdName),
fmt.Sprintf(" View current config on remote target: $ %s %s --target 192.168.1.1 --user fred --key fred_key", common.AppName, cmdName),
fmt.Sprintf(" Set governor on remote targets: $ %s %s --gov performance --targets targets.yaml", common.AppName, cmdName),
@@ -52,6 +55,22 @@ func runCmd(cmd *cobra.Command, args []string) error {
// appContext is the application context that holds common data and resources.
appContext := cmd.Parent().Context().Value(common.AppContext{}).(common.AppContext)
localTempDir := appContext.LocalTempDir
+ outputDir := appContext.OutputDir
+
+ flagRecord := cmd.Flags().Lookup(flagRecordName).Value.String() == "true"
+ flagNoSummary := cmd.Flags().Lookup(flagNoSummaryName).Value.String() == "true"
+
+ // create output directory if we are recording the configuration
+ if flagRecord {
+ err := util.CreateDirectoryIfNotExists(outputDir, 0755) // #nosec G301
+ if err != nil {
+ err = fmt.Errorf("failed to create output directory: %w", err)
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+ }
// get the targets
myTargets, targetErrs, err := common.GetTargets(cmd, true, true, localTempDir)
if err != nil {
@@ -91,14 +110,40 @@ func runCmd(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
return err
}
- // print config prior to changes, optionally
- if !cmd.Flags().Lookup(flagNoSummaryName).Changed {
- if err := printConfig(myTargets, localTempDir); err != nil {
+ // collect and print and/or record the configuration before making changes
+ if !flagNoSummary || flagRecord {
+ config, err := getConfig(myTargets, localTempDir)
+ if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
return err
}
+ reports, err := processConfig(config)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+ filesWritten, err := printConfig(reports, !flagNoSummary, flagRecord, outputDir)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+ if len(filesWritten) > 0 {
+ message := "Configuration"
+ if len(filesWritten) > 1 {
+ message = "Configurations"
+ }
+ fmt.Printf("%s recorded:\n", message)
+ for _, fileWritten := range filesWritten {
+ fmt.Printf(" %s\n", fileWritten)
+ }
+ fmt.Println()
+ }
}
// if no changes were requested, print a message and return
var changeRequested bool
@@ -138,9 +183,24 @@ func runCmd(cmd *cobra.Command, args []string) error {
}
multiSpinner.Finish()
fmt.Println() // blank line
- // print config after making changes
- if !cmd.Flags().Lookup(flagNoSummaryName).Changed {
- if err := printConfig(myTargets, localTempDir); err != nil {
+ // collect and print the configuration before making changes
+ if !flagNoSummary {
+ config, err := getConfig(myTargets, localTempDir)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+ reports, err := processConfig(config)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+ _, err = printConfig(reports, !flagNoSummary, false, outputDir) // print, don't record
+ if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
slog.Error(err.Error())
cmd.SilenceUsage = true
@@ -176,58 +236,53 @@ func setOnTarget(cmd *cobra.Command, myTarget target.Target, flagGroups []flagGr
channelError <- nil
return
}
- channelSetComplete := make(chan setOutput)
- var successMessages []string
- var errorMessages []string
+ var statusMessages []string
_ = statusUpdate(myTarget.GetName(), "updating configuration")
for _, group := range flagGroups {
for _, flag := range group.flags {
if flag.HasSetFunc() && cmd.Flags().Lookup(flag.GetName()).Changed {
- successMessages = append(successMessages, fmt.Sprintf("set %s to %s", flag.GetName(), flag.GetValueAsString()))
- errorMessages = append(errorMessages, fmt.Sprintf("failed to set %s to %s", flag.GetName(), flag.GetValueAsString()))
+ successMessage := fmt.Sprintf("set %s to %s", flag.GetName(), flag.GetValueAsString())
+ errorMessage := fmt.Sprintf("failed to set %s to %s", flag.GetName(), flag.GetValueAsString())
+ var setErr error
switch flag.GetType() {
case "int":
if flag.intSetFunc != nil {
value, _ := cmd.Flags().GetInt(flag.GetName())
- go flag.intSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
+ setErr = flag.intSetFunc(value, myTarget, localTempDir)
}
case "float64":
if flag.floatSetFunc != nil {
value, _ := cmd.Flags().GetFloat64(flag.GetName())
- go flag.floatSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
+ setErr = flag.floatSetFunc(value, myTarget, localTempDir)
}
case "string":
if flag.stringSetFunc != nil {
value, _ := cmd.Flags().GetString(flag.GetName())
- go flag.stringSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
+ setErr = flag.stringSetFunc(value, myTarget, localTempDir)
}
case "bool":
if flag.boolSetFunc != nil {
value, _ := cmd.Flags().GetBool(flag.GetName())
- go flag.boolSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1)
+ setErr = flag.boolSetFunc(value, myTarget, localTempDir)
}
}
+ if setErr != nil {
+ slog.Error(setErr.Error())
+ statusMessages = append(statusMessages, errorMessage)
+ } else {
+ statusMessages = append(statusMessages, successMessage)
+ }
}
}
}
- // wait for all set goroutines to finish
- statusMessages := []string{}
- for range successMessages {
- out := <-channelSetComplete
- if out.err != nil {
- slog.Error(out.err.Error())
- statusMessages = append(statusMessages, errorMessages[out.goRoutineID])
- } else {
- statusMessages = append(statusMessages, successMessages[out.goRoutineID])
- }
- }
statusMessage := fmt.Sprintf("configuration update complete: %s", strings.Join(statusMessages, ", "))
slog.Info(statusMessage, slog.String("target", myTarget.GetName()))
_ = statusUpdate(myTarget.GetName(), statusMessage)
channelError <- nil
}
-func printConfig(myTargets []target.Target, localTempDir string) (err error) {
+// getConfig collects the configuration data from the target(s)
+func getConfig(myTargets []target.Target, localTempDir string) ([]common.TargetScriptOutputs, error) {
scriptNames := report.GetScriptNamesForTable(report.ConfigurationTableName)
var scriptsToRun []script.ScriptDefinition
for _, scriptName := range scriptNames {
@@ -239,10 +294,10 @@ func printConfig(myTargets []target.Target, localTempDir string) (err error) {
channelTargetScriptOutputs := make(chan common.TargetScriptOutputs)
channelError := make(chan error)
for _, myTarget := range myTargets {
- err = multiSpinner.AddSpinner(myTarget.GetName())
+ err := multiSpinner.AddSpinner(myTarget.GetName())
if err != nil {
err = fmt.Errorf("failed to add spinner: %v", err)
- return
+ return nil, err
}
// run the selected scripts on the target
go collectOnTarget(myTarget, scriptsToRun, localTempDir, channelTargetScriptOutputs, channelError, multiSpinner.Status)
@@ -269,28 +324,55 @@ func printConfig(myTargets []target.Target, localTempDir string) (err error) {
}
}
multiSpinner.Finish()
- // process and print the table for each target
- for _, targetScriptOutputs := range orderedTargetScriptOutputs {
+ return orderedTargetScriptOutputs, nil
+}
+
+// processConfig processes the collected configuration data and creates text reports
+func processConfig(targetScriptOutputs []common.TargetScriptOutputs) (map[string][]byte, error) {
+ reports := make(map[string][]byte)
+ var err error
+ for _, targetScriptOutput := range targetScriptOutputs {
// process the tables, i.e., get field values from raw script output
tableNames := []string{report.ConfigurationTableName}
var tableValues []report.TableValues
- if tableValues, err = report.ProcessTables(tableNames, targetScriptOutputs.ScriptOutputs); err != nil {
+ if tableValues, err = report.ProcessTables(tableNames, targetScriptOutput.ScriptOutputs); err != nil {
err = fmt.Errorf("failed to process collected data: %v", err)
- return
+ return nil, err
}
// create the report for this single table
var reportBytes []byte
- if reportBytes, err = report.Create("txt", tableValues, targetScriptOutputs.TargetName); err != nil {
+ if reportBytes, err = report.Create("txt", tableValues, targetScriptOutput.TargetName); err != nil {
err = fmt.Errorf("failed to create report: %v", err)
- return
+ return nil, err
}
- // print the report
- if len(orderedTargetScriptOutputs) > 1 {
- fmt.Printf("%s\n", targetScriptOutputs.TargetName)
+ // append the report to the list
+ reports[targetScriptOutput.TargetName] = reportBytes
+ }
+ return reports, nil
+}
+
+// printConfig prints and/or saves the configuration reports
+func printConfig(reports map[string][]byte, toStdout bool, toFile bool, outputDir string) ([]string, error) {
+ filesWritten := []string{}
+ for targetName, reportBytes := range reports {
+ if toStdout {
+ // print the report to stdout
+ if len(reports) > 1 {
+ fmt.Printf("%s\n", targetName)
+ }
+ fmt.Print(string(reportBytes))
+ }
+ if toFile {
+ outputFilePath := fmt.Sprintf("%s/%s_config.txt", outputDir, targetName)
+ err := os.WriteFile(outputFilePath, reportBytes, 0644) // #nosec G306
+ if err != nil {
+ err = fmt.Errorf("failed to write configuration report to file: %v", err)
+ return filesWritten, err
+ }
+ filesWritten = append(filesWritten, outputFilePath)
}
- fmt.Print(string(reportBytes))
}
- return
+ return filesWritten, nil
}
// collectOnTarget runs the scripts on the target and sends the results to the appropriate channels
diff --git a/cmd/config/flag.go b/cmd/config/flag.go
index f40cc9f9..4a8e8ccb 100644
--- a/cmd/config/flag.go
+++ b/cmd/config/flag.go
@@ -10,20 +10,20 @@ import (
"github.com/spf13/pflag"
)
-// setOutput is a struct that holds the output of a flagDefinition set function
-type setOutput struct {
- goRoutineID int
- err error
-}
+type IntSetFunc func(int, target.Target, string) error
+type FloatSetFunc func(float64, target.Target, string) error
+type StringSetFunc func(string, target.Target, string) error
+type BoolSetFunc func(bool, target.Target, string) error
+type ValidationFunc func(cmd *cobra.Command) bool
// flagDefinition is a struct that defines a command line flag.
type flagDefinition struct {
pflag *pflag.Flag
- intSetFunc func(int, target.Target, string, chan setOutput, int)
- floatSetFunc func(float64, target.Target, string, chan setOutput, int)
- stringSetFunc func(string, target.Target, string, chan setOutput, int)
- boolSetFunc func(bool, target.Target, string, chan setOutput, int)
- validationFunc func(cmd *cobra.Command) bool
+ intSetFunc IntSetFunc
+ floatSetFunc FloatSetFunc
+ stringSetFunc StringSetFunc
+ boolSetFunc BoolSetFunc
+ validationFunc ValidationFunc
validationDescription string
}
@@ -48,7 +48,7 @@ func (f *flagDefinition) GetValueAsString() string {
}
// newIntFlag creates a new int flag and adds it to the command.
-func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc func(int, target.Target, string, chan setOutput, int), help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition {
+func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc IntSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
cmd.Flags().Int(name, defaultValue, help)
pFlag := cmd.Flags().Lookup(name)
return flagDefinition{
@@ -60,7 +60,7 @@ func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc func(
}
// newFloat64Flag creates a new float64 flag and adds it to the command.
-func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFunc func(float64, target.Target, string, chan setOutput, int), help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition {
+func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFunc FloatSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
cmd.Flags().Float64(name, defaultValue, help)
pFlag := cmd.Flags().Lookup(name)
return flagDefinition{
@@ -72,7 +72,7 @@ func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFu
}
// newStringFlag creates a new string flag and adds it to the command.
-func newStringFlag(cmd *cobra.Command, name string, defaultValue string, setFunc func(string, target.Target, string, chan setOutput, int), help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition {
+func newStringFlag(cmd *cobra.Command, name string, defaultValue string, setFunc StringSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
cmd.Flags().String(name, defaultValue, help)
pFlag := cmd.Flags().Lookup(name)
return flagDefinition{
@@ -84,7 +84,7 @@ func newStringFlag(cmd *cobra.Command, name string, defaultValue string, setFunc
}
// newBoolFlag creates a new boolean flag and adds it to the command.
-func newBoolFlag(cmd *cobra.Command, name string, defaultValue bool, setFunc func(bool, target.Target, string, chan setOutput, int), help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition {
+func newBoolFlag(cmd *cobra.Command, name string, defaultValue bool, setFunc BoolSetFunc, help string, validationDescription string, validationFunc ValidationFunc) flagDefinition {
cmd.Flags().Bool(name, defaultValue, help)
pFlag := cmd.Flags().Lookup(name)
return flagDefinition{
diff --git a/cmd/config/flag_groups.go b/cmd/config/flag_groups.go
index 3fb765dc..10fd0f11 100644
--- a/cmd/config/flag_groups.go
+++ b/cmd/config/flag_groups.go
@@ -8,6 +8,7 @@ import (
"perfspect/internal/common"
"perfspect/internal/report"
"perfspect/internal/target"
+ "regexp"
"slices"
"strings"
@@ -38,14 +39,15 @@ const (
// general flag names
const (
- flagCoreCountName = "cores"
- flagLLCSizeName = "llc"
- flagTDPName = "tdp"
- flagAllCoreMaxFrequencyName = "core-max"
- flagEPBName = "epb"
- flagEPPName = "epp"
- flagGovernorName = "gov"
- flagELCName = "elc"
+ flagCoreCountName = "cores"
+ flagLLCSizeName = "llc"
+ flagTDPName = "tdp"
+ flagSSEFrequencyName = "core-max"
+ flagSSEFrequencyAllBucketsName = "core-max-buckets"
+ flagEPBName = "epb"
+ flagEPPName = "epp"
+ flagGovernorName = "gov"
+ flagELCName = "elc"
)
// uncore frequency flag names
@@ -66,6 +68,7 @@ const (
// other flag names
const (
flagNoSummaryName = "no-summary"
+ flagRecordName = "record"
)
// governorOptions - list of valid governor options
@@ -95,11 +98,20 @@ func initializeFlags(cmd *cobra.Command) {
func(cmd *cobra.Command) bool { value, _ := cmd.Flags().GetFloat64(flagLLCSizeName); return value > 0 }),
newIntFlag(cmd, flagTDPName, 0, setTDP, "maximum power per processor in Watts", "greater than 0",
func(cmd *cobra.Command) bool { value, _ := cmd.Flags().GetInt(flagTDPName); return value > 0 }),
- newFloat64Flag(cmd, flagAllCoreMaxFrequencyName, 0, setCoreFrequency, "all-core max frequency in GHz", "greater than 0.1",
+ newFloat64Flag(cmd, flagSSEFrequencyName, 0, setSSEFrequency, "SSE frequency in GHz", "greater than 0.1",
func(cmd *cobra.Command) bool {
- value, _ := cmd.Flags().GetFloat64(flagAllCoreMaxFrequencyName)
+ value, _ := cmd.Flags().GetFloat64(flagSSEFrequencyName)
return value > 0.1
}),
+ newStringFlag(cmd, flagSSEFrequencyAllBucketsName, "", setSSEFrequencies, "SSE frequencies for all core buckets in GHz (e.g., 1-40/3.5, 41-60/3.4, 61-86/3.2)", "correct format",
+ func(cmd *cobra.Command) bool {
+ value, _ := cmd.Flags().GetString(flagSSEFrequencyAllBucketsName)
+ // Regex pattern: 1-8 buckets in format "start-end/freq", comma-separated
+ // Example: "1-40/3.5, 41-60/3.4, 61-86/3.2"
+ pattern := `^\d+-\d+/\d+(\.\d+)?(, \d+-\d+/\d+(\.\d+)?){0,7}$`
+ matched, _ := regexp.MatchString(pattern, value)
+ return matched
+ }),
newIntFlag(cmd, flagEPBName, 0, setEPB, "energy perf bias from best performance (0) to most power savings (15)", "0-15",
func(cmd *cobra.Command) bool {
value, _ := cmd.Flags().GetInt(flagEPBName)
@@ -125,8 +137,8 @@ func initializeFlags(cmd *cobra.Command) {
group = flagGroup{name: flagGroupUncoreFrequencyName, flags: []flagDefinition{}}
group.flags = append(group.flags,
newFloat64Flag(cmd, flagUncoreMaxFrequencyName, 0,
- func(value float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setUncoreFrequency(true, value, myTarget, localTempDir, completeChannel, goRoutineId)
+ func(value float64, myTarget target.Target, localTempDir string) error {
+ return setUncoreFrequency(true, value, myTarget, localTempDir)
},
"maximum uncore frequency in GHz [EMR-]", "greater than 0.1",
func(cmd *cobra.Command) bool {
@@ -134,8 +146,8 @@ func initializeFlags(cmd *cobra.Command) {
return value > 0.1
}),
newFloat64Flag(cmd, flagUncoreMinFrequencyName, 0,
- func(value float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setUncoreFrequency(false, value, myTarget, localTempDir, completeChannel, goRoutineId)
+ func(value float64, myTarget target.Target, localTempDir string) error {
+ return setUncoreFrequency(false, value, myTarget, localTempDir)
},
"minimum uncore frequency in GHz [EMR-]", "greater than 0.1",
func(cmd *cobra.Command) bool {
@@ -143,8 +155,8 @@ func initializeFlags(cmd *cobra.Command) {
return value > 0.1
}),
newFloat64Flag(cmd, flagUncoreMaxComputeFrequencyName, 0,
- func(value float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setUncoreDieFrequency(true, true, value, myTarget, localTempDir, completeChannel, goRoutineId)
+ func(value float64, myTarget target.Target, localTempDir string) error {
+ return setUncoreDieFrequency(true, true, value, myTarget, localTempDir)
},
"maximum uncore compute die frequency in GHz [SRF+]", "greater than 0.1",
func(cmd *cobra.Command) bool {
@@ -152,8 +164,8 @@ func initializeFlags(cmd *cobra.Command) {
return value > 0.1
}),
newFloat64Flag(cmd, flagUncoreMinComputeFrequencyName, 0,
- func(value float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setUncoreDieFrequency(false, true, value, myTarget, localTempDir, completeChannel, goRoutineId)
+ func(value float64, myTarget target.Target, localTempDir string) error {
+ return setUncoreDieFrequency(false, true, value, myTarget, localTempDir)
},
"minimum uncore compute die frequency in GHz [SRF+]", "greater than 0.1",
func(cmd *cobra.Command) bool {
@@ -161,8 +173,8 @@ func initializeFlags(cmd *cobra.Command) {
return value > 0.1
}),
newFloat64Flag(cmd, flagUncoreMaxIOFrequencyName, 0,
- func(value float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setUncoreDieFrequency(true, false, value, myTarget, localTempDir, completeChannel, goRoutineId)
+ func(value float64, myTarget target.Target, localTempDir string) error {
+ return setUncoreDieFrequency(true, false, value, myTarget, localTempDir)
},
"maximum uncore IO die frequency in GHz [SRF+]", "greater than 0.1",
func(cmd *cobra.Command) bool {
@@ -170,8 +182,8 @@ func initializeFlags(cmd *cobra.Command) {
return value > 0.1
}),
newFloat64Flag(cmd, flagUncoreMinIOFrequencyName, 0,
- func(value float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setUncoreDieFrequency(false, false, value, myTarget, localTempDir, completeChannel, goRoutineId)
+ func(value float64, myTarget target.Target, localTempDir string) error {
+ return setUncoreDieFrequency(false, false, value, myTarget, localTempDir)
},
"minimum uncore IO die frequency in GHz [SRF+]", "greater than 0.1",
func(cmd *cobra.Command) bool {
@@ -191,8 +203,8 @@ func initializeFlags(cmd *cobra.Command) {
// flag default value
"",
// flag value setter function
- func(value string, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setPrefetcher(value, myTarget, localTempDir, pref.ShortName, completeChannel, goRoutineId)
+ func(value string, myTarget target.Target, localTempDir string) error {
+ return setPrefetcher(value, myTarget, localTempDir, pref.ShortName)
},
// flag help
func() string {
@@ -212,19 +224,13 @@ func initializeFlags(cmd *cobra.Command) {
// c-state options
group = flagGroup{name: flagGroupCstateName, flags: []flagDefinition{}}
group.flags = append(group.flags,
- newStringFlag(cmd, flagC6Name, "",
- func(value string, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setC6(value, myTarget, localTempDir, completeChannel, goRoutineId)
- },
+ newStringFlag(cmd, flagC6Name, "", setC6,
"C6 ("+strings.Join(c6Options, ", ")+")", strings.Join(c6Options, ", "),
func(cmd *cobra.Command) bool {
value, _ := cmd.Flags().GetString(flagC6Name)
return slices.Contains(c6Options, value)
}),
- newStringFlag(cmd, flagC1DemotionName, "",
- func(value string, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
- setC1Demotion(value, myTarget, localTempDir, completeChannel, goRoutineId)
- },
+ newStringFlag(cmd, flagC1DemotionName, "", setC1Demotion,
"C1 Demotion ("+strings.Join(c1DemotionOptions, ", ")+")", strings.Join(c1DemotionOptions, ", "),
func(cmd *cobra.Command) bool {
value, _ := cmd.Flags().GetString(flagC1DemotionName)
@@ -237,6 +243,9 @@ func initializeFlags(cmd *cobra.Command) {
group.flags = append(group.flags,
newBoolFlag(cmd, flagNoSummaryName, false, nil, "do not print configuration summary", "", nil),
)
+ group.flags = append(group.flags,
+ newBoolFlag(cmd, flagRecordName, false, nil, "record the current configuration to a file to be restored later", "", nil),
+ )
flagGroups = append(flagGroups, group)
common.AddTargetFlags(Cmd)
@@ -261,6 +270,11 @@ func usageFunc(cmd *cobra.Command) error {
cmd.Printf(" --%-20s %s\n", flag.Name, flag.Help)
}
+ cmd.Printf("\nSubcommands:\n")
+ for _, subCmd := range cmd.Commands() {
+ cmd.Printf(" %s: %s\n", subCmd.Name(), subCmd.Short)
+ }
+
cmd.Println("\nGlobal Flags:")
cmd.Parent().PersistentFlags().VisitAll(func(pf *pflag.Flag) {
flagDefault := ""
diff --git a/cmd/config/restore.go b/cmd/config/restore.go
new file mode 100644
index 00000000..f31d2b8f
--- /dev/null
+++ b/cmd/config/restore.go
@@ -0,0 +1,525 @@
+// Package config is a subcommand of the root command. It sets system configuration items on target platform(s).
+package config
+
+// Copyright (C) 2021-2025 Intel Corporation
+// SPDX-License-Identifier: BSD-3-Clause
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "log/slog"
+ "os"
+ "os/exec"
+ "perfspect/internal/common"
+ "perfspect/internal/progress"
+ "regexp"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+)
+
+const restoreCmdName = "restore"
+
+// flagValue represents a single flag name and value pair, preserving order
+type flagValue struct {
+ fieldName string // the human-readable field name from config file (e.g., "Cores per Socket")
+ flagName string // the command-line flag name (e.g., "cores")
+ value string
+}
+
+var restoreExamples = []string{
+ fmt.Sprintf(" Restore config from file on local host: $ %s %s %s gnr_config.txt", common.AppName, cmdName, restoreCmdName),
+ fmt.Sprintf(" Restore config on remote target: $ %s %s %s gnr_config.txt --target 192.168.1.1 --user fred --key fred_key", common.AppName, cmdName, restoreCmdName),
+ fmt.Sprintf(" Restore config without confirmation: $ %s %s %s gnr_config.txt --yes", common.AppName, cmdName, restoreCmdName),
+}
+
+var RestoreCmd = &cobra.Command{
+ Use: restoreCmdName + " ",
+ Short: "Restore system configuration from a previously recorded file",
+ Long: `Restores system configuration from a file that was previously recorded using the --record flag.
+
+The restore command will parse the configuration file, validate the settings against the target system,
+and apply the configuration changes. By default, you will be prompted to confirm before applying changes.`,
+ Example: strings.Join(restoreExamples, "\n"),
+ RunE: runRestoreCmd,
+ PreRunE: validateRestoreFlags,
+ Args: cobra.ExactArgs(1),
+ SilenceErrors: true,
+}
+
+var (
+ flagRestoreYes bool
+)
+
+const (
+ flagRestoreYesName = "yes"
+)
+
+func init() {
+ Cmd.AddCommand(RestoreCmd)
+
+ RestoreCmd.Flags().BoolVar(&flagRestoreYes, flagRestoreYesName, false, "skip confirmation prompt")
+
+ common.AddTargetFlags(RestoreCmd)
+
+ RestoreCmd.SetUsageFunc(restoreUsageFunc)
+}
+
+func restoreUsageFunc(cmd *cobra.Command) error {
+ cmd.Printf("Usage: %s [flags]\n\n", cmd.CommandPath())
+ cmd.Printf("Examples:\n%s\n\n", cmd.Example)
+ cmd.Println("Arguments:")
+ cmd.Printf(" file: path to the configuration file to restore\n\n")
+ cmd.Println("Flags:")
+ cmd.Print(" General Options:\n")
+ cmd.Printf(" --%-20s %s\n", flagRestoreYesName, "skip confirmation prompt")
+
+ targetFlagGroup := common.GetTargetFlagGroup()
+ cmd.Printf(" %s:\n", targetFlagGroup.GroupName)
+ for _, flag := range targetFlagGroup.Flags {
+ cmd.Printf(" --%-20s %s\n", flag.Name, flag.Help)
+ }
+
+ cmd.Println("\nGlobal Flags:")
+ cmd.Root().PersistentFlags().VisitAll(func(pf *pflag.Flag) {
+ flagDefault := ""
+ if cmd.Root().PersistentFlags().Lookup(pf.Name).DefValue != "" {
+ flagDefault = fmt.Sprintf(" (default: %s)", cmd.Root().PersistentFlags().Lookup(pf.Name).DefValue)
+ }
+ cmd.Printf(" --%-20s %s%s\n", pf.Name, pf.Usage, flagDefault)
+ })
+ return nil
+}
+
+func validateRestoreFlags(cmd *cobra.Command, args []string) error {
+ // validate that the file exists
+ if len(args) != 1 {
+ return common.FlagValidationError(cmd, "restore requires exactly one argument: the path to the configuration file")
+ }
+ filePath := args[0]
+ if _, err := os.Stat(filePath); os.IsNotExist(err) {
+ return common.FlagValidationError(cmd, fmt.Sprintf("configuration file does not exist: %s", filePath))
+ }
+ // validate common target flags
+ if err := common.ValidateTargetFlags(cmd); err != nil {
+ return common.FlagValidationError(cmd, err.Error())
+ }
+ return nil
+}
+
+func runRestoreCmd(cmd *cobra.Command, args []string) error {
+ configFilePath := args[0]
+
+ // parse the configuration file
+ flagValues, err := parseConfigFile(configFilePath)
+ if err != nil {
+ err = fmt.Errorf("failed to parse configuration file: %v", err)
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+
+ if len(flagValues) == 0 {
+ fmt.Println("No configuration settings found in file.")
+ return nil
+ }
+
+ // show what will be restored
+ fmt.Printf("Configuration settings to restore from %s:\n", configFilePath)
+ // find the longest field name for alignment
+ maxLen := 0
+ for _, fv := range flagValues {
+ if len(fv.fieldName) > maxLen {
+ maxLen = len(fv.fieldName)
+ }
+ }
+ for _, fv := range flagValues {
+ fmt.Printf(" %-*s: %s\n", maxLen, fv.fieldName, fv.value)
+ }
+ fmt.Println()
+
+ // build the command to execute
+ executable, err := os.Executable()
+ if err != nil {
+ err = fmt.Errorf("failed to get executable path: %v", err)
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+
+ // build arguments: perfspect config --target ... --flag1 value1 --flag2 value2 ...
+ cmdArgs := []string{"config"}
+
+ // copy target flags from restore command first
+ targetFlags := []string{"target", "targets", "user", "key", "port"}
+ for _, flagName := range targetFlags {
+ if flag := cmd.Flags().Lookup(flagName); flag != nil && flag.Changed {
+ cmdArgs = append(cmdArgs, fmt.Sprintf("--%s", flagName), flag.Value.String())
+ }
+ }
+
+ // copy relevant global flags from root command next
+ globalFlags := []string{"debug", "output", "tempdir", "syslog", "log-stdout"}
+ for _, flagName := range globalFlags {
+ if flag := cmd.Root().PersistentFlags().Lookup(flagName); flag != nil && flag.Changed {
+ if flag.Value.Type() == "bool" {
+ // for bool flags, only add if true
+ if flag.Value.String() == "true" {
+ cmdArgs = append(cmdArgs, fmt.Sprintf("--%s", flagName))
+ }
+ } else {
+ cmdArgs = append(cmdArgs, fmt.Sprintf("--%s", flagName), flag.Value.String())
+ }
+ }
+ }
+
+ // add config flags last
+ for _, fv := range flagValues {
+ cmdArgs = append(cmdArgs, fmt.Sprintf("--%s", fv.flagName), fv.value)
+ }
+
+ // always add --no-summary to avoid printing config summary before and after changes
+ cmdArgs = append(cmdArgs, "--no-summary")
+
+ // show the command that will be executed
+ fmt.Printf("Command: %s %s\n\n", executable, strings.Join(cmdArgs, " "))
+
+ // prompt for confirmation unless --yes was specified
+ if !flagRestoreYes {
+ fmt.Print("Apply these configuration changes? [y/N]: ")
+ reader := bufio.NewReader(os.Stdin)
+ response, err := reader.ReadString('\n')
+ if err != nil {
+ err = fmt.Errorf("failed to read user input: %v", err)
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+ response = strings.TrimSpace(strings.ToLower(response))
+ if response != "y" && response != "yes" {
+ fmt.Println("Restore cancelled.")
+ return nil
+ }
+ }
+
+ // execute the command
+ slog.Info("executing perfspect config", slog.String("command", executable), slog.String("args", strings.Join(cmdArgs, " ")))
+
+ execCmd := exec.Command(executable, cmdArgs...)
+ execCmd.Stdin = os.Stdin
+
+ // capture stdout and stderr (don't display in real-time to avoid interfering with spinner)
+ var stdoutBuf, stderrBuf bytes.Buffer
+ execCmd.Stdout = &stdoutBuf
+ execCmd.Stderr = &stderrBuf
+
+ // show progress while command is running
+ multiSpinner := progress.NewMultiSpinner()
+ err = multiSpinner.AddSpinner("config")
+ if err != nil {
+ return fmt.Errorf("failed to add spinner: %v", err)
+ }
+ multiSpinner.Start()
+ _ = multiSpinner.Status("config", "applying configuration changes")
+
+ err = execCmd.Run()
+
+ _ = multiSpinner.Status("config", "configuration changes complete")
+ multiSpinner.Finish()
+
+ if err != nil {
+ if exitErr, ok := err.(*exec.ExitError); ok {
+ err = fmt.Errorf("config command failed with exit code %d", exitErr.ExitCode())
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+ err = fmt.Errorf("failed to execute config command: %v", err)
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ slog.Error(err.Error())
+ cmd.SilenceUsage = true
+ return err
+ }
+
+ // parse stderr output and present results in flag order
+ parseAndPresentResults(stderrBuf.String(), flagValues)
+
+ return nil
+}
+
+// parseConfigFile parses a recorded configuration file and extracts flag names and values
+// Returns a slice of flagValue structs in the order they appear in the file
+func parseConfigFile(filePath string) ([]flagValue, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open file: %v", err)
+ }
+ defer file.Close()
+
+ var flagValues []flagValue
+ scanner := bufio.NewScanner(file)
+
+ // regex to match lines with flag syntax: --flag-name
+ // Format: "Field Name: Value --flag-name "
+ flagLineRegex := regexp.MustCompile(`^\s*(.+?):\s+(.+?)\s+(--\S+)\s+<.+>$`)
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ matches := flagLineRegex.FindStringSubmatch(line)
+ if len(matches) == 4 {
+ // matches[1] = field name (e.g., "Cores per Socket")
+ fieldName := strings.TrimSpace(matches[1])
+ rawValue := strings.TrimSpace(matches[2])
+ flagStr := matches[3]
+
+ // extract flag name (remove the leading --)
+ flagName := strings.TrimPrefix(flagStr, "--")
+
+ // special case: if flag is core-max change it to core-max-buckets
+ if flagName == flagSSEFrequencyName {
+ flagName = flagSSEFrequencyAllBucketsName
+ }
+
+ // convert the raw value to the appropriate format
+ convertedValue, err := convertValue(flagName, rawValue)
+ if err != nil {
+ slog.Warn(fmt.Sprintf("skipping flag %s: %v", flagName, err))
+ continue
+ }
+
+ flagValues = append(flagValues, flagValue{
+ fieldName: fieldName,
+ flagName: flagName,
+ value: convertedValue,
+ })
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("error reading file: %v", err)
+ }
+
+ return flagValues, nil
+}
+
+// convertValue converts a raw value string from the config file to the appropriate format for the flag
+func convertValue(flagName string, rawValue string) (string, error) {
+ // handle "inconsistent" values - skip these
+ if strings.Contains(strings.ToLower(rawValue), "inconsistent") {
+ return "", fmt.Errorf("value is inconsistent, cannot restore")
+ }
+
+ // handle numeric values with units
+ switch flagName {
+ case flagCoreCountName:
+ // "86" -> "86" (just validate it's a number)
+ if _, err := strconv.Atoi(rawValue); err != nil {
+ return "", fmt.Errorf("invalid integer value: %s", rawValue)
+ }
+ return rawValue, nil
+ case flagLLCSizeName:
+ // "336M" -> "336" (MB is assumed)
+ return parseNumericWithUnit(rawValue, "M", "MB")
+ case flagTDPName:
+ // "350W" -> "350" (Watts is assumed)
+ return parseNumericWithUnit(rawValue, "W")
+ case flagSSEFrequencyAllBucketsName:
+ // "1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0" -> keep as-is
+ // Just validate the format: should contain core ranges and frequencies
+ if !strings.Contains(rawValue, "/") {
+ return "", fmt.Errorf("invalid frequency buckets format: %s", rawValue)
+ }
+ return rawValue, nil
+ case flagSSEFrequencyName, flagUncoreMaxFrequencyName, flagUncoreMinFrequencyName,
+ flagUncoreMaxComputeFrequencyName, flagUncoreMinComputeFrequencyName,
+ flagUncoreMaxIOFrequencyName, flagUncoreMinIOFrequencyName:
+ // "3.2GHz" -> "3.2" (GHz is assumed)
+ return parseNumericWithUnit(rawValue, "GHz")
+ case flagEPBName, flagEPPName:
+ // "Performance (0)" -> "0"
+ // "inconsistent" -> error
+ return parseParenthesizedNumber(rawValue)
+ case flagGovernorName:
+ // "performance" or "powersave"
+ return parseEnableDisableOrOption(rawValue, governorOptions)
+ case flagELCName:
+ // "Default" -> "default"
+ // "Latency-Optimized" -> "latency-optimized"
+ rawValueLower := strings.ToLower(rawValue)
+ if slices.Contains(elcOptions, rawValueLower) {
+ return rawValueLower, nil
+ }
+ return "", fmt.Errorf("invalid elc value: %s", rawValue)
+ case flagC6Name:
+ return parseEnableDisableOrOption(rawValue, c6Options)
+ case flagC1DemotionName:
+ return parseEnableDisableOrOption(rawValue, c1DemotionOptions)
+ default:
+ // check if it's a prefetcher flag
+ if strings.HasPrefix(flagName, "pref-") {
+ // "Enabled" or "Disabled" -> "enable" or "disable"
+ return parseEnableDisableOrOption(rawValue, prefetcherOptions)
+ }
+ }
+
+ return "", fmt.Errorf("unknown flag: %s", flagName)
+}
+
+// parseNumericWithUnit extracts numeric value from strings like "3.2GHz", "336M", "350W"
+func parseNumericWithUnit(value string, units ...string) (string, error) {
+ // trim whitespace
+ value = strings.TrimSpace(value)
+
+ // try to remove each unit suffix
+ for _, unit := range units {
+ if strings.HasSuffix(value, unit) {
+ numStr := strings.TrimSuffix(value, unit)
+ // validate it's a valid number
+ if _, err := strconv.ParseFloat(numStr, 64); err != nil {
+ return "", fmt.Errorf("invalid numeric value: %s", value)
+ }
+ return numStr, nil
+ }
+ }
+
+ // if no unit found, check if it's already a valid number
+ if _, err := strconv.ParseFloat(value, 64); err != nil {
+ return "", fmt.Errorf("value missing expected unit (%s): %s", strings.Join(units, ", "), value)
+ }
+ return value, nil
+}
+
+// parseParenthesizedNumber extracts number from strings like "Performance (0)" or "Best Performance (0)"
+func parseParenthesizedNumber(value string) (string, error) {
+ // look for pattern "text (number)"
+ parenRegex := regexp.MustCompile(`\((\d+)\)`)
+ matches := parenRegex.FindStringSubmatch(value)
+ if len(matches) == 2 {
+ return matches[1], nil
+ }
+ return "", fmt.Errorf("could not extract number from: %s", value)
+}
+
+// parseEnableDisableOrOption converts "Enabled"/"Disabled" to "enable"/"disable" or validates against option list
+func parseEnableDisableOrOption(value string, validOptions []string) (string, error) {
+ // normalize: trim and lowercase
+ normalized := strings.ToLower(strings.TrimSpace(value))
+
+ // check direct match with valid options
+ if slices.Contains(validOptions, normalized) {
+ return normalized, nil
+ }
+
+ // special case: "Enabled" -> "enable", "Disabled" -> "disable"
+ switch normalized {
+ case "enabled":
+ normalized = "enable"
+ case "disabled":
+ normalized = "disable"
+ }
+
+ // check if normalized value is in valid options
+ if slices.Contains(validOptions, normalized) {
+ return normalized, nil
+ }
+
+ return "", fmt.Errorf("invalid value '%s', valid options are: %s", value, strings.Join(validOptions, ", "))
+}
+
+// parseAndPresentResults parses the stderr output from the config command and presents
+// successes and errors in the same order as the config flags were specified
+// example: "configuration update complete: set gov to powersave, set c1-demotion to disable, set tdp to 350, set c6 to enable, set epb to 0, set core-max to 3.2, set cores to 86, set elc to default, failed to set pref-l2hw to enable, set pref-dcuhw to enable, set pref-llc to disable, set pref-aop to enable, set pref-l2adj to enable, set uncore-max-compute to 2.2, failed to set llc to 336, set pref-dcunp to enable, set pref-homeless to enable, set pref-amp to enable, set pref-dcuip to enable, set pref-llcpp to enable, set uncore-max-io to 2.5, set uncore-min-compute to 0.8, set uncore-min-io to 0.8"
+func parseAndPresentResults(stderrOutput string, flagValues []flagValue) {
+ if stderrOutput == "" {
+ return
+ }
+
+ // flagResult stores the success/failure status and value for each flag
+ type flagResult struct {
+ success bool
+ value string
+ }
+
+ // Build a map of flag names to their results
+ flagResults := make(map[string]flagResult)
+
+ // Parse stderr line by line
+ // We split on delimiters ", set " and ", failed to set " to handle messages where
+ // flag values themselves contain commas (like core-max-buckets)
+ for line := range strings.SplitSeq(stderrOutput, "\n") {
+ // Split on the delimiter patterns
+ // First, replace delimiters with a marker we can split on
+ line = strings.ReplaceAll(line, ", set ", "\x00SET\x00")
+ line = strings.ReplaceAll(line, ", failed to set ", "\x00FAILED\x00")
+
+ parts := strings.Split(line, "\x00")
+ for i := 0; i < len(parts); i++ {
+ part := parts[i]
+ if part == "SET" && i+1 < len(parts) {
+ // Next part should be "flagname to value"
+ i++
+ part = parts[i]
+ // Match "flagname to value"
+ if matches := regexp.MustCompile(`^([\w-]+) to (.+)$`).FindStringSubmatch(part); len(matches) == 3 {
+ flagName := matches[1]
+ value := strings.TrimSpace(matches[2])
+ flagResults[flagName] = flagResult{success: true, value: value}
+ }
+ } else if part == "FAILED" && i+1 < len(parts) {
+ // Next part should be "flagname to value"
+ i++
+ part = parts[i]
+ // Match "flagname to value"
+ if matches := regexp.MustCompile(`^([\w-]+) to (.+)$`).FindStringSubmatch(part); len(matches) == 3 {
+ flagName := matches[1]
+ value := strings.TrimSpace(matches[2])
+ flagResults[flagName] = flagResult{success: false, value: value}
+ }
+ } else if strings.Contains(part, " to ") {
+ // Handle the first item in the line (e.g., "configuration update complete: set cores to 86")
+ if matches := regexp.MustCompile(`\bset ([\w-]+) to (.+)$`).FindStringSubmatch(part); len(matches) == 3 {
+ flagName := matches[1]
+ value := strings.TrimSpace(matches[2])
+ flagResults[flagName] = flagResult{success: true, value: value}
+ } else if matches := regexp.MustCompile(`\bfailed to set ([\w-]+) to (.+)$`).FindStringSubmatch(part); len(matches) == 3 {
+ flagName := matches[1]
+ value := strings.TrimSpace(matches[2])
+ flagResults[flagName] = flagResult{success: false, value: value}
+ }
+ }
+ }
+ }
+
+ // Present results in the order of flagValues
+ if len(flagValues) > 0 {
+ fmt.Println("\nConfiguration Results:")
+ // find the longest field name for alignment
+ maxLen := 0
+ for _, fv := range flagValues {
+ if len(fv.fieldName) > maxLen {
+ maxLen = len(fv.fieldName)
+ }
+ }
+ for _, fv := range flagValues {
+ if result, found := flagResults[fv.flagName]; found {
+ if result.success {
+ fmt.Printf(" ✓ %-*s: %s\n", maxLen, fv.fieldName, result.value)
+ } else {
+ fmt.Printf(" ✗ %-*s: %s\n", maxLen, fv.fieldName, result.value)
+ }
+ } else {
+ // If no explicit success or error was found, show unknown status
+ fmt.Printf(" ? %-*s: status unknown\n", maxLen, fv.fieldName)
+ }
+ }
+ fmt.Println()
+ }
+}
diff --git a/cmd/config/restore_test.go b/cmd/config/restore_test.go
new file mode 100644
index 00000000..10bbbb4b
--- /dev/null
+++ b/cmd/config/restore_test.go
@@ -0,0 +1,442 @@
+package config
+
+// Copyright (C) 2021-2025 Intel Corporation
+// SPDX-License-Identifier: BSD-3-Clause
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseConfigFile(t *testing.T) {
+ // create a temporary config file
+ content := `Configuration
+=============
+Cores per Socket: 86 --cores
+L3 Cache: 336M --llc
+Package Power / TDP: 350W --tdp
+Core SSE Frequency: 1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0 --core-max
+Uncore Max Frequency (Compute): 2.2GHz --uncore-max-compute
+Energy Performance Bias: Performance (0) --epb <0-15>
+Energy Performance Preference: inconsistent --epp <0-255>
+Scaling Governor: powersave --gov
+L2 HW prefetcher: Enabled --pref-l2hw
+C6: Disabled --c6
+`
+
+ tmpFile, err := os.CreateTemp("", "config_test_*.txt")
+ require.NoError(t, err)
+ defer os.Remove(tmpFile.Name())
+
+ _, err = tmpFile.WriteString(content)
+ require.NoError(t, err)
+ tmpFile.Close()
+
+ // parse the file
+ flagValues, err := parseConfigFile(tmpFile.Name())
+ require.NoError(t, err)
+
+ // convert slice to map for easier testing
+ valueMap := make(map[string]string)
+ for _, fv := range flagValues {
+ valueMap[fv.flagName] = fv.value
+ }
+
+ // verify expected values
+ assert.Equal(t, "86", valueMap["cores"])
+ assert.Equal(t, "336", valueMap["llc"])
+ assert.Equal(t, "350", valueMap["tdp"])
+ // verify core-max with buckets is converted to core-max-buckets
+ assert.Equal(t, "1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0", valueMap["core-max-buckets"])
+ assert.Equal(t, "2.2", valueMap["uncore-max-compute"])
+ assert.Equal(t, "0", valueMap["epb"])
+ assert.Equal(t, "powersave", valueMap["gov"])
+ assert.Equal(t, "enable", valueMap["pref-l2hw"])
+ assert.Equal(t, "disable", valueMap["c6"])
+
+ // verify inconsistent EPP was skipped
+ _, exists := valueMap["epp"]
+ assert.False(t, exists, "EPP with 'inconsistent' value should be skipped")
+
+ // verify order is preserved
+ assert.Equal(t, "cores", flagValues[0].flagName)
+ assert.Equal(t, "llc", flagValues[1].flagName)
+ assert.Equal(t, "tdp", flagValues[2].flagName)
+}
+
+func TestParseConfigFileMissingValues(t *testing.T) {
+ // create a temporary config file
+ content := `Configuration
+=============
+Cores per Socket: 4 --cores
+L3 Cache: 105M --llc
+Package Power / TDP: --tdp
+Core SSE Frequency: 1-24/3.8, 25-30/3.7, 31-34/3.6, 35-38/3.5, 39-44/3.3, 45-48/3.2 --core-max
+Uncore Max Frequency: --uncore-max
+Uncore Min Frequency: --uncore-min
+Energy Performance Bias: --epb <0-15>
+Energy Performance Preference: --epp <0-255>
+Scaling Governor: --gov
+L2 HW prefetcher: Enabled --pref-l2hw
+L2 Adj prefetcher: Enabled --pref-l2adj
+DCU HW prefetcher: Enabled --pref-dcuhw
+DCU IP prefetcher: Enabled --pref-dcuip
+DCU NP prefetcher: Enabled --pref-dcunp
+AMP prefetcher: Enabled --pref-amp
+C6: Enabled --c6
+C1 Demotion: Disabled --c1-demotion
+`
+
+ tmpFile, err := os.CreateTemp("", "config_test_*.txt")
+ require.NoError(t, err)
+ defer os.Remove(tmpFile.Name())
+
+ _, err = tmpFile.WriteString(content)
+ require.NoError(t, err)
+ tmpFile.Close()
+
+ // parse the file
+ flagValues, err := parseConfigFile(tmpFile.Name())
+ require.NoError(t, err)
+
+ // convert slice to map for easier testing
+ valueMap := make(map[string]string)
+ for _, fv := range flagValues {
+ valueMap[fv.flagName] = fv.value
+ }
+
+ // verify expected values
+ assert.Equal(t, "4", valueMap["cores"])
+ assert.Equal(t, "105", valueMap["llc"])
+ assert.Equal(t, "", valueMap["tdp"])
+ // verify core-max with buckets is converted to core-max-buckets
+ assert.Equal(t, "1-24/3.8, 25-30/3.7, 31-34/3.6, 35-38/3.5, 39-44/3.3, 45-48/3.2", valueMap["core-max-buckets"])
+ assert.Equal(t, "", valueMap["uncore-max"])
+ assert.Equal(t, "", valueMap["uncore-min"])
+ assert.Equal(t, "", valueMap["gov"])
+ assert.Equal(t, "enable", valueMap["pref-l2hw"])
+ assert.Equal(t, "enable", valueMap["c6"])
+}
+
+func TestConvertValue(t *testing.T) {
+ tests := []struct {
+ name string
+ flagName string
+ rawValue string
+ expected string
+ shouldErr bool
+ }{
+ {"LLC with M suffix", "llc", "336M", "336", false},
+ {"LLC with MB suffix", "llc", "336MB", "336", false},
+ {"TDP with W suffix", "tdp", "350W", "350", false},
+ {"Frequency with GHz suffix", "core-max", "3.2GHz", "3.2", false},
+ {"EPB with parentheses", "epb", "Performance (0)", "0", false},
+ {"EPB with text and parentheses", "epb", "Best Performance (15)", "15", false},
+ {"Governor lowercase", "gov", "performance", "performance", false},
+ {"Prefetcher enabled", "pref-l2hw", "Enabled", "enable", false},
+ {"Prefetcher disabled", "pref-l2hw", "Disabled", "disable", false},
+ {"C6 enabled", "c6", "Enabled", "enable", false},
+ {"ELC lowercase", "elc", "default", "default", false},
+ {"ELC capitalized", "elc", "Default", "default", false},
+ {"Core SSE freq buckets", "core-max-buckets", "1-44/3.6, 45-52/3.5, 53-60/3.4", "1-44/3.6, 45-52/3.5, 53-60/3.4", false},
+ {"Core SSE freq buckets full", "core-max-buckets", "1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0", "1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0", false},
+ {"Core SSE freq buckets invalid", "core-max-buckets", "invalid-format", "", true},
+ {"Inconsistent value", "epp", "inconsistent", "", true},
+ {"Unknown flag", "unknown-flag", "value", "", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := convertValue(tt.flagName, tt.rawValue)
+ if tt.shouldErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expected, result)
+ }
+ })
+ }
+}
+
+func TestParseNumericWithUnit(t *testing.T) {
+ tests := []struct {
+ name string
+ value string
+ units []string
+ expected string
+ shouldErr bool
+ }{
+ {"GHz unit", "3.2GHz", []string{"GHz"}, "3.2", false},
+ {"MB unit", "336MB", []string{"M", "MB"}, "336", false},
+ {"M unit", "336M", []string{"M", "MB"}, "336", false},
+ {"W unit", "350W", []string{"W"}, "350", false},
+ {"No unit but valid number", "350", []string{"W"}, "350", false},
+ {"Invalid number", "abc", []string{"W"}, "", true},
+ {"Missing unit", "350", []string{}, "350", false},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := parseNumericWithUnit(tt.value, tt.units...)
+ if tt.shouldErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expected, result)
+ }
+ })
+ }
+}
+
+func TestParseParenthesizedNumber(t *testing.T) {
+ tests := []struct {
+ name string
+ value string
+ expected string
+ shouldErr bool
+ }{
+ {"Simple parentheses", "Performance (0)", "0", false},
+ {"Multiple words", "Best Performance (15)", "15", false},
+ {"Two digit number", "Some Text (255)", "255", false},
+ {"No parentheses", "Performance", "", true},
+ {"Empty parentheses", "Performance ()", "", true},
+ {"Non-numeric", "Performance (abc)", "", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := parseParenthesizedNumber(tt.value)
+ if tt.shouldErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expected, result)
+ }
+ })
+ }
+}
+
+func TestParseEnableDisableOrOption(t *testing.T) {
+ tests := []struct {
+ name string
+ value string
+ validOptions []string
+ expected string
+ shouldErr bool
+ }{
+ {"Enabled to enable", "Enabled", []string{"enable", "disable"}, "enable", false},
+ {"Disabled to disable", "Disabled", []string{"enable", "disable"}, "disable", false},
+ {"Lowercase enable", "enable", []string{"enable", "disable"}, "enable", false},
+ {"Performance option", "performance", []string{"performance", "powersave"}, "performance", false},
+ {"Powersave option", "powersave", []string{"performance", "powersave"}, "powersave", false},
+ {"Invalid option", "invalid", []string{"enable", "disable"}, "", true},
+ {"Case insensitive", "ENABLED", []string{"enable", "disable"}, "enable", false},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := parseEnableDisableOrOption(tt.value, tt.validOptions)
+ if tt.shouldErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expected, result)
+ }
+ })
+ }
+}
+
+func TestParseAndPresentResults(t *testing.T) {
+ tests := []struct {
+ name string
+ stderrOutput string
+ flagValues []flagValue
+ expectedOutput []string // lines we expect to see in output
+ }{
+ {
+ name: "Example from function header comment",
+ stderrOutput: "configuration update complete: set cores to 86, set llc to 336, set tdp to 350, set core-max-buckets to 1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0, set epb to 6, set epp to 128, set gov to powersave, set elc to default, set uncore-max-compute to 2.2, set uncore-min-compute to 0.8, set uncore-max-io to 2.5, set uncore-min-io to 0.8, set pref-l2hw to enable, set pref-l2adj to enable, set pref-dcuhw to enable, set pref-dcuip to enable, set pref-dcunp to enable, set pref-amp to enable, set pref-llcpp to enable, set pref-aop to enable, set pref-homeless to enable, set pref-llc to disable, set c6 to enable, set c1-demotion to disable",
+ flagValues: []flagValue{
+ {fieldName: "Cores per Socket", flagName: "cores", value: "86"},
+ {fieldName: "L3 Cache", flagName: "llc", value: "336"},
+ {fieldName: "Package Power / TDP", flagName: "tdp", value: "350"},
+ {fieldName: "Core SSE Frequency", flagName: "core-max-buckets", value: "1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0"},
+ {fieldName: "Uncore Max Frequency (Compute)", flagName: "uncore-max-compute", value: "2.2"},
+ {fieldName: "Uncore Min Frequency (Compute)", flagName: "uncore-min-compute", value: "0.8"},
+ {fieldName: "Uncore Max Frequency (I/O)", flagName: "uncore-max-io", value: "2.5"},
+ {fieldName: "Uncore Min Frequency (I/O)", flagName: "uncore-min-io", value: "0.8"},
+ {fieldName: "Energy Performance Bias", flagName: "epb", value: "6"},
+ {fieldName: "Energy Performance Preference", flagName: "epp", value: "128"},
+ {fieldName: "Scaling Governor", flagName: "gov", value: "powersave"},
+ {fieldName: "Efficiency Latency Control", flagName: "elc", value: "default"},
+ {fieldName: "L2 HW prefetcher", flagName: "pref-l2hw", value: "enable"},
+ {fieldName: "L2 Adj prefetcher", flagName: "pref-l2adj", value: "enable"},
+ {fieldName: "DCU HW prefetcher", flagName: "pref-dcuhw", value: "enable"},
+ {fieldName: "DCU IP prefetcher", flagName: "pref-dcuip", value: "enable"},
+ {fieldName: "DCU NP prefetcher", flagName: "pref-dcunp", value: "enable"},
+ {fieldName: "AMP prefetcher", flagName: "pref-amp", value: "enable"},
+ {fieldName: "LLCPP prefetcher", flagName: "pref-llcpp", value: "enable"},
+ {fieldName: "AOP prefetcher", flagName: "pref-aop", value: "enable"},
+ {fieldName: "Homeless prefetcher", flagName: "pref-homeless", value: "enable"},
+ {fieldName: "LLC prefetcher", flagName: "pref-llc", value: "disable"},
+ {fieldName: "C6", flagName: "c6", value: "enable"},
+ {fieldName: "C1 Demotion", flagName: "c1-demotion", value: "disable"},
+ },
+ expectedOutput: []string{
+ "✓ Cores per Socket",
+ "✓ L3 Cache",
+ "✓ Package Power / TDP",
+ "✓ Core SSE Frequency",
+ "✓ Uncore Max Frequency (Compute)",
+ "✓ Uncore Min Frequency (Compute)",
+ "✓ Uncore Max Frequency (I/O)",
+ "✓ Uncore Min Frequency (I/O)",
+ "✓ Energy Performance Bias",
+ "✓ Energy Performance Preference",
+ "✓ Scaling Governor",
+ "✓ Efficiency Latency Control",
+ "✓ L2 HW prefetcher",
+ "✓ L2 Adj prefetcher",
+ "✓ DCU HW prefetcher",
+ "✓ DCU IP prefetcher",
+ "✓ DCU NP prefetcher",
+ "✓ AMP prefetcher",
+ "✓ LLCPP prefetcher",
+ "✓ AOP prefetcher",
+ "✓ Homeless prefetcher",
+ "✓ LLC prefetcher",
+ "✓ C6",
+ "✓ C1 Demotion",
+ },
+ },
+ {
+ name: "Empty stderr output",
+ stderrOutput: "",
+ flagValues: []flagValue{
+ {fieldName: "Cores per Socket", flagName: "cores", value: "86"},
+ },
+ expectedOutput: []string{}, // nothing should be printed
+ },
+ {
+ name: "Mixed success and error messages on separate lines",
+ stderrOutput: "gnr ⣾ preparing target\n" +
+ "gnr ⣽ configuration update complete: set cores to 86, failed to set llc to 336, set tdp to 350\n",
+ flagValues: []flagValue{
+ {fieldName: "Cores per Socket", flagName: "cores", value: "86"},
+ {fieldName: "L3 Cache", flagName: "llc", value: "336"},
+ {fieldName: "Package Power / TDP", flagName: "tdp", value: "350"},
+ },
+ expectedOutput: []string{
+ "✓ Cores per Socket",
+ "✗ L3 Cache",
+ "✓ Package Power / TDP",
+ },
+ },
+ {
+ name: "Flag name with multiple hyphens",
+ stderrOutput: "set uncore-max-compute to 2.2, set uncore-min-io to 0.8",
+ flagValues: []flagValue{
+ {fieldName: "Uncore Max Frequency (Compute)", flagName: "uncore-max-compute", value: "2.2"},
+ {fieldName: "Uncore Min Frequency (I/O)", flagName: "uncore-min-io", value: "0.8"},
+ },
+ expectedOutput: []string{
+ "✓ Uncore Max Frequency (Compute)",
+ "✓ Uncore Min Frequency (I/O)",
+ },
+ },
+ {
+ name: "No matching flags in output",
+ stderrOutput: "some other message without flag updates",
+ flagValues: []flagValue{
+ {fieldName: "Cores per Socket", flagName: "cores", value: "86"},
+ },
+ expectedOutput: []string{
+ "? Cores per Socket",
+ },
+ },
+ {
+ name: "Flag with underscore and numbers",
+ stderrOutput: "set pref_test123 to enable, failed to set flag_456 to disable",
+ flagValues: []flagValue{
+ {fieldName: "Pref Test", flagName: "pref_test123", value: "enable"},
+ {fieldName: "Flag 456", flagName: "flag_456", value: "disable"},
+ },
+ expectedOutput: []string{
+ "✓ Pref Test",
+ "✗ Flag 456",
+ },
+ },
+ {
+ name: "Core SSE freq buckets with commas in value",
+ stderrOutput: "set core-max-buckets to 1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0",
+ flagValues: []flagValue{
+ {fieldName: "Core SSE Frequency", flagName: "core-max-buckets", value: "1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0"},
+ },
+ expectedOutput: []string{
+ "✓ Core SSE Frequency",
+ },
+ },
+ {
+ name: "Some flags updated, others not mentioned",
+ stderrOutput: "set cores to 86, set tdp to 350",
+ flagValues: []flagValue{
+ {fieldName: "Cores per Socket", flagName: "cores", value: "86"},
+ {fieldName: "L3 Cache", flagName: "llc", value: "336"},
+ {fieldName: "Package Power / TDP", flagName: "tdp", value: "350"},
+ },
+ expectedOutput: []string{
+ "✓ Cores per Socket",
+ "? L3 Cache",
+ "✓ Package Power / TDP",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Capture stdout
+ oldStdout := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+
+ // Call the function
+ parseAndPresentResults(tt.stderrOutput, tt.flagValues)
+
+ // Restore stdout
+ w.Close()
+ os.Stdout = oldStdout
+
+ // Read captured output
+ var buf bytes.Buffer
+ _, _ = io.Copy(&buf, r)
+ output := buf.String()
+
+ // Verify expected output
+ if len(tt.expectedOutput) == 0 {
+ // Should be empty or just whitespace
+ if strings.TrimSpace(output) != "" {
+ t.Errorf("Expected no output, got: %q", output)
+ }
+ return
+ }
+
+ // Check that each expected line is in the output
+ for _, expected := range tt.expectedOutput {
+ if !strings.Contains(output, expected) {
+ t.Errorf("Expected output to contain %q, but it didn't.\nFull output:\n%s", expected, output)
+ }
+ }
+
+ // Verify the output contains "Configuration Results:" header
+ if !strings.Contains(output, "Configuration Results:") {
+ t.Errorf("Expected output to contain 'Configuration Results:' header")
+ }
+ })
+ }
+}
diff --git a/cmd/config/set.go b/cmd/config/set.go
index 6a9d82df..745193c0 100644
--- a/cmd/config/set.go
+++ b/cmd/config/set.go
@@ -22,7 +22,7 @@ import (
var uncoreDieFrequencyMutex sync.Mutex
var uncoreFrequencyMutex sync.Mutex
-func setCoreCount(cores int, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setCoreCount(cores int, myTarget target.Target, localTempDir string) error {
setScript := script.ScriptDefinition{
Name: "set core count",
ScriptTemplate: fmt.Sprintf(`
@@ -115,10 +115,10 @@ done
if err != nil {
err = fmt.Errorf("failed to set core count: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setLlcSize(desiredLlcSize float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setLlcSize(desiredLlcSize float64, myTarget target.Target, localTempDir string) error {
// get the data we need to set the LLC size
scripts := []script.ScriptDefinition{}
scripts = append(scripts, script.GetScriptByName(script.LscpuScriptName))
@@ -128,50 +128,42 @@ func setLlcSize(desiredLlcSize float64, myTarget target.Target, localTempDir str
scripts = append(scripts, script.GetScriptByName(script.L3CacheWayEnabledName))
outputs, err := script.RunScripts(myTarget, scripts, true, localTempDir, nil, "")
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to run scripts on target: %w", err)}
- return
+ return fmt.Errorf("failed to run scripts on target: %w", err)
}
uarch := report.UarchFromOutput(outputs)
cpu, err := cpus.GetCPUByMicroArchitecture(uarch)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get CPU by microarchitecture: %w", err)}
- return
+ return fmt.Errorf("failed to get CPU by microarchitecture: %w", err)
}
if cpu.CacheWayCount == 0 {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("cache way count is zero")}
- return
+ return fmt.Errorf("cache way count is zero")
}
maximumLlcSize, _, err := report.GetL3LscpuMB(outputs)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get maximum LLC size: %w", err)}
- return
+ return fmt.Errorf("failed to get maximum LLC size: %w", err)
}
currentLlcSize, _, err := report.GetL3MSRMB(outputs)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get current LLC size: %w", err)}
- return
+ return fmt.Errorf("failed to get current LLC size: %w", err)
}
if currentLlcSize == desiredLlcSize {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("LLC size is already set to %.2f MB", desiredLlcSize)}
- return
+ // return success
+ return nil
}
if desiredLlcSize > maximumLlcSize {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("LLC size is too large, maximum is %.2f MB", maximumLlcSize)}
- return
+ return fmt.Errorf("LLC size is too large, maximum is %.2f MB", maximumLlcSize)
}
// calculate the number of ways to set
cachePerWay := maximumLlcSize / float64(cpu.CacheWayCount)
waysToSet := int(math.Ceil(desiredLlcSize / cachePerWay))
if waysToSet > cpu.CacheWayCount {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("LLC size is too large, maximum is %.2f MB", maximumLlcSize)}
- return
+ return fmt.Errorf("LLC size is too large, maximum is %.2f MB", maximumLlcSize)
}
// set the LLC size
msrVal, err := util.Uint64FromNumLowerBits(waysToSet)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to convert waysToSet to uint64: %w", err)}
- return
+ return fmt.Errorf("failed to convert waysToSet to uint64: %w", err)
}
setScript := script.ScriptDefinition{
Name: "set LLC size",
@@ -185,31 +177,27 @@ func setLlcSize(desiredLlcSize float64, myTarget target.Target, localTempDir str
if err != nil {
err = fmt.Errorf("failed to set LLC size: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setSSEFrequency(sseFrequency float64, myTarget target.Target, localTempDir string) error {
targetFamily, err := myTarget.GetFamily()
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get target family: %w", err)}
- return
+ return fmt.Errorf("failed to get target family: %w", err)
}
targetModel, err := myTarget.GetModel()
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get target model: %w", err)}
- return
+ return fmt.Errorf("failed to get target model: %w", err)
}
targetVendor, err := myTarget.GetVendor()
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get target vendor: %w", err)}
- return
+ return fmt.Errorf("failed to get target vendor: %w", err)
}
if targetVendor != cpus.IntelVendor {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("core frequency setting not supported on %s due to vendor mismatch", myTarget.GetName())}
- return
+ return fmt.Errorf("core frequency setting not supported on %s due to vendor mismatch", myTarget.GetName())
}
var setScript script.ScriptDefinition
- freqInt := uint64(coreFrequency * 10)
+ freqInt := uint64(sseFrequency * 10)
if targetFamily == "6" && (targetModel == "175" || targetModel == "221") { // SRF, CWF
// get the pstate driver
getScript := script.ScriptDefinition{
@@ -219,14 +207,13 @@ func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDi
}
output, err := runScript(myTarget, getScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get pstate driver: %w", err)}
- return
+ return fmt.Errorf("failed to get pstate driver: %w", err)
}
if strings.Contains(output, "intel_pstate") {
var value uint64
var i uint
for i = range 2 {
- value = value | freqInt<= r.startCore && bucketMidpoint <= r.endCore {
+ bucketFrequencies[i] = r.freq
+ break
+ }
+ }
+ }
+
+ return bucketFrequencies, nil
+}
+
+// setSSEFrequencies sets the SSE frequencies for all core buckets
+// The input string should be in the format "start-end/freq", comma-separated
+// e.g., "1-40/3.5, 41-60/3.4, 61-86/3.2"
+// Note that the buckets have been consolidated where frequencies are the same, so they
+// will need to be expanded back out to individual buckets for setting.
+func setSSEFrequencies(sseFrequencies string, myTarget target.Target, localTempDir string) error {
+ targetFamily, err := myTarget.GetFamily()
+ if err != nil {
+ return fmt.Errorf("failed to get target family: %w", err)
+ }
+ targetModel, err := myTarget.GetModel()
+ if err != nil {
+ return fmt.Errorf("failed to get target model: %w", err)
+ }
+ targetVendor, err := myTarget.GetVendor()
+ if err != nil {
+ return fmt.Errorf("failed to get target vendor: %w", err)
+ }
+ if targetVendor != cpus.IntelVendor {
+ return fmt.Errorf("core frequency setting not supported on %s due to vendor mismatch", myTarget.GetName())
+ }
+
+ // retrieve the original frequency bucket sizes so that we can expand the consolidated input
+ output, err := runScript(myTarget, script.GetScriptByName(script.SpecCoreFrequenciesScriptName), localTempDir)
+ if err != nil {
+ return fmt.Errorf("failed to get original frequency buckets: %w", err)
+ }
+ // expected script output format, the number of fields may vary:
+ // "cores sse avx2 avx512 avx512h amx"
+ // "hex hex hex hex hex hex"
+
+ // confirm output format
+ lines := strings.Split(strings.TrimSpace(output), "\n")
+ if len(lines) < 2 {
+ return fmt.Errorf("unexpected output format from spec-core-frequencies script")
+ }
+ // extract the bucket sizes from the first field (cores) in the 2nd line
+ coreCountsHex := strings.Fields(lines[1])[0]
+ bucketSizes, err := util.HexToIntList(coreCountsHex)
+ if err != nil {
+ return fmt.Errorf("failed to parse core counts from hex: %w", err)
+ }
+ // there should be 8 buckets
+ if len(bucketSizes) != 8 {
+ return fmt.Errorf("unexpected number of core buckets: %d", len(bucketSizes))
+ }
+ // they are in reverse order, so reverse the slice
+ slices.Reverse(bucketSizes)
+
+ // expand the consolidated input into the 8 original bucket sizes
+ // archMultiplier is used to adjust core numbering for certain architectures, i.e., multiply core numbers by 2, 3, or 4.
+ uarch, err := getUarch(myTarget, localTempDir)
+ if err != nil {
+ return fmt.Errorf("failed to get microarchitecture: %w", err)
+ }
+ var archMultiplier int
+ if strings.Contains(uarch, "SRF") || strings.Contains(uarch, "CWF") {
+ archMultiplier = 4
+ } else if strings.Contains(uarch, "GNR_X3") {
+ archMultiplier = 3
+ } else if strings.Contains(uarch, "GNR_X2") {
+ archMultiplier = 2
+ } else {
+ archMultiplier = 1
+ }
+ if archMultiplier == 0 {
+ return fmt.Errorf("unsupported microarchitecture for SSE frequency setting: %s", uarch)
+ }
+ adjustedBucketSizes := make([]int, len(bucketSizes))
+ for i, size := range bucketSizes {
+ adjustedBucketSizes[i] = size * archMultiplier
+ }
+
+ bucketFrequencies, err := expandConsolidatedFrequencies(sseFrequencies, adjustedBucketSizes)
+ if err != nil {
+ return fmt.Errorf("failed to expand consolidated frequencies: %w", err)
+ }
+
+ // Now set the frequencies using the same approach as setSSEFrequency
+ var setScript script.ScriptDefinition
+
+ if targetFamily == "6" && (targetModel == "175" || targetModel == "221") { // SRF, CWF
+ // get the pstate driver
+ getScript := script.ScriptDefinition{
+ Name: "get pstate driver",
+ ScriptTemplate: "cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver",
+ Vendors: []string{cpus.IntelVendor},
+ }
+ output, err := runScript(myTarget, getScript, localTempDir)
+ if err != nil {
+ return fmt.Errorf("failed to get pstate driver: %w", err)
+ }
+ if strings.Contains(output, "intel_pstate") {
+ // For SRF/CWF with intel_pstate, we only set 2 buckets
+ var value uint64
+ for i := range uint(2) {
+ freqInt := uint64(bucketFrequencies[i] * 10)
+ value = value | freqInt<<(i*8)
+ }
+ setScript = script.ScriptDefinition{
+ Name: "set frequency bins",
+ ScriptTemplate: fmt.Sprintf("wrmsr 0x774 %d", value),
+ Superuser: true,
+ Vendors: []string{cpus.IntelVendor},
+ }
+ } else {
+ // For non-intel_pstate driver
+ freqInt := uint64(bucketFrequencies[0] * 10)
+ value := freqInt << uint(2*8)
+ setScript = script.ScriptDefinition{
+ Name: "set frequency bins",
+ ScriptTemplate: fmt.Sprintf("wrmsr 0x199 %d", value),
+ Superuser: true,
+ Vendors: []string{cpus.IntelVendor},
+ }
+ }
+ } else {
+ // For other platforms, set all 8 buckets
+ var value uint64
+ for i := range uint(8) {
+ freqInt := uint64(bucketFrequencies[i] * 10)
+ value = value | freqInt<<(i*8)
+ }
+ setScript = script.ScriptDefinition{
+ Name: "set frequency bins",
+ ScriptTemplate: fmt.Sprintf("wrmsr -a 0x1AD %d", value),
+ Superuser: true,
+ Vendors: []string{cpus.IntelVendor},
+ }
+ }
+
+ _, err = runScript(myTarget, setScript, localTempDir)
+ if err != nil {
+ err = fmt.Errorf("failed to set core frequencies: %w", err)
+ }
+ return err
+}
+
+func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float64, myTarget target.Target, localTempDir string) error {
// Acquire mutex lock to protect concurrent access
uncoreDieFrequencyMutex.Lock()
defer uncoreDieFrequencyMutex.Unlock()
targetFamily, err := myTarget.GetFamily()
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get target family: %w", err)}
- return
+ return fmt.Errorf("failed to get target family: %w", err)
}
targetModel, err := myTarget.GetModel()
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get target model: %w", err)}
- return
+ return fmt.Errorf("failed to get target model: %w", err)
}
if targetFamily != "6" || (targetFamily == "6" && targetModel != "173" && targetModel != "174" && targetModel != "175" && targetModel != "221") { // not Intel || not GNR, GNR-D, SRF, CWF
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("uncore frequency setting not supported on %s due to family/model mismatch", myTarget.GetName())}
- return
+ return fmt.Errorf("uncore frequency setting not supported on %s due to family/model mismatch", myTarget.GetName())
}
type dieId struct {
instance string
@@ -298,8 +498,7 @@ func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float6
scripts = append(scripts, script.GetScriptByName(script.UncoreDieTypesFromTPMIScriptName))
outputs, err := script.RunScripts(myTarget, scripts, true, localTempDir, nil, "")
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to run scripts on target: %w", err)}
- return
+ return fmt.Errorf("failed to run scripts on target: %w", err)
}
re := regexp.MustCompile(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry (\d+) in instance (\d+)`)
for line := range strings.SplitSeq(outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout, "\n") {
@@ -317,30 +516,36 @@ func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float6
value := uint64(uncoreFrequency * 10)
var bits string
+ var freqType string
if maxFreq {
bits = "8:14" // bits 8:14 are the max frequency
+ freqType = "max"
} else {
bits = "15:21" // bits 15:21 are the min frequency
+ freqType = "min"
}
// run script for each die of specified type
+ scripts = []script.ScriptDefinition{}
for _, die := range dies {
setScript := script.ScriptDefinition{
- Name: "write max and min uncore frequency TPMI",
+ Name: fmt.Sprintf("write %s uncore frequency TPMI %s %s", freqType, die.instance, die.entry),
ScriptTemplate: fmt.Sprintf("pcm-tpmi 2 0x18 -d -b %s -w %d -i %s -e %s", bits, value, die.instance, die.entry),
Vendors: []string{cpus.IntelVendor},
Depends: []string{"pcm-tpmi"},
Superuser: true,
+ Sequential: true,
}
- _, err = runScript(myTarget, setScript, localTempDir)
- if err != nil {
- err = fmt.Errorf("failed to set uncore die frequency: %w", err)
- break
- }
+ scripts = append(scripts, setScript)
+ }
+ _, err = script.RunScripts(myTarget, scripts, false, localTempDir, nil, "")
+ if err != nil {
+ err = fmt.Errorf("failed to set uncore die frequency: %w", err)
+ slog.Error(err.Error())
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setUncoreFrequency(maxFreq bool, uncoreFrequency float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setUncoreFrequency(maxFreq bool, uncoreFrequency float64, myTarget target.Target, localTempDir string) error {
// Acquire mutex lock to protect concurrent access
uncoreFrequencyMutex.Lock()
defer uncoreFrequencyMutex.Unlock()
@@ -356,27 +561,22 @@ func setUncoreFrequency(maxFreq bool, uncoreFrequency float64, myTarget target.T
})
outputs, err := script.RunScripts(myTarget, scripts, true, localTempDir, nil, "")
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to run scripts on target: %w", err)}
- return
+ return fmt.Errorf("failed to run scripts on target: %w", err)
}
targetFamily, err := myTarget.GetFamily()
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get target family: %w", err)}
- return
+ return fmt.Errorf("failed to get target family: %w", err)
}
targetModel, err := myTarget.GetModel()
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get target model: %w", err)}
- return
+ return fmt.Errorf("failed to get target model: %w", err)
}
if targetFamily != "6" || (targetFamily == "6" && (targetModel == "173" || targetModel == "174" || targetModel == "175" || targetModel == "221")) { // not Intel || not GNR, GNR-D, SRF, CWF
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("uncore frequency setting not supported on %s due to family/model mismatch", myTarget.GetName())}
- return
+ return fmt.Errorf("uncore frequency setting not supported on %s due to family/model mismatch", myTarget.GetName())
}
msrUint, err := strconv.ParseUint(strings.TrimSpace(outputs["get uncore frequency MSR"].Stdout), 16, 0)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to parse uncore frequency MSR: %w", err)}
- return
+ return fmt.Errorf("failed to parse uncore frequency MSR: %w", err)
}
newFreq := uint64((uncoreFrequency * 1000) / 100)
var newVal uint64
@@ -403,10 +603,10 @@ func setUncoreFrequency(maxFreq bool, uncoreFrequency float64, myTarget target.T
if err != nil {
err = fmt.Errorf("failed to set uncore frequency: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setTDP(power int, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setTDP(power int, myTarget target.Target, localTempDir string) error {
readScript := script.ScriptDefinition{
Name: "get power MSR",
ScriptTemplate: "rdmsr 0x610",
@@ -417,14 +617,12 @@ func setTDP(power int, myTarget target.Target, localTempDir string, completeChan
}
readOutput, err := script.RunScript(myTarget, readScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to read power MSR: %w", err)}
- return
+ return fmt.Errorf("failed to read power MSR: %w", err)
} else {
msrHex := strings.TrimSpace(readOutput.Stdout)
msrUint, err := strconv.ParseUint(msrHex, 16, 0)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to parse power MSR: %w", err)}
- return
+ return fmt.Errorf("failed to parse power MSR: %w", err)
} else {
// mask out lower 14 bits
newVal := uint64(msrUint) & 0xFFFFFFFFFFFFC000
@@ -440,26 +638,23 @@ func setTDP(power int, myTarget target.Target, localTempDir string, completeChan
}
_, err := runScript(myTarget, setScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to set power: %w", err)}
- return
+ return fmt.Errorf("failed to set power: %w", err)
}
}
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: nil}
+ return nil
}
-func setEPB(epb int, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setEPB(epb int, myTarget target.Target, localTempDir string) error {
epbSourceScript := script.GetScriptByName(script.EpbSourceScriptName)
epbSourceOutput, err := runScript(myTarget, epbSourceScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get EPB source: %w", err)}
- return
+ return fmt.Errorf("failed to get EPB source: %w", err)
}
epbSource := strings.TrimSpace(epbSourceOutput)
source, err := strconv.ParseInt(epbSource, 16, 0)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to parse EPB source: %w", err)}
- return
+ return fmt.Errorf("failed to parse EPB source: %w", err)
}
var msr string
var bitOffset uint
@@ -480,13 +675,11 @@ func setEPB(epb int, myTarget target.Target, localTempDir string, completeChanne
}
readOutput, err := runScript(myTarget, readScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to read EPB MSR %s: %w", msr, err)}
- return
+ return fmt.Errorf("failed to read EPB MSR %s: %w", msr, err)
}
msrValue, err := strconv.ParseUint(strings.TrimSpace(readOutput), 16, 64)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to parse EPB MSR %s: %w", msr, err)}
- return
+ return fmt.Errorf("failed to parse EPB MSR %s: %w", msr, err)
}
// mask out 4 bits starting at bitOffset
maskedValue := msrValue &^ (0xF << bitOffset)
@@ -505,10 +698,10 @@ func setEPB(epb int, myTarget target.Target, localTempDir string, completeChanne
if err != nil {
err = fmt.Errorf("failed to set EPB: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setEPP(epp int, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setEPP(epp int, myTarget target.Target, localTempDir string) error {
// Set both the per-core EPP value and the package EPP value
// Reference: 15.4.4 Managing HWP in the Intel SDM
@@ -523,13 +716,11 @@ func setEPP(epp int, myTarget target.Target, localTempDir string, completeChanne
}
stdout, err := runScript(myTarget, getScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to read EPP MSR %s: %w", "0x774", err)}
- return
+ return fmt.Errorf("failed to read EPP MSR %s: %w", "0x774", err)
}
msrValue, err := strconv.ParseUint(strings.TrimSpace(stdout), 16, 64)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to parse EPP MSR %s: %w", "0x774", err)}
- return
+ return fmt.Errorf("failed to parse EPP MSR %s: %w", "0x774", err)
}
// mask out bits 24-31 IA32_HWP_REQUEST MSR value
maskedValue := msrValue & 0xFFFFFFFF00FFFFFF
@@ -546,8 +737,7 @@ func setEPP(epp int, myTarget target.Target, localTempDir string, completeChanne
}
_, err = runScript(myTarget, setScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to set EPP: %w", err)}
- return
+ return fmt.Errorf("failed to set EPP: %w", err)
}
// get the current value of the IA32_HWP_REQUEST_PKG MSR that includes the current package EPP value
getScript = script.ScriptDefinition{
@@ -560,13 +750,11 @@ func setEPP(epp int, myTarget target.Target, localTempDir string, completeChanne
}
stdout, err = runScript(myTarget, getScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to read EPP pkg MSR %s: %w", "0x772", err)}
- return
+ return fmt.Errorf("failed to read EPP pkg MSR %s: %w", "0x772", err)
}
msrValue, err = strconv.ParseUint(strings.TrimSpace(stdout), 16, 64)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to parse EPP pkg MSR %s: %w", "0x772", err)}
- return
+ return fmt.Errorf("failed to parse EPP pkg MSR %s: %w", "0x772", err)
}
// mask out bits 24-31 IA32_HWP_REQUEST_PKG MSR value
maskedValue = msrValue & 0xFFFFFFFF00FFFFFF
@@ -585,10 +773,10 @@ func setEPP(epp int, myTarget target.Target, localTempDir string, completeChanne
if err != nil {
err = fmt.Errorf("failed to set EPP pkg: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setGovernor(governor string, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setGovernor(governor string, myTarget target.Target, localTempDir string) error {
setScript := script.ScriptDefinition{
Name: "set governor",
ScriptTemplate: fmt.Sprintf("echo %s | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor", governor),
@@ -598,10 +786,10 @@ func setGovernor(governor string, myTarget target.Target, localTempDir string, c
if err != nil {
err = fmt.Errorf("failed to set governor: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setELC(elc string, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setELC(elc string, myTarget target.Target, localTempDir string) error {
var mode string
switch elc {
case elcOptions[0]:
@@ -609,8 +797,7 @@ func setELC(elc string, myTarget target.Target, localTempDir string, completeCha
case elcOptions[1]:
mode = "default"
default:
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("invalid ELC mode: %s", elc)}
- return
+ return fmt.Errorf("invalid ELC mode: %s", elc)
}
setScript := script.ScriptDefinition{
Name: "set elc",
@@ -624,35 +811,37 @@ func setELC(elc string, myTarget target.Target, localTempDir string, completeCha
if err != nil {
err = fmt.Errorf("failed to set ELC mode: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir string, prefetcherType string, completeChannel chan setOutput, goRoutineId int) {
- pf, err := report.GetPrefetcherDefByName(prefetcherType)
- if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get prefetcher definition: %w", err)}
- return
- }
- // check if the prefetcher is supported on this target's architecture
- // get the uarch
+func getUarch(myTarget target.Target, localTempDir string) (string, error) {
scripts := []script.ScriptDefinition{}
scripts = append(scripts, script.GetScriptByName(script.LscpuScriptName))
scripts = append(scripts, script.GetScriptByName(script.LspciBitsScriptName))
scripts = append(scripts, script.GetScriptByName(script.LspciDevicesScriptName))
outputs, err := script.RunScripts(myTarget, scripts, true, localTempDir, nil, "")
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to run scripts on target: %w", err)}
- return
+ return "", fmt.Errorf("failed to run scripts on target: %w", err)
}
uarch := report.UarchFromOutput(outputs)
if uarch == "" {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get microarchitecture")}
- return
+ return "", fmt.Errorf("failed to get microarchitecture")
+ }
+ return uarch, nil
+}
+
+func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir string, prefetcherType string) error {
+ pf, err := report.GetPrefetcherDefByName(prefetcherType)
+ if err != nil {
+ return fmt.Errorf("failed to get prefetcher definition: %w", err)
+ }
+ uarch, err := getUarch(myTarget, localTempDir)
+ if err != nil {
+ return fmt.Errorf("failed to get microarchitecture: %w", err)
}
// is the prefetcher supported on this uarch?
if !slices.Contains(pf.Uarchs, "all") && !slices.Contains(pf.Uarchs, uarch[:3]) {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("prefetcher %s is not supported on %s", prefetcherType, uarch)}
- return
+ return fmt.Errorf("prefetcher %s is not supported on %s", prefetcherType, uarch)
}
// get the current value of the prefetcher MSR
getScript := script.ScriptDefinition{
@@ -665,13 +854,11 @@ func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir st
}
stdout, err := runScript(myTarget, getScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to read prefetcher MSR: %w", err)}
- return
+ return fmt.Errorf("failed to read prefetcher MSR: %w", err)
}
msrValue, err := strconv.ParseUint(strings.TrimSpace(stdout), 16, 64)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to parse prefetcher MSR: %w", err)}
- return
+ return fmt.Errorf("failed to parse prefetcher MSR: %w", err)
}
// set the prefetcher bit to bitValue determined by the onOff value, note: 0 is enable, 1 is disable
var bitVal uint64
@@ -681,8 +868,7 @@ func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir st
case prefetcherOptions[1]:
bitVal = 1
default:
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("invalid prefetcher setting: %s", enableDisable)}
- return
+ return fmt.Errorf("invalid prefetcher setting: %s", enableDisable)
}
// mask out the prefetcher bit
maskedValue := msrValue &^ (1 << pf.Bit)
@@ -701,11 +887,11 @@ func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir st
if err != nil {
err = fmt.Errorf("failed to set %s prefetcher: %w", prefetcherType, err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
// setC6 enables or disables C6 C-States
-func setC6(enableDisable string, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setC6(enableDisable string, myTarget target.Target, localTempDir string) error {
getScript := script.ScriptDefinition{
Name: "get C6 state folder names",
ScriptTemplate: `# This script finds the states of the CPU that include "C6" in their name
@@ -724,13 +910,11 @@ fi
}
stdout, err := runScript(myTarget, getScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get C6 state folders: %w", err)}
- return
+ return fmt.Errorf("failed to get C6 state folders: %w", err)
}
c6StateFolders := strings.Split(strings.TrimSpace(stdout), "\n")
if len(c6StateFolders) == 0 {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("no C6 state folders found")}
- return
+ return fmt.Errorf("no C6 state folders found")
}
var enableDisableValue int
switch enableDisable {
@@ -739,8 +923,7 @@ fi
case c6Options[1]: // disable
enableDisableValue = 1
default:
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("invalid C6 setting: %s", enableDisable)}
- return
+ return fmt.Errorf("invalid C6 setting: %s", enableDisable)
}
bash := "for cpu in /sys/devices/system/cpu/cpu[0-9]*; do\n"
for _, folder := range c6StateFolders {
@@ -756,10 +939,10 @@ fi
if err != nil {
err = fmt.Errorf("failed to set C6: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
-func setC1Demotion(enableDisable string, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) {
+func setC1Demotion(enableDisable string, myTarget target.Target, localTempDir string) error {
getScript := script.ScriptDefinition{
Name: "get C1 demotion",
ScriptTemplate: "rdmsr 0xe2",
@@ -770,13 +953,11 @@ func setC1Demotion(enableDisable string, myTarget target.Target, localTempDir st
}
stdout, err := runScript(myTarget, getScript, localTempDir)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get C1 demotion: %w", err)}
- return
+ return fmt.Errorf("failed to get C1 demotion: %w", err)
}
msrValue, err := strconv.ParseUint(strings.TrimSpace(stdout), 16, 64)
if err != nil {
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to parse C1 demotion MSR: %w", err)}
- return
+ return fmt.Errorf("failed to parse C1 demotion MSR: %w", err)
}
// set the c1 demotion bits to bitValue, note: 1 is enable, 0 is disable
var bitVal uint64
@@ -786,8 +967,7 @@ func setC1Demotion(enableDisable string, myTarget target.Target, localTempDir st
case c1DemotionOptions[1]: // disable
bitVal = 0
default:
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("invalid C1 demotion setting: %s", enableDisable)}
- return
+ return fmt.Errorf("invalid C1 demotion setting: %s", enableDisable)
}
// mask out the C1 demotion bits (26 and 28)
maskedValue := msrValue &^ (1 << 26)
@@ -807,7 +987,7 @@ func setC1Demotion(enableDisable string, myTarget target.Target, localTempDir st
if err != nil {
err = fmt.Errorf("failed to set C1 demotion: %w", err)
}
- completeChannel <- setOutput{goRoutineID: goRoutineId, err: err}
+ return err
}
// runScript runs a script on the target and returns the output
diff --git a/cmd/config/set_test.go b/cmd/config/set_test.go
new file mode 100644
index 00000000..218f1642
--- /dev/null
+++ b/cmd/config/set_test.go
@@ -0,0 +1,159 @@
+package config
+
+// Copyright (C) 2021-2025 Intel Corporation
+// SPDX-License-Identifier: BSD-3-Clause
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestExpandConsolidatedFrequencies(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ bucketSizes []int
+ expected []float64
+ expectError bool
+ errorContains string
+ }{
+ {
+ name: "three consolidated ranges",
+ input: "1-40/3.5, 41-60/3.4, 61-86/3.2",
+ bucketSizes: []int{20, 40, 60, 80, 86, 86, 86, 86},
+ expected: []float64{3.5, 3.5, 3.4, 3.2, 3.2, 3.2, 3.2, 3.2},
+ expectError: false,
+ },
+ {
+ name: "two consolidated ranges",
+ input: "1-43/3.5, 44-86/3.2",
+ bucketSizes: []int{20, 40, 60, 80, 86, 86, 86, 86},
+ expected: []float64{3.5, 3.5, 3.2, 3.2, 3.2, 3.2, 3.2, 3.2},
+ expectError: false,
+ },
+ {
+ name: "eight separate ranges (no consolidation)",
+ input: "1-20/3.5, 21-40/3.4, 41-60/3.3, 61-80/3.2, 81-82/3.1, 83-84/3.0, 85-86/2.9, 87-88/2.8",
+ bucketSizes: []int{20, 40, 60, 80, 82, 84, 86, 88},
+ expected: []float64{3.5, 3.4, 3.3, 3.2, 3.1, 3.0, 2.9, 2.8},
+ expectError: false,
+ },
+ {
+ name: "single consolidated range",
+ input: "1-86/3.5",
+ bucketSizes: []int{20, 40, 60, 80, 86, 86, 86, 86},
+ expected: []float64{3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5},
+ expectError: false,
+ },
+ {
+ name: "decimal frequencies",
+ input: "1-50/3.75, 51-86/2.25",
+ bucketSizes: []int{20, 40, 60, 80, 86, 86, 86, 86},
+ expected: []float64{3.75, 3.75, 3.75, 2.25, 2.25, 2.25, 2.25, 2.25},
+ expectError: false,
+ },
+ {
+ name: "wrong number of bucket sizes",
+ input: "1-40/3.5, 41-60/3.4",
+ bucketSizes: []int{20, 40, 60, 80},
+ expected: nil,
+ expectError: true,
+ errorContains: "expected 8 bucket sizes",
+ },
+ {
+ name: "invalid format - missing slash",
+ input: "1-40 3.5, 41-60/3.4",
+ bucketSizes: []int{20, 40, 60, 80, 86, 86, 86, 86},
+ expected: nil,
+ expectError: true,
+ errorContains: "invalid format",
+ },
+ {
+ name: "invalid format - missing dash in range",
+ input: "1/3.5, 41-60/3.4",
+ bucketSizes: []int{20, 40, 60, 80, 86, 86, 86, 86},
+ expected: nil,
+ expectError: true,
+ errorContains: "invalid range format",
+ },
+ {
+ name: "invalid frequency value",
+ input: "1-40/abc, 41-60/3.4",
+ bucketSizes: []int{20, 40, 60, 80, 86, 86, 86, 86},
+ expected: nil,
+ expectError: true,
+ errorContains: "invalid frequency",
+ },
+ {
+ name: "invalid core number",
+ input: "1-abc/3.5, 41-60/3.4",
+ bucketSizes: []int{20, 40, 60, 80, 86, 86, 86, 86},
+ expected: nil,
+ expectError: true,
+ errorContains: "invalid end core",
+ },
+ {
+ name: "six consolidated ranges with smaller bucket sizes",
+ input: "1-44/3.6, 45-52/3.5, 53-60/3.4, 61-72/3.2, 73-76/3.1, 77-86/3.0",
+ bucketSizes: []int{22, 26, 30, 34, 36, 38, 40, 43},
+ expected: []float64{3.6, 3.6, 3.6, 3.6, 3.6, 3.6, 3.6, 3.6},
+ expectError: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := expandConsolidatedFrequencies(tt.input, tt.bucketSizes)
+
+ if tt.expectError {
+ require.Error(t, err)
+ if tt.errorContains != "" {
+ assert.Contains(t, err.Error(), tt.errorContains)
+ }
+ assert.Nil(t, result)
+ } else {
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ assert.Equal(t, len(tt.expected), len(result), "result should have 8 frequencies")
+ for i := range tt.expected {
+ assert.InDelta(t, tt.expected[i], result[i], 0.01, "frequency at index %d should match", i)
+ }
+ }
+ })
+ }
+}
+
+func TestExpandConsolidatedFrequencies_EdgeCases(t *testing.T) {
+ t.Run("buckets with same end values", func(t *testing.T) {
+ // Some buckets may have the same end value (e.g., when there are fewer active buckets)
+ input := "1-60/3.5, 61-86/3.2"
+ bucketSizes := []int{20, 40, 60, 86, 86, 86, 86, 86}
+ expected := []float64{3.5, 3.5, 3.5, 3.2, 3.2, 3.2, 3.2, 3.2}
+
+ result, err := expandConsolidatedFrequencies(input, bucketSizes)
+
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ assert.Equal(t, len(expected), len(result))
+ for i := range expected {
+ assert.InDelta(t, expected[i], result[i], 0.01)
+ }
+ })
+
+ t.Run("very small buckets", func(t *testing.T) {
+ input := "1-2/3.5, 3-4/3.4, 5-6/3.3, 7-8/3.2, 9-10/3.1, 11-12/3.0, 13-14/2.9, 15-16/2.8"
+ bucketSizes := []int{2, 4, 6, 8, 10, 12, 14, 16}
+ expected := []float64{3.5, 3.4, 3.3, 3.2, 3.1, 3.0, 2.9, 2.8}
+
+ result, err := expandConsolidatedFrequencies(input, bucketSizes)
+
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ assert.Equal(t, len(expected), len(result))
+ for i := range expected {
+ assert.InDelta(t, expected[i], result[i], 0.01)
+ }
+ })
+}
diff --git a/internal/common/targets.go b/internal/common/targets.go
index 94b44d3a..30ab6026 100644
--- a/internal/common/targets.go
+++ b/internal/common/targets.go
@@ -121,7 +121,20 @@ func ValidateTargetFlags(cmd *cobra.Command) error {
// GetTargets retrieves the list of targets based on the provided command and parameters. It creates
// a temporary directory for each target and returns a slice of target.Target objects.
func GetTargets(cmd *cobra.Command, needsElevatedPrivileges bool, failIfCantElevate bool, localTempDir string) (targets []target.Target, targetErrs []error, err error) {
- targetTempDirRoot := cmd.Parent().PersistentFlags().Lookup("tempdir").Value.String()
+ tempDirFlag := cmd.Parent().PersistentFlags().Lookup("tempdir")
+ if tempDirFlag == nil {
+ // try grand-parent command (in case this is a subcommand)
+ grandParent := cmd.Parent().Parent()
+ if grandParent != nil {
+ tempDirFlag = grandParent.PersistentFlags().Lookup("tempdir")
+ }
+ }
+ if tempDirFlag == nil {
+ err = fmt.Errorf("failed to find 'tempdir' persistent flag")
+ slog.Error(err.Error())
+ return
+ }
+ targetTempDirRoot := tempDirFlag.Value.String()
flagTargetsFile, _ := cmd.Flags().GetString(flagTargetsFileName)
if flagTargetsFile != "" {
targets, targetErrs, err = getTargetsFromFile(flagTargetsFile, localTempDir)
diff --git a/internal/report/table_defs.go b/internal/report/table_defs.go
index 36db59c0..22377d46 100644
--- a/internal/report/table_defs.go
+++ b/internal/report/table_defs.go
@@ -2184,7 +2184,7 @@ func configurationTableValues(outputs map[string]script.ScriptOutput) []Field {
{Name: "Cores per Socket", Description: "--cores ", Values: []string{valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Core\(s\) per socket:\s*(.+)$`)}},
{Name: "L3 Cache", Description: "--llc ", Values: []string{l3InstanceFromOutput(outputs)}},
{Name: "Package Power / TDP", Description: "--tdp ", Values: []string{tdpFromOutput(outputs)}},
- {Name: "All-Core Max Frequency", Description: "--core-max ", Values: []string{allCoreMaxFrequencyFromOutput(outputs)}},
+ {Name: "Core SSE Frequency", Description: "--core-max ", Values: []string{sseFrequenciesFromOutput(outputs)}},
}
if strings.Contains(uarch, "SRF") || strings.Contains(uarch, "GNR") || strings.Contains(uarch, "CWF") {
fields = append(fields, []Field{
diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go
index d488fbb7..4aee27ad 100644
--- a/internal/report/table_helpers.go
+++ b/internal/report/table_helpers.go
@@ -153,326 +153,6 @@ func UarchFromOutput(outputs map[string]script.ScriptOutput) string {
return ""
}
-// baseFrequencyFromOutput gets base core frequency
-//
-// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/base_frequency
-// 2nd option) from dmidecode "Current Speed"
-// 3nd option) parse it from the model name
-func baseFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
- cmdout := strings.TrimSpace(outputs[script.BaseFrequencyScriptName].Stdout)
- if cmdout != "" {
- freqf, err := strconv.ParseFloat(cmdout, 64)
- if err == nil {
- freqf = freqf / 1000000
- return fmt.Sprintf("%.1fGHz", freqf)
- }
- }
- currentSpeedVal := valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Current Speed:\s(.*)$`)
- tokens := strings.Split(currentSpeedVal, " ")
- if len(tokens) == 2 {
- num, err := strconv.ParseFloat(tokens[0], 64)
- if err == nil {
- unit := tokens[1]
- if unit == "MHz" {
- num = num / 1000
- unit = "GHz"
- }
- return fmt.Sprintf("%.1f%s", num, unit)
- }
- }
- // the frequency (if included) is at the end of the model name in lscpu's output
- modelName := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^[Mm]odel name.*:\s*(.+?)$`)
- tokens = strings.Split(modelName, " ")
- if len(tokens) > 0 {
- lastToken := tokens[len(tokens)-1]
- if len(lastToken) > 0 && lastToken[len(lastToken)-1] == 'z' {
- return lastToken
- }
- }
- return ""
-}
-
-// getFrequenciesFromHex
-func getFrequenciesFromHex(hex string) ([]int, error) {
- freqs, err := util.HexToIntList(hex)
- if err != nil {
- return nil, err
- }
- // reverse the order of the frequencies
- slices.Reverse(freqs)
- return freqs, nil
-}
-
-// getBucketSizesFromHex
-func getBucketSizesFromHex(hex string) ([]int, error) {
- bucketSizes, err := util.HexToIntList(hex)
- if err != nil {
- return nil, err
- }
- if len(bucketSizes) != 8 {
- err = fmt.Errorf("expected 8 bucket sizes, got %d", len(bucketSizes))
- return nil, err
- }
- // reverse the order of the core counts
- slices.Reverse(bucketSizes)
- return bucketSizes, nil
-}
-
-// padFrequencies adds items to the frequencies slice until it reaches the desired length.
-// The value of the added items is the same as the last item in the original slice.
-func padFrequencies(freqs []int, desiredLength int) ([]int, error) {
- if len(freqs) == 0 {
- return nil, fmt.Errorf("cannot pad empty frequencies slice")
- }
- for len(freqs) < desiredLength {
- freqs = append(freqs, freqs[len(freqs)-1])
- }
- return freqs, nil
-}
-
-// getSpecFrequencyBuckets
-// returns slice of rows
-// first row is header
-// each row is a slice of strings
-// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx"
-// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1"
-// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1"
-// "64-85", "32-43", "3.5", "3.5", "3.3", "3.2", "3.1"
-// ...
-// the "cores per die" column is only present for some architectures
-func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string, error) {
- arch := UarchFromOutput(outputs)
- if arch == "" {
- return nil, fmt.Errorf("uarch is required")
- }
- out := outputs[script.SpecCoreFrequenciesScriptName].Stdout
- // expected script output format, the number of fields may vary:
- // "cores sse avx2 avx512 avx512h amx"
- // "hex hex hex hex hex hex"
- if out == "" {
- return nil, fmt.Errorf("no core frequencies found")
- }
- lines := strings.Split(out, "\n")
- if len(lines) < 2 {
- return nil, fmt.Errorf("unexpected output format")
- }
- fieldNames := strings.Fields(lines[0])
- if len(fieldNames) < 2 {
- return nil, fmt.Errorf("unexpected output format")
- }
- values := strings.Fields(lines[1])
- if len(values) != len(fieldNames) {
- return nil, fmt.Errorf("unexpected output format")
- }
- // get list of buckets sizes
- bucketCoreCounts, err := getBucketSizesFromHex(values[0])
- if err != nil {
- return nil, fmt.Errorf("failed to get bucket sizes from Hex string: %w", err)
- }
- // create buckets
- var totalCoreBuckets []string // only for multi-die architectures
- var dieCoreBuckets []string
- totalCoreStartRange := 1
- startRange := 1
- var archMultiplier int
- if strings.Contains(arch, "SRF") || strings.Contains(arch, "CWF") {
- archMultiplier = 4
- } else if strings.Contains(arch, "GNR_X3") {
- archMultiplier = 3
- } else if strings.Contains(arch, "GNR_X2") {
- archMultiplier = 2
- } else {
- archMultiplier = 1
- }
- for _, count := range bucketCoreCounts {
- if startRange > count {
- break
- }
- if archMultiplier > 1 {
- totalCoreCount := count * archMultiplier
- if totalCoreStartRange > int(totalCoreCount) {
- break
- }
- totalCoreBuckets = append(totalCoreBuckets, fmt.Sprintf("%d-%d", totalCoreStartRange, totalCoreCount))
- totalCoreStartRange = int(totalCoreCount) + 1
- }
- dieCoreBuckets = append(dieCoreBuckets, fmt.Sprintf("%d-%d", startRange, count))
- startRange = int(count) + 1
- }
- // get the frequencies for each isa
- var allIsaFreqs [][]string
- for _, isaHex := range values[1:] {
- var isaFreqs []string
- var freqs []int
- if isaHex != "0" {
- var err error
- freqs, err = getFrequenciesFromHex(isaHex)
- if err != nil {
- return nil, fmt.Errorf("failed to get frequencies from Hex string: %w", err)
- }
- } else {
- // if the ISA is not supported, set the frequency to zero for all buckets
- freqs = make([]int, len(bucketCoreCounts))
- for i := range freqs {
- freqs[i] = 0
- }
- }
- if len(freqs) != len(bucketCoreCounts) {
- freqs, err = padFrequencies(freqs, len(bucketCoreCounts))
- if err != nil {
- return nil, fmt.Errorf("failed to pad frequencies: %w", err)
- }
- }
- for _, freq := range freqs {
- // convert freq to GHz
- freqf := float64(freq) / 10.0
- isaFreqs = append(isaFreqs, fmt.Sprintf("%.1f", freqf))
- }
- allIsaFreqs = append(allIsaFreqs, isaFreqs)
- }
- // format the output
- var specCoreFreqs [][]string
- specCoreFreqs = make([][]string, 1, len(dieCoreBuckets)+1)
- // add bucket field name(s)
- specCoreFreqs[0] = append(specCoreFreqs[0], "Cores")
- if archMultiplier > 1 {
- specCoreFreqs[0] = append(specCoreFreqs[0], "Cores per Die")
- }
- // add fieldNames for ISAs that have frequencies
- for i := range allIsaFreqs {
- if allIsaFreqs[i][0] == "0.0" {
- continue
- }
- specCoreFreqs[0] = append(specCoreFreqs[0], strings.ToUpper(fieldNames[i+1]))
- }
- for i, bucket := range dieCoreBuckets {
- row := make([]string, 0, len(allIsaFreqs)+2)
- // add the total core buckets for multi-die architectures
- if archMultiplier > 1 {
- row = append(row, totalCoreBuckets[i])
- }
- // add the die core buckets
- row = append(row, bucket)
- // add the frequencies for each ISA
- for _, isaFreqs := range allIsaFreqs {
- if isaFreqs[0] == "0.0" {
- continue
- } else {
- if i >= len(isaFreqs) {
- return nil, fmt.Errorf("index out of range for isa frequencies")
- }
- row = append(row, isaFreqs[i])
- }
- }
- specCoreFreqs = append(specCoreFreqs, row)
- }
- return specCoreFreqs, nil
-}
-
-// expandTurboFrequencies expands the turbo frequencies to a list of frequencies
-// input is the output of getSpecFrequencyBuckets, e.g.:
-// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx"
-// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1"
-// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1"
-// ...
-// output is the expanded list of the frequencies for the requested ISA
-func expandTurboFrequencies(specFrequencyBuckets [][]string, isa string) ([]string, error) {
- if len(specFrequencyBuckets) < 2 || len(specFrequencyBuckets[0]) < 2 {
- return nil, fmt.Errorf("unable to parse core frequency buckets")
- }
- rangeIdx := 0 // the first column is the bucket, e.g., 1-44
- // find the index of the ISA column
- var isaIdx int
- for i := 1; i < len(specFrequencyBuckets[0]); i++ {
- if strings.EqualFold(specFrequencyBuckets[0][i], isa) {
- isaIdx = i
- break
- }
- }
- if isaIdx == 0 {
- return nil, fmt.Errorf("unable to find %s frequency column", isa)
- }
- var freqs []string
- for i := 1; i < len(specFrequencyBuckets); i++ {
- bucketCores, err := util.IntRangeToIntList(strings.TrimSpace(specFrequencyBuckets[i][rangeIdx]))
- if err != nil {
- return nil, fmt.Errorf("unable to parse bucket range %s", specFrequencyBuckets[i][rangeIdx])
- }
- bucketFreq := strings.TrimSpace(specFrequencyBuckets[i][isaIdx])
- if bucketFreq == "" {
- return nil, fmt.Errorf("unable to parse bucket frequency %s", specFrequencyBuckets[i][isaIdx])
- }
- for range bucketCores {
- freqs = append(freqs, bucketFreq)
- }
- }
- return freqs, nil
-}
-
-// maxFrequencyFromOutputs gets max core frequency
-//
-// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
-// 2nd option) from MSR/tpmi
-// 3rd option) from dmidecode "Max Speed"
-func maxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
- cmdout := strings.TrimSpace(outputs[script.MaximumFrequencyScriptName].Stdout)
- if cmdout != "" {
- freqf, err := strconv.ParseFloat(cmdout, 64)
- if err == nil {
- freqf = freqf / 1000000
- return fmt.Sprintf("%.1fGHz", freqf)
- }
- }
- // get the max frequency from the MSR/tpmi
- specCoreFrequencies, err := getSpecFrequencyBuckets(outputs)
- if err == nil {
- sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies)
- if len(sseFreqs) > 0 {
- // max (single-core) frequency is the first SSE frequency
- return sseFreqs[0] + "GHz"
- }
- }
- return valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Max Speed:\s(.*)`)
-}
-
-func getSSEFreqsFromBuckets(buckets [][]string) []string {
- if len(buckets) < 2 {
- return nil
- }
- // find the SSE column
- sseColumn := -1
- for i, col := range buckets[0] {
- if strings.ToUpper(col) == "SSE" {
- sseColumn = i
- break
- }
- }
- if sseColumn == -1 {
- return nil
- }
- // get the SSE values from the buckets
- sse := make([]string, 0, len(buckets)-1)
- for i := 1; i < len(buckets); i++ {
- if len(buckets[i]) > sseColumn {
- sse = append(sse, buckets[i][sseColumn])
- }
- }
- return sse
-}
-
-func allCoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
- specCoreFrequencies, err := getSpecFrequencyBuckets(outputs)
- if err != nil {
- return ""
- }
- sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies)
- if len(sseFreqs) < 1 {
- return ""
- }
- // all core max frequency is the last SSE frequency
- return sseFreqs[len(sseFreqs)-1] + "GHz"
-}
-
func hyperthreadingFromOutput(outputs map[string]script.ScriptOutput) string {
family := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^CPU family:\s*(.+)$`)
model := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Model:\s*(.+)$`)
@@ -773,94 +453,6 @@ func tdpFromOutput(outputs map[string]script.ScriptOutput) string {
return fmt.Sprint(msr/8) + "W"
}
-func uncoreMinMaxDieFrequencyFromOutput(maxFreq bool, computeDie bool, outputs map[string]script.ScriptOutput) string {
- // find the first die that matches requrested die type (compute or I/O)
- re := regexp.MustCompile(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry (\d+) in instance (\d+)`)
- var instance, entry string
- found := false
- for line := range strings.SplitSeq(outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout, "\n") {
- match := re.FindStringSubmatch(line)
- if match == nil {
- continue
- }
- if computeDie && match[1] == "0" {
- found = true
- entry = match[2]
- instance = match[3]
- break
- }
- if !computeDie && match[1] == "1" {
- found = true
- entry = match[2]
- instance = match[3]
- break
- }
- }
- if !found {
- slog.Error("failed to find uncore die type in TPMI output", slog.String("output", outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout))
- return ""
- }
- // get the frequency for the found die
- re = regexp.MustCompile(fmt.Sprintf(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry %s in instance %s`, entry, instance))
- found = false
- var parsed int64
- var err error
- var scriptName string
- if maxFreq {
- scriptName = script.UncoreMaxFromTPMIScriptName
- } else {
- scriptName = script.UncoreMinFromTPMIScriptName
- }
- for line := range strings.SplitSeq(outputs[scriptName].Stdout, "\n") {
- match := re.FindStringSubmatch(line)
- if len(match) > 0 {
- found = true
- parsed, err = strconv.ParseInt(match[1], 10, 64)
- if err != nil {
- slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("line", line))
- return ""
- }
- break
- }
- }
- if !found {
- slog.Error("failed to find uncore frequency in TPMI output", slog.String("output", outputs[scriptName].Stdout))
- return ""
- }
- return fmt.Sprintf("%.1fGHz", float64(parsed)/10)
-}
-
-func uncoreMinMaxFrequencyFromOutput(maxFreq bool, outputs map[string]script.ScriptOutput) string {
- var parsed int64
- var err error
- var scriptName string
- if maxFreq {
- scriptName = script.UncoreMaxFromMSRScriptName
- } else {
- scriptName = script.UncoreMinFromMSRScriptName
- }
- hex := strings.TrimSpace(outputs[scriptName].Stdout)
- if hex != "" && hex != "0" {
- parsed, err = strconv.ParseInt(hex, 16, 64)
- if err != nil {
- slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("hex", hex))
- return ""
- }
- } else {
- slog.Warn("failed to get uncore frequency from MSR", slog.String("hex", hex))
- return ""
- }
- return fmt.Sprintf("%.1fGHz", float64(parsed)/10)
-}
-
-func uncoreMinFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
- return uncoreMinMaxFrequencyFromOutput(false, outputs)
-}
-
-func uncoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
- return uncoreMinMaxFrequencyFromOutput(true, outputs)
-}
-
func chaCountFromOutput(outputs map[string]script.ScriptOutput) string {
// output is the result of three rdmsr calls
// - client cha count
diff --git a/internal/report/table_helpers_frequency.go b/internal/report/table_helpers_frequency.go
new file mode 100644
index 00000000..e8a10d2a
--- /dev/null
+++ b/internal/report/table_helpers_frequency.go
@@ -0,0 +1,463 @@
+package report
+
+import (
+ "fmt"
+ "log/slog"
+ "perfspect/internal/script"
+ "perfspect/internal/util"
+ "regexp"
+ "slices"
+ "strconv"
+ "strings"
+)
+
+// Copyright (C) 2021-2025 Intel Corporation
+// SPDX-License-Identifier: BSD-3-Clause
+
+// baseFrequencyFromOutput gets base core frequency
+//
+// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/base_frequency
+// 2nd option) from dmidecode "Current Speed"
+// 3nd option) parse it from the model name
+func baseFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
+ cmdout := strings.TrimSpace(outputs[script.BaseFrequencyScriptName].Stdout)
+ if cmdout != "" {
+ freqf, err := strconv.ParseFloat(cmdout, 64)
+ if err == nil {
+ freqf = freqf / 1000000
+ return fmt.Sprintf("%.1fGHz", freqf)
+ }
+ }
+ currentSpeedVal := valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Current Speed:\s(.*)$`)
+ tokens := strings.Split(currentSpeedVal, " ")
+ if len(tokens) == 2 {
+ num, err := strconv.ParseFloat(tokens[0], 64)
+ if err == nil {
+ unit := tokens[1]
+ if unit == "MHz" {
+ num = num / 1000
+ unit = "GHz"
+ }
+ return fmt.Sprintf("%.1f%s", num, unit)
+ }
+ }
+ // the frequency (if included) is at the end of the model name in lscpu's output
+ modelName := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^[Mm]odel name.*:\s*(.+?)$`)
+ tokens = strings.Split(modelName, " ")
+ if len(tokens) > 0 {
+ lastToken := tokens[len(tokens)-1]
+ if len(lastToken) > 0 && lastToken[len(lastToken)-1] == 'z' {
+ return lastToken
+ }
+ }
+ return ""
+}
+
+// getFrequenciesFromHex
+func getFrequenciesFromHex(hex string) ([]int, error) {
+ freqs, err := util.HexToIntList(hex)
+ if err != nil {
+ return nil, err
+ }
+ // reverse the order of the frequencies
+ slices.Reverse(freqs)
+ return freqs, nil
+}
+
+// getBucketSizesFromHex
+func getBucketSizesFromHex(hex string) ([]int, error) {
+ bucketSizes, err := util.HexToIntList(hex)
+ if err != nil {
+ return nil, err
+ }
+ if len(bucketSizes) != 8 {
+ err = fmt.Errorf("expected 8 bucket sizes, got %d", len(bucketSizes))
+ return nil, err
+ }
+ // reverse the order of the core counts
+ slices.Reverse(bucketSizes)
+ return bucketSizes, nil
+}
+
+// padFrequencies adds items to the frequencies slice until it reaches the desired length.
+// The value of the added items is the same as the last item in the original slice.
+func padFrequencies(freqs []int, desiredLength int) ([]int, error) {
+ if len(freqs) == 0 {
+ return nil, fmt.Errorf("cannot pad empty frequencies slice")
+ }
+ for len(freqs) < desiredLength {
+ freqs = append(freqs, freqs[len(freqs)-1])
+ }
+ return freqs, nil
+}
+
+// getSpecFrequencyBuckets
+// returns slice of rows
+// first row is header
+// each row is a slice of strings
+// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx"
+// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1"
+// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1"
+// "64-85", "32-43", "3.5", "3.5", "3.3", "3.2", "3.1"
+// ...
+// the "cores per die" column is only present for some architectures
+func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string, error) {
+ arch := UarchFromOutput(outputs)
+ if arch == "" {
+ return nil, fmt.Errorf("uarch is required")
+ }
+ out := outputs[script.SpecCoreFrequenciesScriptName].Stdout
+ // expected script output format, the number of fields may vary:
+ // "cores sse avx2 avx512 avx512h amx"
+ // "hex hex hex hex hex hex"
+ if out == "" {
+ return nil, fmt.Errorf("no core frequencies found")
+ }
+ lines := strings.Split(out, "\n")
+ if len(lines) < 2 {
+ return nil, fmt.Errorf("unexpected output format")
+ }
+ fieldNames := strings.Fields(lines[0])
+ if len(fieldNames) < 2 {
+ return nil, fmt.Errorf("unexpected output format")
+ }
+ values := strings.Fields(lines[1])
+ if len(values) != len(fieldNames) {
+ return nil, fmt.Errorf("unexpected output format")
+ }
+ // get list of buckets sizes
+ bucketCoreCounts, err := getBucketSizesFromHex(values[0])
+ if err != nil {
+ return nil, fmt.Errorf("failed to get bucket sizes from Hex string: %w", err)
+ }
+ // create buckets
+ var totalCoreBuckets []string // only for multi-die architectures
+ var dieCoreBuckets []string
+ totalCoreStartRange := 1
+ startRange := 1
+ var archMultiplier int
+ if strings.Contains(arch, "SRF") || strings.Contains(arch, "CWF") {
+ archMultiplier = 4
+ } else if strings.Contains(arch, "GNR_X3") {
+ archMultiplier = 3
+ } else if strings.Contains(arch, "GNR_X2") {
+ archMultiplier = 2
+ } else {
+ archMultiplier = 1
+ }
+ for _, count := range bucketCoreCounts {
+ if startRange > count {
+ break
+ }
+ if archMultiplier > 1 {
+ totalCoreCount := count * archMultiplier
+ if totalCoreStartRange > int(totalCoreCount) {
+ break
+ }
+ totalCoreBuckets = append(totalCoreBuckets, fmt.Sprintf("%d-%d", totalCoreStartRange, totalCoreCount))
+ totalCoreStartRange = int(totalCoreCount) + 1
+ }
+ dieCoreBuckets = append(dieCoreBuckets, fmt.Sprintf("%d-%d", startRange, count))
+ startRange = int(count) + 1
+ }
+ // get the frequencies for each isa
+ var allIsaFreqs [][]string
+ for _, isaHex := range values[1:] {
+ var isaFreqs []string
+ var freqs []int
+ if isaHex != "0" {
+ var err error
+ freqs, err = getFrequenciesFromHex(isaHex)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get frequencies from Hex string: %w", err)
+ }
+ } else {
+ // if the ISA is not supported, set the frequency to zero for all buckets
+ freqs = make([]int, len(bucketCoreCounts))
+ for i := range freqs {
+ freqs[i] = 0
+ }
+ }
+ if len(freqs) != len(bucketCoreCounts) {
+ freqs, err = padFrequencies(freqs, len(bucketCoreCounts))
+ if err != nil {
+ return nil, fmt.Errorf("failed to pad frequencies: %w", err)
+ }
+ }
+ for _, freq := range freqs {
+ // convert freq to GHz
+ freqf := float64(freq) / 10.0
+ isaFreqs = append(isaFreqs, fmt.Sprintf("%.1f", freqf))
+ }
+ allIsaFreqs = append(allIsaFreqs, isaFreqs)
+ }
+ // format the output
+ var specCoreFreqs [][]string
+ specCoreFreqs = make([][]string, 1, len(dieCoreBuckets)+1)
+ // add bucket field name(s)
+ specCoreFreqs[0] = append(specCoreFreqs[0], "Cores")
+ if archMultiplier > 1 {
+ specCoreFreqs[0] = append(specCoreFreqs[0], "Cores per Die")
+ }
+ // add fieldNames for ISAs that have frequencies
+ for i := range allIsaFreqs {
+ if allIsaFreqs[i][0] == "0.0" {
+ continue
+ }
+ specCoreFreqs[0] = append(specCoreFreqs[0], strings.ToUpper(fieldNames[i+1]))
+ }
+ for i, bucket := range dieCoreBuckets {
+ row := make([]string, 0, len(allIsaFreqs)+2)
+ // add the total core buckets for multi-die architectures
+ if archMultiplier > 1 {
+ row = append(row, totalCoreBuckets[i])
+ }
+ // add the die core buckets
+ row = append(row, bucket)
+ // add the frequencies for each ISA
+ for _, isaFreqs := range allIsaFreqs {
+ if isaFreqs[0] == "0.0" {
+ continue
+ } else {
+ if i >= len(isaFreqs) {
+ return nil, fmt.Errorf("index out of range for isa frequencies")
+ }
+ row = append(row, isaFreqs[i])
+ }
+ }
+ specCoreFreqs = append(specCoreFreqs, row)
+ }
+ return specCoreFreqs, nil
+}
+
+// expandTurboFrequencies expands the turbo frequencies to a list of frequencies
+// input is the output of getSpecFrequencyBuckets, e.g.:
+// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx"
+// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1"
+// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1"
+// ...
+// output is the expanded list of the frequencies for the requested ISA
+func expandTurboFrequencies(specFrequencyBuckets [][]string, isa string) ([]string, error) {
+ if len(specFrequencyBuckets) < 2 || len(specFrequencyBuckets[0]) < 2 {
+ return nil, fmt.Errorf("unable to parse core frequency buckets")
+ }
+ rangeIdx := 0 // the first column is the bucket, e.g., 1-44
+ // find the index of the ISA column
+ var isaIdx int
+ for i := 1; i < len(specFrequencyBuckets[0]); i++ {
+ if strings.EqualFold(specFrequencyBuckets[0][i], isa) {
+ isaIdx = i
+ break
+ }
+ }
+ if isaIdx == 0 {
+ return nil, fmt.Errorf("unable to find %s frequency column", isa)
+ }
+ var freqs []string
+ for i := 1; i < len(specFrequencyBuckets); i++ {
+ bucketCores, err := util.IntRangeToIntList(strings.TrimSpace(specFrequencyBuckets[i][rangeIdx]))
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse bucket range %s", specFrequencyBuckets[i][rangeIdx])
+ }
+ bucketFreq := strings.TrimSpace(specFrequencyBuckets[i][isaIdx])
+ if bucketFreq == "" {
+ return nil, fmt.Errorf("unable to parse bucket frequency %s", specFrequencyBuckets[i][isaIdx])
+ }
+ for range bucketCores {
+ freqs = append(freqs, bucketFreq)
+ }
+ }
+ return freqs, nil
+}
+
+// maxFrequencyFromOutputs gets max core frequency
+//
+// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
+// 2nd option) from MSR/tpmi
+// 3rd option) from dmidecode "Max Speed"
+func maxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
+ cmdout := strings.TrimSpace(outputs[script.MaximumFrequencyScriptName].Stdout)
+ if cmdout != "" {
+ freqf, err := strconv.ParseFloat(cmdout, 64)
+ if err == nil {
+ freqf = freqf / 1000000
+ return fmt.Sprintf("%.1fGHz", freqf)
+ }
+ }
+ // get the max frequency from the MSR/tpmi
+ specCoreFrequencies, err := getSpecFrequencyBuckets(outputs)
+ if err == nil {
+ sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies)
+ if len(sseFreqs) > 0 {
+ // max (single-core) frequency is the first SSE frequency
+ return sseFreqs[0] + "GHz"
+ }
+ }
+ return valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Max Speed:\s(.*)`)
+}
+
+func getSSEFreqsFromBuckets(buckets [][]string) []string {
+ if len(buckets) < 2 {
+ return nil
+ }
+ // find the SSE column
+ sseColumn := -1
+ for i, col := range buckets[0] {
+ if strings.ToUpper(col) == "SSE" {
+ sseColumn = i
+ break
+ }
+ }
+ if sseColumn == -1 {
+ return nil
+ }
+ // get the SSE values from the buckets
+ sse := make([]string, 0, len(buckets)-1)
+ for i := 1; i < len(buckets); i++ {
+ if len(buckets[i]) > sseColumn {
+ sse = append(sse, buckets[i][sseColumn])
+ }
+ }
+ return sse
+}
+
+func allCoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
+ specCoreFrequencies, err := getSpecFrequencyBuckets(outputs)
+ if err != nil {
+ return ""
+ }
+ sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies)
+ if len(sseFreqs) < 1 {
+ return ""
+ }
+ // all core max frequency is the last SSE frequency
+ return sseFreqs[len(sseFreqs)-1] + "GHz"
+}
+
+// sseFrequenciesFromOutput gets the bucketed SSE frequencies from the output
+// and returns a compact string representation with consolidated ranges, e.g.:
+// "1-40/3.5, 41-60/3.4, 61-86/3.2"
+func sseFrequenciesFromOutput(outputs map[string]script.ScriptOutput) string {
+ specCoreFrequencies, err := getSpecFrequencyBuckets(outputs)
+ if err != nil {
+ return ""
+ }
+ sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies)
+ if len(sseFreqs) < 1 {
+ return ""
+ }
+
+ var result []string
+ i := 1
+ for i < len(specCoreFrequencies) {
+ startIdx := i
+ currentFreq := sseFreqs[i-1]
+
+ // Find consecutive buckets with the same frequency
+ for i < len(specCoreFrequencies) && sseFreqs[i-1] == currentFreq {
+ i++
+ }
+ endIdx := i - 1
+
+ // Extract start and end core numbers from the ranges
+ startRange := strings.Split(specCoreFrequencies[startIdx][0], "-")[0]
+ endRange := strings.Split(specCoreFrequencies[endIdx][0], "-")[1]
+
+ // Format the consolidated range
+ if startRange == endRange {
+ result = append(result, fmt.Sprintf("%s/%s", startRange, currentFreq))
+ } else {
+ result = append(result, fmt.Sprintf("%s-%s/%s", startRange, endRange, currentFreq))
+ }
+ }
+
+ return strings.Join(result, ", ")
+}
+
+func uncoreMinMaxDieFrequencyFromOutput(maxFreq bool, computeDie bool, outputs map[string]script.ScriptOutput) string {
+ // find the first die that matches requrested die type (compute or I/O)
+ re := regexp.MustCompile(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry (\d+) in instance (\d+)`)
+ var instance, entry string
+ found := false
+ for line := range strings.SplitSeq(outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout, "\n") {
+ match := re.FindStringSubmatch(line)
+ if match == nil {
+ continue
+ }
+ if computeDie && match[1] == "0" {
+ found = true
+ entry = match[2]
+ instance = match[3]
+ break
+ }
+ if !computeDie && match[1] == "1" {
+ found = true
+ entry = match[2]
+ instance = match[3]
+ break
+ }
+ }
+ if !found {
+ slog.Error("failed to find uncore die type in TPMI output", slog.String("output", outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout))
+ return ""
+ }
+ // get the frequency for the found die
+ re = regexp.MustCompile(fmt.Sprintf(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry %s in instance %s`, entry, instance))
+ found = false
+ var parsed int64
+ var err error
+ var scriptName string
+ if maxFreq {
+ scriptName = script.UncoreMaxFromTPMIScriptName
+ } else {
+ scriptName = script.UncoreMinFromTPMIScriptName
+ }
+ for line := range strings.SplitSeq(outputs[scriptName].Stdout, "\n") {
+ match := re.FindStringSubmatch(line)
+ if len(match) > 0 {
+ found = true
+ parsed, err = strconv.ParseInt(match[1], 10, 64)
+ if err != nil {
+ slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("line", line))
+ return ""
+ }
+ break
+ }
+ }
+ if !found {
+ slog.Error("failed to find uncore frequency in TPMI output", slog.String("output", outputs[scriptName].Stdout))
+ return ""
+ }
+ return fmt.Sprintf("%.1fGHz", float64(parsed)/10)
+}
+
+func uncoreMinMaxFrequencyFromOutput(maxFreq bool, outputs map[string]script.ScriptOutput) string {
+ var parsed int64
+ var err error
+ var scriptName string
+ if maxFreq {
+ scriptName = script.UncoreMaxFromMSRScriptName
+ } else {
+ scriptName = script.UncoreMinFromMSRScriptName
+ }
+ hex := strings.TrimSpace(outputs[scriptName].Stdout)
+ if hex != "" && hex != "0" {
+ parsed, err = strconv.ParseInt(hex, 16, 64)
+ if err != nil {
+ slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("hex", hex))
+ return ""
+ }
+ } else {
+ slog.Warn("failed to get uncore frequency from MSR", slog.String("hex", hex))
+ return ""
+ }
+ return fmt.Sprintf("%.1fGHz", float64(parsed)/10)
+}
+
+func uncoreMinFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
+ return uncoreMinMaxFrequencyFromOutput(false, outputs)
+}
+
+func uncoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string {
+ return uncoreMinMaxFrequencyFromOutput(true, outputs)
+}
diff --git a/tools/build.Dockerfile b/tools/build.Dockerfile
index 733ee498..1ce6d1bb 100644
--- a/tools/build.Dockerfile
+++ b/tools/build.Dockerfile
@@ -15,7 +15,7 @@ ENV http_proxy=${http_proxy}
ENV https_proxy=${https_proxy}
ENV LANG=en_US.UTF-8
ARG DEBIAN_FRONTEND=noninteractive
-ARG GO_VERSION=1.25.4
+ARG GO_VERSION=1.25.5
# install minimum packages to add repositories
RUN success=false; \