From b1b539f7562cf4da9b2f1a9db68412fa8b7e939f Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 1 Dec 2025 06:53:43 -0800 Subject: [PATCH 01/19] record Signed-off-by: Harper, Jason M --- cmd/config/config.go | 126 ++++++++++++++++++++++++++++++++------ cmd/config/flag_groups.go | 8 +++ 2 files changed, 114 insertions(+), 20 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 6c08f2f0..e6a5317b 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" @@ -52,6 +53,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 +108,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 +181,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 @@ -227,7 +285,8 @@ func setOnTarget(cmd *cobra.Command, myTarget target.Target, flagGroups []flagGr 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 +298,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 +328,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) { + filesWriten := []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/config_%s.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 filesWriten, err + } + filesWriten = append(filesWriten, outputFilePath) } - fmt.Print(string(reportBytes)) } - return + return filesWriten, nil } // collectOnTarget runs the scripts on the target and sends the results to the appropriate channels diff --git a/cmd/config/flag_groups.go b/cmd/config/flag_groups.go index 3fb765dc..d0ac8515 100644 --- a/cmd/config/flag_groups.go +++ b/cmd/config/flag_groups.go @@ -66,6 +66,8 @@ const ( // other flag names const ( flagNoSummaryName = "no-summary" + flagRecordName = "record" + flagRestoreName = "restore" ) // governorOptions - list of valid governor options @@ -237,6 +239,12 @@ 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), + ) + group.flags = append(group.flags, + newStringFlag(cmd, flagRestoreName, "", nil, "restore a previously recorded configuration from the specified file", "", nil), + ) flagGroups = append(flagGroups, group) common.AddTargetFlags(Cmd) From 5c44feabca2572a550a72e6d4342b55e96d0a3e6 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 1 Dec 2025 15:16:32 -0800 Subject: [PATCH 02/19] restore Signed-off-by: Harper, Jason M --- README.md | 32 ++++ cmd/config/config.go | 4 +- cmd/config/flag_groups.go | 9 +- cmd/config/restore.go | 373 +++++++++++++++++++++++++++++++++++++ cmd/config/restore_test.go | 192 +++++++++++++++++++ internal/common/targets.go | 15 +- 6 files changed, 619 insertions(+), 6 deletions(-) create mode 100644 cmd/config/restore.go create mode 100644 cmd/config/restore_test.go diff --git a/README.md b/README.md index 4b891459..f7455917 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,38 @@ $ ./perfspect config --cores 24 --llc 2.0 --uncore-max 1.8 ... +##### Recording Configuration +Before making changes, you can record the current configuration 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 --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 86
+  --llc 2.4
+  --uncore-max-compute 2.2
+  ...
+Apply these settings? (yes/no): yes
+...
+
+ +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/cmd/config/config.go b/cmd/config/config.go index e6a5317b..7e268bd2 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -26,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 current config to file: $ %s %s --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), @@ -367,7 +369,7 @@ func printConfig(reports map[string][]byte, toStdout bool, toFile bool, outputDi fmt.Print(string(reportBytes)) } if toFile { - outputFilePath := fmt.Sprintf("%s/config_%s.txt", outputDir, targetName) + 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) diff --git a/cmd/config/flag_groups.go b/cmd/config/flag_groups.go index d0ac8515..a16d026c 100644 --- a/cmd/config/flag_groups.go +++ b/cmd/config/flag_groups.go @@ -67,7 +67,6 @@ const ( const ( flagNoSummaryName = "no-summary" flagRecordName = "record" - flagRestoreName = "restore" ) // governorOptions - list of valid governor options @@ -242,9 +241,6 @@ func initializeFlags(cmd *cobra.Command) { group.flags = append(group.flags, newBoolFlag(cmd, flagRecordName, false, nil, "record the current configuration to a file to be restored later", "", nil), ) - group.flags = append(group.flags, - newStringFlag(cmd, flagRestoreName, "", nil, "restore a previously recorded configuration from the specified file", "", nil), - ) flagGroups = append(flagGroups, group) common.AddTargetFlags(Cmd) @@ -269,6 +265,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..eafe6bd6 --- /dev/null +++ b/cmd/config/restore.go @@ -0,0 +1,373 @@ +// 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" + "fmt" + "log/slog" + "os" + "os/exec" + "perfspect/internal/common" + "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 { + flagName string + 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 { + fmt.Fprintf(os.Stderr, "Error: failed to parse configuration file: %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) + for _, fv := range flagValues { + fmt.Printf(" --%s %s\n", fv.flagName, fv.value) + } + fmt.Println() + + // build the command to execute + executable, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %v", 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", "keystring", "port", "password"} + 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) + } + + // 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 { + return fmt.Errorf("failed to read user input: %v", 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, " "))) + fmt.Println() // blank line before config output + + execCmd := exec.Command(executable, cmdArgs...) + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + execCmd.Stdin = os.Stdin + + err = execCmd.Run() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return fmt.Errorf("config command failed with exit code %d", exitErr.ExitCode()) + } + return fmt.Errorf("failed to execute config command: %v", err) + } + + 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 (not used) + rawValue := strings.TrimSpace(matches[2]) + flagStr := matches[3] + + // extract flag name (remove the leading --) + flagName := strings.TrimPrefix(flagStr, "--") + + // 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{ + 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 flagAllCoreMaxFrequencyName, 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, ", ")) +} diff --git a/cmd/config/restore_test.go b/cmd/config/restore_test.go new file mode 100644 index 00000000..82cf54ed --- /dev/null +++ b/cmd/config/restore_test.go @@ -0,0 +1,192 @@ +package config + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "os" + "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 +All-Core Max Frequency: 3.2GHz --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"]) + assert.Equal(t, "3.2", valueMap["core-max"]) + 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 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}, + {"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) + } + }) + } +} 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) From d476221557db97770a5d85ad2c2c1f1efa93c2c7 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 1 Dec 2025 15:45:26 -0800 Subject: [PATCH 03/19] clean output Signed-off-by: Harper, Jason M --- cmd/config/restore.go | 103 +++++++++++++++++++-- cmd/config/restore_test.go | 180 +++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 6 deletions(-) diff --git a/cmd/config/restore.go b/cmd/config/restore.go index eafe6bd6..af31791c 100644 --- a/cmd/config/restore.go +++ b/cmd/config/restore.go @@ -6,7 +6,9 @@ package config import ( "bufio" + "bytes" "fmt" + "io" "log/slog" "os" "os/exec" @@ -114,7 +116,8 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { // parse the configuration file flagValues, err := parseConfigFile(configFilePath) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to parse configuration file: %v\n", err) + 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 @@ -135,7 +138,11 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { // build the command to execute executable, err := os.Executable() if err != nil { - return fmt.Errorf("failed to get executable path: %v", err) + 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 ... @@ -169,6 +176,9 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { 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, " ")) @@ -178,7 +188,11 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { reader := bufio.NewReader(os.Stdin) response, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("failed to read user input: %v", err) + 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" { @@ -193,15 +207,31 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { execCmd := exec.Command(executable, cmdArgs...) execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr execCmd.Stdin = os.Stdin + // capture stderr for parsing + var stderrBuf bytes.Buffer + stderrWriter := io.MultiWriter(os.Stderr, &stderrBuf) + execCmd.Stderr = stderrWriter + err = execCmd.Run() + + // parse stderr output and present results in flag order + parseAndPresentResults(stderrBuf.String(), flagValues) + if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("config command failed with exit code %d", exitErr.ExitCode()) + 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 } - return fmt.Errorf("failed to execute config command: %v", 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 } return nil @@ -371,3 +401,64 @@ func parseEnableDisableOrOption(value string, validOptions []string) (string, er 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 + } + + // Parse stderr for success and error messages + // Looking for patterns like: + // - "set to " + // - "failed to set to " + // - "error: ..." messages related to flags + + // Build a map of flag names to their results + flagResults := make(map[string]string) + + // Regex patterns to match success and error messages + // Flag names can contain hyphens, so use [\w-]+ instead of \S+ + successPattern := regexp.MustCompile(`set ([\w-]+) to ([^,]+)`) + errorPattern := regexp.MustCompile(`failed to set ([\w-]+) to ([^,]+)`) + + // Parse stderr line by line + lines := strings.Split(stderrOutput, "\n") + for _, line := range lines { + // Check for success messages - use FindAllStringSubmatch to find all matches on the line + successMatches := successPattern.FindAllStringSubmatch(line, -1) + for _, matches := range successMatches { + if len(matches) >= 3 { + flagName := matches[1] + value := strings.TrimSpace(matches[2]) + flagResults[flagName] = fmt.Sprintf("✓ Set %s to %s", flagName, value) + } + } + + // Check for error messages - use FindAllStringSubmatch to find all matches on the line + errorMatches := errorPattern.FindAllStringSubmatch(line, -1) + for _, matches := range errorMatches { + if len(matches) >= 3 { + flagName := matches[1] + value := strings.TrimSpace(matches[2]) + flagResults[flagName] = fmt.Sprintf("✗ Failed to set %s to %s", flagName, value) + } + } + } + + // Present results in the order of flagValues + if len(flagValues) > 0 { + fmt.Println("\nConfiguration Results:") + for _, fv := range flagValues { + if result, found := flagResults[fv.flagName]; found { + fmt.Printf(" %s\n", result) + } else { + // If no explicit success or error was found, show unknown status + fmt.Printf(" ? %s: status unknown\n", fv.flagName) + } + } + fmt.Println() + } +} diff --git a/cmd/config/restore_test.go b/cmd/config/restore_test.go index 82cf54ed..5870b78f 100644 --- a/cmd/config/restore_test.go +++ b/cmd/config/restore_test.go @@ -4,7 +4,10 @@ package config // SPDX-License-Identifier: BSD-3-Clause import ( + "bytes" + "io" "os" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -190,3 +193,180 @@ func TestParseEnableDisableOrOption(t *testing.T) { }) } } + +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 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", + flagValues: []flagValue{ + {flagName: "cores", value: "86"}, + {flagName: "llc", value: "336"}, + {flagName: "tdp", value: "350"}, + {flagName: "core-max", value: "3.2"}, + {flagName: "uncore-max-compute", value: "2.2"}, + {flagName: "uncore-min-compute", value: "0.8"}, + {flagName: "uncore-max-io", value: "2.5"}, + {flagName: "uncore-min-io", value: "0.8"}, + {flagName: "epb", value: "0"}, + {flagName: "gov", value: "powersave"}, + {flagName: "elc", value: "default"}, + {flagName: "pref-l2hw", value: "enable"}, + {flagName: "pref-l2adj", value: "enable"}, + {flagName: "pref-dcuhw", value: "enable"}, + {flagName: "pref-dcuip", value: "enable"}, + {flagName: "pref-dcunp", value: "enable"}, + {flagName: "pref-amp", value: "enable"}, + {flagName: "pref-llcpp", value: "enable"}, + {flagName: "pref-aop", value: "enable"}, + {flagName: "pref-homeless", value: "enable"}, + {flagName: "pref-llc", value: "disable"}, + {flagName: "c6", value: "enable"}, + {flagName: "c1-demotion", value: "disable"}, + }, + expectedOutput: []string{ + "✓ Set cores to 86", + "✗ Failed to set llc to 336", + "✓ Set tdp to 350", + "✓ Set core-max to 3.2", + "✓ 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 epb to 0", + "✓ Set gov to powersave", + "✓ Set elc to default", + "✗ Failed to 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", + }, + }, + { + name: "Empty stderr output", + stderrOutput: "", + flagValues: []flagValue{ + {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{ + {flagName: "cores", value: "86"}, + {flagName: "llc", value: "336"}, + {flagName: "tdp", value: "350"}, + }, + expectedOutput: []string{ + "✓ Set cores to 86", + "✗ Failed to set llc to 336", + "✓ Set tdp to 350", + }, + }, + { + name: "Flag name with multiple hyphens", + stderrOutput: "set uncore-max-compute to 2.2, set uncore-min-io to 0.8", + flagValues: []flagValue{ + {flagName: "uncore-max-compute", value: "2.2"}, + {flagName: "uncore-min-io", value: "0.8"}, + }, + expectedOutput: []string{ + "✓ Set uncore-max-compute to 2.2", + "✓ Set uncore-min-io to 0.8", + }, + }, + { + name: "No matching flags in output", + stderrOutput: "some other message without flag updates", + flagValues: []flagValue{ + {flagName: "cores", value: "86"}, + }, + expectedOutput: []string{ + "? cores: status unknown", + }, + }, + { + name: "Flag with underscore and numbers", + stderrOutput: "set pref_test123 to enable, failed to set flag_456 to disable", + flagValues: []flagValue{ + {flagName: "pref_test123", value: "enable"}, + {flagName: "flag_456", value: "disable"}, + }, + expectedOutput: []string{ + "✓ Set pref_test123 to enable", + "✗ Failed to set flag_456 to disable", + }, + }, + { + name: "Some flags updated, others not mentioned", + stderrOutput: "set cores to 86, set tdp to 350", + flagValues: []flagValue{ + {flagName: "cores", value: "86"}, + {flagName: "llc", value: "336"}, + {flagName: "tdp", value: "350"}, + }, + expectedOutput: []string{ + "✓ Set cores to 86", + "? llc: status unknown", + "✓ Set tdp to 350", + }, + }, + } + + 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") + } + }) + } +} From c16682064370ba60ba5166b1b14ee469e277b740 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 1 Dec 2025 16:02:02 -0800 Subject: [PATCH 04/19] progress indicator Signed-off-by: Harper, Jason M --- cmd/config/restore.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/cmd/config/restore.go b/cmd/config/restore.go index af31791c..0ccf4d18 100644 --- a/cmd/config/restore.go +++ b/cmd/config/restore.go @@ -8,11 +8,11 @@ import ( "bufio" "bytes" "fmt" - "io" "log/slog" "os" "os/exec" "perfspect/internal/common" + "perfspect/internal/progress" "regexp" "slices" "strconv" @@ -149,7 +149,7 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { cmdArgs := []string{"config"} // copy target flags from restore command first - targetFlags := []string{"target", "targets", "user", "key", "keystring", "port", "password"} + 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()) @@ -203,21 +203,28 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { // execute the command slog.Info("executing perfspect config", slog.String("command", executable), slog.String("args", strings.Join(cmdArgs, " "))) - fmt.Println() // blank line before config output execCmd := exec.Command(executable, cmdArgs...) execCmd.Stdout = os.Stdout execCmd.Stdin = os.Stdin - // capture stderr for parsing + // capture stderr for parsing (don't display it in real-time to keep output clean) var stderrBuf bytes.Buffer - stderrWriter := io.MultiWriter(os.Stderr, &stderrBuf) - execCmd.Stderr = stderrWriter + 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() - // parse stderr output and present results in flag order - parseAndPresentResults(stderrBuf.String(), flagValues) + _ = multiSpinner.Status("config", "configuration changes complete") + multiSpinner.Finish() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { @@ -234,6 +241,9 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { return err } + // parse stderr output and present results in flag order + parseAndPresentResults(stderrBuf.String(), flagValues) + return nil } From 75510c3eb3171207e36b8be5fa65625fd13b25f3 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 1 Dec 2025 17:32:25 -0800 Subject: [PATCH 05/19] human readable Signed-off-by: Harper, Jason M --- cmd/config/restore.go | 53 ++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/cmd/config/restore.go b/cmd/config/restore.go index 0ccf4d18..aa445caf 100644 --- a/cmd/config/restore.go +++ b/cmd/config/restore.go @@ -26,8 +26,9 @@ const restoreCmdName = "restore" // flagValue represents a single flag name and value pair, preserving order type flagValue struct { - flagName string - value string + 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{ @@ -130,8 +131,15 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { // 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 { - fmt.Printf(" --%s %s\n", fv.flagName, fv.value) + 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() @@ -267,7 +275,8 @@ func parseConfigFile(filePath string) ([]flagValue, error) { line := scanner.Text() matches := flagLineRegex.FindStringSubmatch(line) if len(matches) == 4 { - // matches[1] = field name (not used) + // matches[1] = field name (e.g., "Cores per Socket") + fieldName := strings.TrimSpace(matches[1]) rawValue := strings.TrimSpace(matches[2]) flagStr := matches[3] @@ -282,8 +291,9 @@ func parseConfigFile(filePath string) ([]flagValue, error) { } flagValues = append(flagValues, flagValue{ - flagName: flagName, - value: convertedValue, + fieldName: fieldName, + flagName: flagName, + value: convertedValue, }) } } @@ -420,14 +430,14 @@ func parseAndPresentResults(stderrOutput string, flagValues []flagValue) { return } - // Parse stderr for success and error messages - // Looking for patterns like: - // - "set to " - // - "failed to set to " - // - "error: ..." messages related to flags + // 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]string) + flagResults := make(map[string]flagResult) // Regex patterns to match success and error messages // Flag names can contain hyphens, so use [\w-]+ instead of \S+ @@ -443,7 +453,7 @@ func parseAndPresentResults(stderrOutput string, flagValues []flagValue) { if len(matches) >= 3 { flagName := matches[1] value := strings.TrimSpace(matches[2]) - flagResults[flagName] = fmt.Sprintf("✓ Set %s to %s", flagName, value) + flagResults[flagName] = flagResult{success: true, value: value} } } @@ -453,7 +463,7 @@ func parseAndPresentResults(stderrOutput string, flagValues []flagValue) { if len(matches) >= 3 { flagName := matches[1] value := strings.TrimSpace(matches[2]) - flagResults[flagName] = fmt.Sprintf("✗ Failed to set %s to %s", flagName, value) + flagResults[flagName] = flagResult{success: false, value: value} } } } @@ -461,12 +471,23 @@ func parseAndPresentResults(stderrOutput string, flagValues []flagValue) { // 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 { - fmt.Printf(" %s\n", result) + 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", fv.flagName) + fmt.Printf(" ? %-*s: status unknown\n", maxLen, fv.fieldName) } } fmt.Println() From d4bcc6213e1c3746fc4549bc03ab64813caf52aa Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 2 Dec 2025 08:22:13 -0800 Subject: [PATCH 06/19] sequential execution Signed-off-by: Harper, Jason M --- cmd/config/config.go | 37 ++++++++++++++++++------------------- cmd/config/set.go | 3 ++- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 7e268bd2..928db457 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -237,50 +237,49 @@ func setOnTarget(cmd *cobra.Command, myTarget target.Target, flagGroups []flagGr 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 out setOutput 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) + go flag.intSetFunc(value, myTarget, localTempDir, channelSetComplete, 0) + out = <-channelSetComplete } case "float64": if flag.floatSetFunc != nil { value, _ := cmd.Flags().GetFloat64(flag.GetName()) - go flag.floatSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1) + go flag.floatSetFunc(value, myTarget, localTempDir, channelSetComplete, 0) + out = <-channelSetComplete } case "string": if flag.stringSetFunc != nil { value, _ := cmd.Flags().GetString(flag.GetName()) - go flag.stringSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1) + go flag.stringSetFunc(value, myTarget, localTempDir, channelSetComplete, 0) + out = <-channelSetComplete } case "bool": if flag.boolSetFunc != nil { value, _ := cmd.Flags().GetBool(flag.GetName()) - go flag.boolSetFunc(value, myTarget, localTempDir, channelSetComplete, len(successMessages)-1) + go flag.boolSetFunc(value, myTarget, localTempDir, channelSetComplete, 0) + out = <-channelSetComplete } } + if out.err != nil { + slog.Error(out.err.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) diff --git a/cmd/config/set.go b/cmd/config/set.go index 6a9d82df..7b004494 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -153,7 +153,8 @@ func setLlcSize(desiredLlcSize float64, myTarget target.Target, localTempDir str return } if currentLlcSize == desiredLlcSize { - completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("LLC size is already set to %.2f MB", desiredLlcSize)} + // return success + completeChannel <- setOutput{goRoutineID: goRoutineId, err: nil} return } if desiredLlcSize > maximumLlcSize { From 471a11e87238aec1e0676ee123f55161285aa61b Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 2 Dec 2025 10:01:29 -0800 Subject: [PATCH 07/19] runscripts for uncore freq Signed-off-by: Harper, Jason M --- cmd/config/set.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/config/set.go b/cmd/config/set.go index 7b004494..f1611e66 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -324,19 +324,22 @@ func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float6 bits = "15:21" // bits 15:21 are the min frequency } // 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 max and min uncore frequency TPMI %s %s", 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} } From 62cddb8c2a5acd0e3c53c6679b506b6795163899 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 2 Dec 2025 10:33:55 -0800 Subject: [PATCH 08/19] fix spinner output Signed-off-by: Harper, Jason M --- cmd/config/restore.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/config/restore.go b/cmd/config/restore.go index aa445caf..05be5eca 100644 --- a/cmd/config/restore.go +++ b/cmd/config/restore.go @@ -213,11 +213,11 @@ func runRestoreCmd(cmd *cobra.Command, args []string) error { slog.Info("executing perfspect config", slog.String("command", executable), slog.String("args", strings.Join(cmdArgs, " "))) execCmd := exec.Command(executable, cmdArgs...) - execCmd.Stdout = os.Stdout execCmd.Stdin = os.Stdin - // capture stderr for parsing (don't display it in real-time to keep output clean) - var stderrBuf bytes.Buffer + // 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 From 6bf545fd238dfff6b0c2d1977cb2c73bf54e4d82 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 2 Dec 2025 12:11:35 -0800 Subject: [PATCH 09/19] remove remnants of set go routines Signed-off-by: Harper, Jason M --- cmd/config/config.go | 19 ++-- cmd/config/flag.go | 28 +++--- cmd/config/flag_groups.go | 38 +++----- cmd/config/set.go | 199 ++++++++++++++------------------------ 4 files changed, 112 insertions(+), 172 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 928db457..cb9ad479 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -236,7 +236,6 @@ func setOnTarget(cmd *cobra.Command, myTarget target.Target, flagGroups []flagGr channelError <- nil return } - channelSetComplete := make(chan setOutput) var statusMessages []string _ = statusUpdate(myTarget.GetName(), "updating configuration") for _, group := range flagGroups { @@ -244,35 +243,31 @@ func setOnTarget(cmd *cobra.Command, myTarget target.Target, flagGroups []flagGr if flag.HasSetFunc() && cmd.Flags().Lookup(flag.GetName()).Changed { 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 out setOutput + 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, 0) - out = <-channelSetComplete + 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, 0) - out = <-channelSetComplete + 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, 0) - out = <-channelSetComplete + 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, 0) - out = <-channelSetComplete + setErr = flag.boolSetFunc(value, myTarget, localTempDir) } } - if out.err != nil { - slog.Error(out.err.Error()) + if setErr != nil { + slog.Error(setErr.Error()) statusMessages = append(statusMessages, errorMessage) } else { statusMessages = append(statusMessages, successMessage) 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 a16d026c..e8c06399 100644 --- a/cmd/config/flag_groups.go +++ b/cmd/config/flag_groups.go @@ -126,8 +126,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 { @@ -135,8 +135,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 { @@ -144,8 +144,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 { @@ -153,8 +153,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 { @@ -162,8 +162,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 { @@ -171,8 +171,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 { @@ -192,8 +192,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 { @@ -213,19 +213,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) diff --git a/cmd/config/set.go b/cmd/config/set.go index f1611e66..aa702723 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,51 +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 { // return success - completeChannel <- setOutput{goRoutineID: goRoutineId, err: nil} - return + 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", @@ -186,28 +177,24 @@ 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 setCoreFrequency(coreFrequency 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) @@ -220,8 +207,7 @@ 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 @@ -267,27 +253,24 @@ func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDi if err != nil { err = fmt.Errorf("failed to set core frequency: %w", err) } - completeChannel <- setOutput{goRoutineID: goRoutineId, err: err} + return err } -func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float64, myTarget target.Target, localTempDir string, completeChannel chan setOutput, goRoutineId int) { +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 @@ -299,8 +282,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") { @@ -341,10 +323,10 @@ func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float6 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() @@ -360,27 +342,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 @@ -407,10 +384,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", @@ -421,14 +398,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 @@ -444,26 +419,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 @@ -484,13 +456,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) @@ -509,10 +479,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 @@ -527,13 +497,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 @@ -550,8 +518,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{ @@ -564,13 +531,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 @@ -589,10 +554,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), @@ -602,10 +567,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]: @@ -613,8 +578,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", @@ -628,14 +592,13 @@ 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) { +func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir string, prefetcherType string) error { pf, err := report.GetPrefetcherDefByName(prefetcherType) if err != nil { - completeChannel <- setOutput{goRoutineID: goRoutineId, err: fmt.Errorf("failed to get prefetcher definition: %w", err)} - return + return fmt.Errorf("failed to get prefetcher definition: %w", err) } // check if the prefetcher is supported on this target's architecture // get the uarch @@ -645,18 +608,15 @@ func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir st 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") } // 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{ @@ -669,13 +629,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 @@ -685,8 +643,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) @@ -705,11 +662,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 @@ -728,13 +685,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 { @@ -743,8 +698,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 { @@ -760,10 +714,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", @@ -774,13 +728,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 @@ -790,8 +742,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) @@ -811,7 +762,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 From dfd03442316307d40da02d98870d3509f2641d3b Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 2 Dec 2025 13:02:58 -0800 Subject: [PATCH 10/19] fix set core frequency Signed-off-by: Harper, Jason M --- cmd/config/set.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/config/set.go b/cmd/config/set.go index aa702723..2b78c58b 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -213,7 +213,7 @@ func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDi var value uint64 var i uint for i = range 2 { - value = value | freqInt< Date: Tue, 2 Dec 2025 15:48:15 -0800 Subject: [PATCH 11/19] set bucket frequencies Signed-off-by: Harper, Jason M --- cmd/config/flag_groups.go | 31 +- cmd/config/restore.go | 2 +- cmd/config/set.go | 244 ++++++++++- cmd/config/set_test.go | 159 +++++++ internal/report/table_defs.go | 2 +- internal/report/table_helpers.go | 408 ------------------ internal/report/table_helpers_frequency.go | 463 +++++++++++++++++++++ 7 files changed, 878 insertions(+), 431 deletions(-) create mode 100644 cmd/config/set_test.go create mode 100644 internal/report/table_helpers_frequency.go diff --git a/cmd/config/flag_groups.go b/cmd/config/flag_groups.go index e8c06399..6be88d51 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-sse-freq-buckets" + flagEPBName = "epb" + flagEPPName = "epp" + flagGovernorName = "gov" + flagELCName = "elc" ) // uncore frequency flag names @@ -96,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) diff --git a/cmd/config/restore.go b/cmd/config/restore.go index 05be5eca..ecb75209 100644 --- a/cmd/config/restore.go +++ b/cmd/config/restore.go @@ -326,7 +326,7 @@ func convertValue(flagName string, rawValue string) (string, error) { case flagTDPName: // "350W" -> "350" (Watts is assumed) return parseNumericWithUnit(rawValue, "W") - case flagAllCoreMaxFrequencyName, flagUncoreMaxFrequencyName, flagUncoreMinFrequencyName, + case flagSSEFrequencyName, flagUncoreMaxFrequencyName, flagUncoreMinFrequencyName, flagUncoreMaxComputeFrequencyName, flagUncoreMinComputeFrequencyName, flagUncoreMaxIOFrequencyName, flagUncoreMinIOFrequencyName: // "3.2GHz" -> "3.2" (GHz is assumed) diff --git a/cmd/config/set.go b/cmd/config/set.go index 2b78c58b..de5feef7 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -180,7 +180,7 @@ func setLlcSize(desiredLlcSize float64, myTarget target.Target, localTempDir str return err } -func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDir string) error { +func setSSEFrequency(sseFrequency float64, myTarget target.Target, localTempDir string) error { targetFamily, err := myTarget.GetFamily() if err != nil { return fmt.Errorf("failed to get target family: %w", err) @@ -197,7 +197,7 @@ func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDi 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{ @@ -256,6 +256,222 @@ func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDi return err } +// expandConsolidatedFrequencies takes a consolidated frequency string and bucket sizes, +// and returns the 8 individual bucket frequencies. +// Input format: "1-40/3.5, 41-60/3.4, 61-86/3.2" +// bucketSizes: slice of 8 integers representing the end core number of each bucket (e.g., [20, 40, 60, 80, 86, 86, 86, 86]). +// This example corresponds to the following buckets: 0-19, 20-39, 40-59, 60-79, 80-85, 80-85, 80-85, 80-85 +// Returns: slice of 8 float64 values, one frequency per bucket +func expandConsolidatedFrequencies(consolidatedStr string, bucketSizes []int) ([]float64, error) { + if len(bucketSizes) != 8 { + return nil, fmt.Errorf("expected 8 bucket sizes, got %d", len(bucketSizes)) + } + + bucketFrequencies := make([]float64, 8) + entries := strings.Split(consolidatedStr, ", ") + + // Parse all consolidated entries + type consolidatedRange struct { + startCore int + endCore int + freq float64 + } + var ranges []consolidatedRange + + for _, entry := range entries { + // Parse each entry in format "start-end/freq" + parts := strings.Split(entry, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid format for entry: %s", entry) + } + + // Parse the frequency + freq, err := strconv.ParseFloat(parts[1], 64) + if err != nil { + return nil, fmt.Errorf("invalid frequency in entry %s: %w", entry, err) + } + + // Parse the range + rangeParts := strings.Split(parts[0], "-") + if len(rangeParts) != 2 { + return nil, fmt.Errorf("invalid range format in entry: %s", entry) + } + + startCore, err := strconv.Atoi(rangeParts[0]) + if err != nil { + return nil, fmt.Errorf("invalid start core in entry %s: %w", entry, err) + } + + endCore, err := strconv.Atoi(rangeParts[1]) + if err != nil { + return nil, fmt.Errorf("invalid end core in entry %s: %w", entry, err) + } + + ranges = append(ranges, consolidatedRange{startCore, endCore, freq}) + } + + // Map each original bucket to its frequency + for i, bucketSize := range bucketSizes { + // Calculate the start and end of this original bucket + var bucketStart, bucketEnd int + if i == 0 { + bucketStart = 1 + } else { + bucketStart = bucketSizes[i-1] + 1 + } + bucketEnd = bucketSize + + // Find which consolidated range contains the midpoint of this bucket + bucketMidpoint := (bucketStart + bucketEnd) / 2 + for _, r := range ranges { + if bucketMidpoint >= 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 := uint(0); i < 2; i++ { + 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 := uint(0); i < 8; i++ { + 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() @@ -595,24 +811,30 @@ func setELC(elc string, myTarget target.Target, localTempDir string) error { return err } -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) - } - // 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 { - return fmt.Errorf("failed to run scripts on target: %w", err) + return "", fmt.Errorf("failed to run scripts on target: %w", err) } uarch := report.UarchFromOutput(outputs) if uarch == "" { - return fmt.Errorf("failed to get microarchitecture") + 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]) { 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/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) +} From b5d6a679a2108ee94328affb298df8fb830ac9a7 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 3 Dec 2025 07:50:08 -0800 Subject: [PATCH 12/19] go 1.25.5 Signed-off-by: Harper, Jason M --- builder/build.Dockerfile | 2 +- tools/build.Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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; \ From 85e7b84a9f0b5902b1f0b4bceef24d1b82a5fe72 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 3 Dec 2025 07:50:19 -0800 Subject: [PATCH 13/19] modernize Signed-off-by: Harper, Jason M --- cmd/config/set.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/config/set.go b/cmd/config/set.go index de5feef7..f2ddb566 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -429,7 +429,7 @@ func setSSEFrequencies(sseFrequencies string, myTarget target.Target, localTempD if strings.Contains(output, "intel_pstate") { // For SRF/CWF with intel_pstate, we only set 2 buckets var value uint64 - for i := uint(0); i < 2; i++ { + for i := range uint(2) { freqInt := uint64(bucketFrequencies[i] * 10) value = value | freqInt<<(i*8) } @@ -453,7 +453,7 @@ func setSSEFrequencies(sseFrequencies string, myTarget target.Target, localTempD } else { // For other platforms, set all 8 buckets var value uint64 - for i := uint(0); i < 8; i++ { + for i := range uint(8) { freqInt := uint64(bucketFrequencies[i] * 10) value = value | freqInt<<(i*8) } From 1bf679c5ed3a54547dcd12e1c85a3c74dbe9d717 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 3 Dec 2025 07:50:33 -0800 Subject: [PATCH 14/19] handle new core frequency format Signed-off-by: Harper, Jason M --- cmd/config/restore.go | 78 ++++++++++++------ cmd/config/restore_test.go | 162 ++++++++++++++++++++----------------- 2 files changed, 143 insertions(+), 97 deletions(-) diff --git a/cmd/config/restore.go b/cmd/config/restore.go index ecb75209..5620ad92 100644 --- a/cmd/config/restore.go +++ b/cmd/config/restore.go @@ -283,6 +283,11 @@ func parseConfigFile(filePath string) ([]flagValue, error) { // extract flag name (remove the leading --) flagName := strings.TrimPrefix(flagStr, "--") + // special case: if flag is core-max change it to core-sse-freq-buckets + if flagName == flagSSEFrequencyName { + flagName = flagSSEFrequencyAllBucketsName + } + // convert the raw value to the appropriate format convertedValue, err := convertValue(flagName, rawValue) if err != nil { @@ -326,6 +331,13 @@ func convertValue(flagName string, rawValue string) (string, error) { 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: @@ -439,31 +451,49 @@ func parseAndPresentResults(stderrOutput string, flagValues []flagValue) { // Build a map of flag names to their results flagResults := make(map[string]flagResult) - // Regex patterns to match success and error messages - // Flag names can contain hyphens, so use [\w-]+ instead of \S+ - successPattern := regexp.MustCompile(`set ([\w-]+) to ([^,]+)`) - errorPattern := regexp.MustCompile(`failed to set ([\w-]+) to ([^,]+)`) - // Parse stderr line by line - lines := strings.Split(stderrOutput, "\n") - for _, line := range lines { - // Check for success messages - use FindAllStringSubmatch to find all matches on the line - successMatches := successPattern.FindAllStringSubmatch(line, -1) - for _, matches := range successMatches { - if len(matches) >= 3 { - flagName := matches[1] - value := strings.TrimSpace(matches[2]) - flagResults[flagName] = flagResult{success: true, value: value} - } - } - - // Check for error messages - use FindAllStringSubmatch to find all matches on the line - errorMatches := errorPattern.FindAllStringSubmatch(line, -1) - for _, matches := range errorMatches { - if len(matches) >= 3 { - flagName := matches[1] - value := strings.TrimSpace(matches[2]) - flagResults[flagName] = flagResult{success: false, value: value} + // We split on delimiters ", set " and ", failed to set " to handle messages where + // flag values themselves contain commas (like core-sse-freq-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} + } } } } diff --git a/cmd/config/restore_test.go b/cmd/config/restore_test.go index 5870b78f..80422507 100644 --- a/cmd/config/restore_test.go +++ b/cmd/config/restore_test.go @@ -21,7 +21,7 @@ func TestParseConfigFile(t *testing.T) { Cores per Socket: 86 --cores L3 Cache: 336M --llc Package Power / TDP: 350W --tdp -All-Core Max Frequency: 3.2GHz --core-max +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> @@ -52,7 +52,8 @@ C6: Disabled --c6 assert.Equal(t, "86", valueMap["cores"]) assert.Equal(t, "336", valueMap["llc"]) assert.Equal(t, "350", valueMap["tdp"]) - assert.Equal(t, "3.2", valueMap["core-max"]) + // verify core-max with buckets is converted to core-sse-freq-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-sse-freq-buckets"]) assert.Equal(t, "2.2", valueMap["uncore-max-compute"]) assert.Equal(t, "0", valueMap["epb"]) assert.Equal(t, "powersave", valueMap["gov"]) @@ -89,6 +90,9 @@ func TestConvertValue(t *testing.T) { {"C6 enabled", "c6", "Enabled", "enable", false}, {"ELC lowercase", "elc", "default", "default", false}, {"ELC capitalized", "elc", "Default", "default", false}, + {"Core SSE freq buckets", "core-sse-freq-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-sse-freq-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-sse-freq-buckets", "invalid-format", "", true}, {"Inconsistent value", "epp", "inconsistent", "", true}, {"Unknown flag", "unknown-flag", "value", "", true}, } @@ -203,63 +207,65 @@ func TestParseAndPresentResults(t *testing.T) { }{ { name: "Example from function header comment", - stderrOutput: "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", + stderrOutput: "configuration update complete: set cores to 86, set llc to 336, set tdp to 350, set core-sse-freq-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{ - {flagName: "cores", value: "86"}, - {flagName: "llc", value: "336"}, - {flagName: "tdp", value: "350"}, - {flagName: "core-max", value: "3.2"}, - {flagName: "uncore-max-compute", value: "2.2"}, - {flagName: "uncore-min-compute", value: "0.8"}, - {flagName: "uncore-max-io", value: "2.5"}, - {flagName: "uncore-min-io", value: "0.8"}, - {flagName: "epb", value: "0"}, - {flagName: "gov", value: "powersave"}, - {flagName: "elc", value: "default"}, - {flagName: "pref-l2hw", value: "enable"}, - {flagName: "pref-l2adj", value: "enable"}, - {flagName: "pref-dcuhw", value: "enable"}, - {flagName: "pref-dcuip", value: "enable"}, - {flagName: "pref-dcunp", value: "enable"}, - {flagName: "pref-amp", value: "enable"}, - {flagName: "pref-llcpp", value: "enable"}, - {flagName: "pref-aop", value: "enable"}, - {flagName: "pref-homeless", value: "enable"}, - {flagName: "pref-llc", value: "disable"}, - {flagName: "c6", value: "enable"}, - {flagName: "c1-demotion", value: "disable"}, + {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-sse-freq-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{ - "✓ Set cores to 86", - "✗ Failed to set llc to 336", - "✓ Set tdp to 350", - "✓ Set core-max to 3.2", - "✓ 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 epb to 0", - "✓ Set gov to powersave", - "✓ Set elc to default", - "✗ Failed to 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", + "✓ 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{ - {flagName: "cores", value: "86"}, + {fieldName: "Cores per Socket", flagName: "cores", value: "86"}, }, expectedOutput: []string{}, // nothing should be printed }, @@ -268,62 +274,72 @@ func TestParseAndPresentResults(t *testing.T) { 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{ - {flagName: "cores", value: "86"}, - {flagName: "llc", value: "336"}, - {flagName: "tdp", value: "350"}, + {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{ - "✓ Set cores to 86", - "✗ Failed to set llc to 336", - "✓ Set tdp to 350", + "✓ 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{ - {flagName: "uncore-max-compute", value: "2.2"}, - {flagName: "uncore-min-io", value: "0.8"}, + {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{ - "✓ Set uncore-max-compute to 2.2", - "✓ Set uncore-min-io to 0.8", + "✓ Uncore Max Frequency (Compute)", + "✓ Uncore Min Frequency (I/O)", }, }, { name: "No matching flags in output", stderrOutput: "some other message without flag updates", flagValues: []flagValue{ - {flagName: "cores", value: "86"}, + {fieldName: "Cores per Socket", flagName: "cores", value: "86"}, }, expectedOutput: []string{ - "? cores: status unknown", + "? Cores per Socket", }, }, { name: "Flag with underscore and numbers", stderrOutput: "set pref_test123 to enable, failed to set flag_456 to disable", flagValues: []flagValue{ - {flagName: "pref_test123", value: "enable"}, - {flagName: "flag_456", value: "disable"}, + {fieldName: "Pref Test", flagName: "pref_test123", value: "enable"}, + {fieldName: "Flag 456", flagName: "flag_456", value: "disable"}, }, expectedOutput: []string{ - "✓ Set pref_test123 to enable", - "✗ Failed to set flag_456 to disable", + "✓ Pref Test", + "✗ Flag 456", + }, + }, + { + name: "Core SSE freq buckets with commas in value", + stderrOutput: "set core-sse-freq-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-sse-freq-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{ - {flagName: "cores", value: "86"}, - {flagName: "llc", value: "336"}, - {flagName: "tdp", value: "350"}, + {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{ - "✓ Set cores to 86", - "? llc: status unknown", - "✓ Set tdp to 350", + "✓ Cores per Socket", + "? L3 Cache", + "✓ Package Power / TDP", }, }, } @@ -344,7 +360,7 @@ func TestParseAndPresentResults(t *testing.T) { // Read captured output var buf bytes.Buffer - io.Copy(&buf, r) + _, _ = io.Copy(&buf, r) output := buf.String() // Verify expected output From 0aaa997e4017cd7a75ac9beae4873280fd016880 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 3 Dec 2025 07:58:52 -0800 Subject: [PATCH 15/19] readme Signed-off-by: Harper, Jason M --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f7455917..7e312dfd 100644 --- a/README.md +++ b/README.md @@ -130,11 +130,11 @@ $ ./perfspect config --cores 24 --llc 2.0 --uncore-max 1.8 ##### Recording Configuration -Before making changes, you can record the current configuration to a file using the `--record` flag. This creates a human-readable configuration file that can be used to restore settings later. +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 --record
+$ ./perfspect config --tdp 300 --record
 Configuration recorded to: perfspect_2025-12-01_14-30-45/gnr_config.txt
 
@@ -145,11 +145,11 @@ 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 86
-  --llc 2.4
-  --uncore-max-compute 2.2
+  Cores per Socket    : 86
+  L3 Cache            : 336
+  Package Power / TDP : 350
   ...
-Apply these settings? (yes/no): yes
+Apply these configuration changes? [y/N]: y
 ...
 
From 25abdd078f68951d70eb16cba07c9e2b0064d14a Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 3 Dec 2025 12:42:40 -0800 Subject: [PATCH 16/19] add test Signed-off-by: Harper, Jason M --- cmd/config/restore_test.go | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/cmd/config/restore_test.go b/cmd/config/restore_test.go index 80422507..58102056 100644 --- a/cmd/config/restore_test.go +++ b/cmd/config/restore_test.go @@ -70,6 +70,60 @@ C6: Disabled --c6 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-sse-freq-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-sse-freq-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 From b98043f45586bc52dc1b0a1ab36dcefb7076bda4 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 3 Dec 2025 12:52:50 -0800 Subject: [PATCH 17/19] spelling Signed-off-by: Harper, Jason M --- cmd/config/config.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index cb9ad479..857c1a8f 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -353,7 +353,7 @@ func processConfig(targetScriptOutputs []common.TargetScriptOutputs) (map[string // printConfig prints and/or saves the configuration reports func printConfig(reports map[string][]byte, toStdout bool, toFile bool, outputDir string) ([]string, error) { - filesWriten := []string{} + filesWritten := []string{} for targetName, reportBytes := range reports { if toStdout { // print the report to stdout @@ -367,12 +367,12 @@ func printConfig(reports map[string][]byte, toStdout bool, toFile bool, outputDi err := os.WriteFile(outputFilePath, reportBytes, 0644) // #nosec G306 if err != nil { err = fmt.Errorf("failed to write configuration report to file: %v", err) - return filesWriten, err + return filesWritten, err } - filesWriten = append(filesWriten, outputFilePath) + filesWritten = append(filesWritten, outputFilePath) } } - return filesWriten, nil + return filesWritten, nil } // collectOnTarget runs the scripts on the target and sends the results to the appropriate channels From e40065d7cba55f55df810ecb66c8a45be8f3d6c5 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 3 Dec 2025 12:56:29 -0800 Subject: [PATCH 18/19] improve script name Signed-off-by: Harper, Jason M --- cmd/config/set.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/config/set.go b/cmd/config/set.go index f2ddb566..745193c0 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -516,16 +516,19 @@ 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: fmt.Sprintf("write max and min uncore frequency TPMI %s %s", die.instance, die.entry), + 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"}, From 5c7d7095ec5dcb6cd1df5cc95006f28bb024bb56 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 3 Dec 2025 13:47:56 -0800 Subject: [PATCH 19/19] flag name Signed-off-by: Harper, Jason M --- cmd/config/config.go | 2 +- cmd/config/flag_groups.go | 4 ++-- cmd/config/restore.go | 4 ++-- cmd/config/restore_test.go | 22 +++++++++++----------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index 857c1a8f..bd1a2b03 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -26,7 +26,7 @@ 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 current config to file: $ %s %s --record", 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), diff --git a/cmd/config/flag_groups.go b/cmd/config/flag_groups.go index 6be88d51..10fd0f11 100644 --- a/cmd/config/flag_groups.go +++ b/cmd/config/flag_groups.go @@ -43,7 +43,7 @@ const ( flagLLCSizeName = "llc" flagTDPName = "tdp" flagSSEFrequencyName = "core-max" - flagSSEFrequencyAllBucketsName = "core-sse-freq-buckets" + flagSSEFrequencyAllBucketsName = "core-max-buckets" flagEPBName = "epb" flagEPPName = "epp" flagGovernorName = "gov" @@ -103,7 +103,7 @@ func initializeFlags(cmd *cobra.Command) { 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", + 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 diff --git a/cmd/config/restore.go b/cmd/config/restore.go index 5620ad92..f31d2b8f 100644 --- a/cmd/config/restore.go +++ b/cmd/config/restore.go @@ -283,7 +283,7 @@ func parseConfigFile(filePath string) ([]flagValue, error) { // extract flag name (remove the leading --) flagName := strings.TrimPrefix(flagStr, "--") - // special case: if flag is core-max change it to core-sse-freq-buckets + // special case: if flag is core-max change it to core-max-buckets if flagName == flagSSEFrequencyName { flagName = flagSSEFrequencyAllBucketsName } @@ -453,7 +453,7 @@ func parseAndPresentResults(stderrOutput string, flagValues []flagValue) { // 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-sse-freq-buckets) + // 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 diff --git a/cmd/config/restore_test.go b/cmd/config/restore_test.go index 58102056..10bbbb4b 100644 --- a/cmd/config/restore_test.go +++ b/cmd/config/restore_test.go @@ -52,8 +52,8 @@ C6: Disabled --c6 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-sse-freq-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-sse-freq-buckets"]) + // 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"]) @@ -115,8 +115,8 @@ C1 Demotion: Disabled 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-sse-freq-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-sse-freq-buckets"]) + // 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"]) @@ -144,9 +144,9 @@ func TestConvertValue(t *testing.T) { {"C6 enabled", "c6", "Enabled", "enable", false}, {"ELC lowercase", "elc", "default", "default", false}, {"ELC capitalized", "elc", "Default", "default", false}, - {"Core SSE freq buckets", "core-sse-freq-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-sse-freq-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-sse-freq-buckets", "invalid-format", "", true}, + {"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}, } @@ -261,12 +261,12 @@ func TestParseAndPresentResults(t *testing.T) { }{ { name: "Example from function header comment", - stderrOutput: "configuration update complete: set cores to 86, set llc to 336, set tdp to 350, set core-sse-freq-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", + 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-sse-freq-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: "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"}, @@ -374,9 +374,9 @@ func TestParseAndPresentResults(t *testing.T) { }, { name: "Core SSE freq buckets with commas in value", - stderrOutput: "set core-sse-freq-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", + 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-sse-freq-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: "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",