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; \