diff --git a/cmd/config/config.go b/cmd/config/config.go index eba6442f..a95a74bc 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -20,72 +20,8 @@ import ( "strings" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) -var ( - flagCoreCount int - flagLLCSize float64 - flagAllCoreMaxFrequency float64 - flagUncoreMaxFrequency float64 - flagUncoreMinFrequency float64 - flagUncoreMaxComputeFrequency float64 - flagUncoreMinComputeFrequency float64 - flagUncoreMaxIOFrequency float64 - flagUncoreMinIOFrequency float64 - flagTDP int - flagEPB int - flagEPP int - flagGovernor string - flagELC string - flagPrefetcherL2HW string - flagPrefetcherL2Adj string - flagPrefetcherDCUHW string - flagPrefetcherDCUIP string - flagPrefetcherDCUNP string - flagPrefetcherAMP string - flagPrefetcherLLCPP string - flagPrefetcherAOP string - flagPrefetcherHomeless string - flagPrefetcherLLC string -) - -const ( - flagCoreCountName = "cores" - flagLLCSizeName = "llc" - flagAllCoreMaxFrequencyName = "core-max" - flagUncoreMaxFrequencyName = "uncore-max" - flagUncoreMinFrequencyName = "uncore-min" - flagUncoreMaxComputeFrequencyName = "uncore-max-compute" - flagUncoreMinComputeFrequencyName = "uncore-min-compute" - flagUncoreMaxIOFrequencyName = "uncore-max-io" - flagUncoreMinIOFrequencyName = "uncore-min-io" - flagTDPName = "tdp" - flagEPBName = "epb" - flagEPPName = "epp" - flagGovernorName = "gov" - flagELCName = "elc" - flagPrefetcherL2HWName = "pref-l2hw" - flagPrefetcherL2AdjName = "pref-l2adj" - flagPrefetcherDCUHWName = "pref-dcuhw" - flagPrefetcherDCUIPName = "pref-dcuip" - flagPrefetcherDCUNPName = "pref-dcunp" - flagPrefetcherAMPName = "pref-amp" - flagPrefetcherLLCPPName = "pref-llcpp" - flagPrefetcherAOPName = "pref-aop" - flagPrefetcherHomelessName = "pref-homeless" - flagPrefetcherLLCName = "pref-llc" -) - -// governorOptions - list of valid governor options -var governorOptions = []string{"performance", "powersave"} - -// elcOptions - list of valid elc options -var elcOptions = []string{"latency-optimized", "default"} - -// prefetcherOptions - list of valid prefetcher options -var prefetcherOptions = []string{"enable", "disable"} - const cmdName = "config" var examples = []string{ @@ -111,285 +47,7 @@ USE CAUTION! Target may become unstable. It is up to the user to ensure that the } func init() { - Cmd.Flags().IntVar(&flagCoreCount, flagCoreCountName, 0, "") - Cmd.Flags().Float64Var(&flagLLCSize, flagLLCSizeName, 0, "") - Cmd.Flags().Float64Var(&flagAllCoreMaxFrequency, flagAllCoreMaxFrequencyName, 0, "") - Cmd.Flags().Float64Var(&flagUncoreMaxFrequency, flagUncoreMaxFrequencyName, 0, "") - Cmd.Flags().Float64Var(&flagUncoreMinFrequency, flagUncoreMinFrequencyName, 0, "") - Cmd.Flags().Float64Var(&flagUncoreMaxComputeFrequency, flagUncoreMaxComputeFrequencyName, 0, "") - Cmd.Flags().Float64Var(&flagUncoreMinComputeFrequency, flagUncoreMinComputeFrequencyName, 0, "") - Cmd.Flags().Float64Var(&flagUncoreMaxIOFrequency, flagUncoreMaxIOFrequencyName, 0, "") - Cmd.Flags().Float64Var(&flagUncoreMinIOFrequency, flagUncoreMinIOFrequencyName, 0, "") - Cmd.Flags().IntVar(&flagTDP, flagTDPName, 0, "") - Cmd.Flags().IntVar(&flagEPB, flagEPBName, 0, "") - Cmd.Flags().IntVar(&flagEPP, flagEPPName, 0, "") - Cmd.Flags().StringVar(&flagGovernor, flagGovernorName, "", "") - Cmd.Flags().StringVar(&flagELC, flagELCName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherL2HW, flagPrefetcherL2HWName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherL2Adj, flagPrefetcherL2AdjName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherDCUHW, flagPrefetcherDCUHWName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherDCUIP, flagPrefetcherDCUIPName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherDCUNP, flagPrefetcherDCUNPName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherAMP, flagPrefetcherAMPName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherLLCPP, flagPrefetcherLLCPPName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherAOP, flagPrefetcherAOPName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherHomeless, flagPrefetcherHomelessName, "", "") - Cmd.Flags().StringVar(&flagPrefetcherLLC, flagPrefetcherLLCName, "", "") - - common.AddTargetFlags(Cmd) - - Cmd.SetUsageFunc(usageFunc) -} - -func usageFunc(cmd *cobra.Command) error { - cmd.Printf("Usage: %s [flags]\n\n", cmd.CommandPath()) - cmd.Printf("Examples:\n%s\n\n", cmd.Example) - cmd.Println("Flags:") - for _, group := range getFlagGroups() { - cmd.Printf(" %s:\n", group.GroupName) - for _, flag := range group.Flags { - cmd.Printf(" --%-20s %s\n", flag.Name, flag.Help) - } - } - cmd.Println("\nGlobal Flags:") - cmd.Parent().PersistentFlags().VisitAll(func(pf *pflag.Flag) { - flagDefault := "" - if cmd.Parent().PersistentFlags().Lookup(pf.Name).DefValue != "" { - flagDefault = fmt.Sprintf(" (default: %s)", cmd.Flags().Lookup(pf.Name).DefValue) - } - cmd.Printf(" --%-20s %s%s\n", pf.Name, pf.Usage, flagDefault) - }) - return nil -} - -func getFlagGroups() []common.FlagGroup { - generalFlags := []common.Flag{ - { - Name: flagCoreCountName, - Help: "number of physical cores per processor", - }, - { - Name: flagLLCSizeName, - Help: "LLC size in MB", - }, - { - Name: flagAllCoreMaxFrequencyName, - Help: "all-core max frequency in GHz", - }, - { - Name: flagTDPName, - Help: "maximum power per processor in Watts", - }, - { - Name: flagEPBName, - Help: "energy perf bias from best performance (0) to most power savings (15)", - }, - { - Name: flagEPPName, - Help: "energy perf profile from best performance (0) to most power savings (255)", - }, - { - Name: flagGovernorName, - Help: "CPU scaling governor (" + strings.Join(governorOptions, ", ") + ")", - }, - { - Name: flagELCName, - Help: "Efficiency Latency Control (" + strings.Join(elcOptions, ", ") + ") [SRF+]", - }, - } - groups := []common.FlagGroup{} - groups = append(groups, common.FlagGroup{ - GroupName: "General Options", - Flags: generalFlags, - }) - uncoreFrequencyFlags := []common.Flag{ - { - Name: flagUncoreMaxFrequencyName, - Help: "maximum uncore frequency in GHz [EMR-]", - }, - { - Name: flagUncoreMinFrequencyName, - Help: "minimum uncore frequency in GHz [EMR-]", - }, - { - Name: flagUncoreMaxComputeFrequencyName, - Help: "maximum uncore compute die frequency in GHz [SRF+]", - }, - { - Name: flagUncoreMinComputeFrequencyName, - Help: "minimum uncore compute die frequency in GHz [SRF+]", - }, - { - Name: flagUncoreMaxIOFrequencyName, - Help: "maximum uncore IO die frequency in GHz [SRF+]", - }, - { - Name: flagUncoreMinIOFrequencyName, - Help: "minimum uncore IO die frequency in GHz [SRF+]", - }, - } - groups = append(groups, common.FlagGroup{ - GroupName: "Uncore Frequency Options", - Flags: uncoreFrequencyFlags, - }) - prefetcherFlags := []common.Flag{ - { - Name: flagPrefetcherL2HWName, - Help: "L2 hardware prefetcher (" + strings.Join(prefetcherOptions, ", ") + ")", - }, - { - Name: flagPrefetcherL2AdjName, - Help: "L2 adjacent cache line prefetcher (" + strings.Join(prefetcherOptions, ", ") + ")", - }, - { - Name: flagPrefetcherDCUHWName, - Help: "DCU hardware prefetcher (" + strings.Join(prefetcherOptions, ", ") + ")", - }, - { - Name: flagPrefetcherDCUIPName, - Help: "DCU instruction pointer prefetcher (" + strings.Join(prefetcherOptions, ", ") + ")", - }, - { - Name: flagPrefetcherDCUNPName, - Help: "DCU next page prefetcher (" + strings.Join(prefetcherOptions, ", ") + ")", - }, - { - Name: flagPrefetcherAMPName, - Help: "Adaptive multipath probability prefetcher (" + strings.Join(prefetcherOptions, ", ") + ") [SPR,EMR,GNR]", - }, - { - Name: flagPrefetcherLLCPPName, - Help: "LLC page prefetcher (" + strings.Join(prefetcherOptions, ", ") + ") [GNR]", - }, - { - Name: flagPrefetcherAOPName, - Help: "Array of pointers prefetcher (" + strings.Join(prefetcherOptions, ", ") + ") [GNR]", - }, - { - Name: flagPrefetcherHomelessName, - Help: "Homeless prefetcher (" + strings.Join(prefetcherOptions, ", ") + ") [SPR,EMR,GNR]", - }, - { - Name: flagPrefetcherLLCName, - Help: "Last level cache prefetcher (" + strings.Join(prefetcherOptions, ", ") + ") [SPR,EMR,GNR]", - }, - } - groups = append(groups, common.FlagGroup{ - GroupName: "Prefetcher Options", - Flags: prefetcherFlags, - }) - - groups = append(groups, common.GetTargetFlagGroup()) - return groups -} - -func validateFlags(cmd *cobra.Command, args []string) error { - if cmd.Flags().Lookup(flagCoreCountName).Changed && flagCoreCount < 1 { - err := fmt.Errorf("invalid core count: %d", flagCoreCount) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagLLCSizeName).Changed && flagLLCSize < 1 { - err := fmt.Errorf("invalid LLC size: %.2f MB", flagLLCSize) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagAllCoreMaxFrequencyName).Changed && flagAllCoreMaxFrequency < 0.1 { - err := fmt.Errorf("invalid core frequency: %.1f GHz", flagAllCoreMaxFrequency) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagUncoreMaxFrequencyName).Changed && flagUncoreMaxFrequency < 0.1 { - err := fmt.Errorf("invalid uncore max frequency: %.1f GHz", flagUncoreMaxFrequency) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagUncoreMinFrequencyName).Changed && flagUncoreMinFrequency < 0.1 { - err := fmt.Errorf("invalid uncore min frequency: %.1f GHz", flagUncoreMinFrequency) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagTDPName).Changed && flagTDP < 1 { - err := fmt.Errorf("invalid power: %d", flagTDP) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagEPBName).Changed && (flagEPB < 0 || flagEPB > 15) { - err := fmt.Errorf("invalid epb: %d, valid values are 0-15", flagEPB) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagEPPName).Changed && (flagEPP < 0 || flagEPP > 255) { - err := fmt.Errorf("invalid epp: %d, valid values are 0-255", flagEPP) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagGovernorName).Changed && !slices.Contains(governorOptions, flagGovernor) { - err := fmt.Errorf("invalid governor: %s, valid options are: %s", flagGovernor, strings.Join(governorOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagELCName).Changed && !slices.Contains(elcOptions, flagELC) { - err := fmt.Errorf("invalid elc mode: %s, valid options are: %s", flagELC, strings.Join(elcOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherL2HWName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherL2HW) { - err := fmt.Errorf("invalid prefetcher value: %s, valid options are: %s", flagPrefetcherL2HW, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherL2AdjName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherL2Adj) { - err := fmt.Errorf("invalid L2 Adj prefetcher: %s, valid options are: %s", flagPrefetcherL2Adj, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherDCUHWName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherDCUHW) { - err := fmt.Errorf("invalid DCU HW prefetcher: %s, valid options are: %s", flagPrefetcherDCUHW, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherDCUIPName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherDCUIP) { - err := fmt.Errorf("invalid DCU IP prefetcher: %s, valid options are: %s", flagPrefetcherDCUIP, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherDCUNPName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherDCUNP) { - err := fmt.Errorf("invalid DCU NP prefetcher: %s, valid options are: %s", flagPrefetcherDCUNP, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherAMPName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherAMP) { - err := fmt.Errorf("invalid AMP prefetcher: %s, valid options are: %s", flagPrefetcherAMP, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherLLCPPName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherLLCPP) { - err := fmt.Errorf("invalid LLCPP prefetcher: %s, valid options are: %s", flagPrefetcherLLCPP, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherAOPName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherAOP) { - err := fmt.Errorf("invalid AOP prefetcher: %s, valid options are: %s", flagPrefetcherAOP, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherHomelessName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherHomeless) { - err := fmt.Errorf("invalid homeless prefetcher: %s, valid options are: %s", flagPrefetcherHomeless, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - if cmd.Flags().Lookup(flagPrefetcherLLCName).Changed && !slices.Contains(prefetcherOptions, flagPrefetcherLLC) { - err := fmt.Errorf("invalid LLC prefetcher: %s, valid options are: %s", flagPrefetcherLLC, strings.Join(prefetcherOptions, ", ")) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - // common target flags - if err := common.ValidateTargetFlags(cmd); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - return err - } - return nil + initializeFlags(Cmd) } func runCmd(cmd *cobra.Command, args []string) error { @@ -442,14 +100,37 @@ func runCmd(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true return err } - // were any changes requested? + // make requested changes, one target at a time changeRequested := false - flagGroups := getFlagGroups() - for i := range len(flagGroups) - 1 { - for _, flag := range flagGroups[i].Flags { - if cmd.Flags().Lookup(flag.Name).Changed { - changeRequested = true - break + for _, myTarget := range myTargets { + for _, group := range flagGroups { + for _, flag := range group.flags { + if cmd.Flags().Lookup(flag.GetName()).Changed { + changeRequested = true + fmt.Printf("%s setting %s to %s\n", myTarget.GetName(), flag.GetName(), flag.GetValueAsString()) + var err error + switch flag.GetType() { + case "int": + if flag.intSetFunc != nil { + value, _ := cmd.Flags().GetInt(flag.GetName()) + err = flag.intSetFunc(value, myTarget, localTempDir) + } + case "float64": + if flag.floatSetFunc != nil { + value, _ := cmd.Flags().GetFloat64(flag.GetName()) + err = flag.floatSetFunc(value, myTarget, localTempDir) + } + case "string": + if flag.stringSetFunc != nil { + value, _ := cmd.Flags().GetString(flag.GetName()) + err = flag.stringSetFunc(value, myTarget, localTempDir) + } + } + if err != nil { + fmt.Fprintf(os.Stderr, "%s Error: %v\n", myTarget.GetName(), err) + slog.Error(err.Error(), slog.String("target", myTarget.GetName())) + } + } } } } @@ -457,86 +138,6 @@ func runCmd(cmd *cobra.Command, args []string) error { fmt.Println("No changes requested.") return nil } - // make requested changes, one target at a time - for _, myTarget := range myTargets { - if cmd.Flags().Lookup(flagCoreCountName).Changed { - out, err := setCoreCount(flagCoreCount, myTarget, localTempDir) - if err != nil { - fmt.Printf("Error: %v, %s\n", err, out) - cmd.SilenceUsage = true - return err - } - } - if cmd.Flags().Lookup(flagLLCSizeName).Changed { - setLlcSize(flagLLCSize, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagAllCoreMaxFrequencyName).Changed { - setCoreFrequency(flagAllCoreMaxFrequency, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagUncoreMaxFrequencyName).Changed { - setUncoreFrequency(true, flagUncoreMaxFrequency, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagUncoreMinFrequencyName).Changed { - setUncoreFrequency(false, flagUncoreMinFrequency, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagUncoreMaxComputeFrequencyName).Changed { - setUncoreDieFrequency(true, true, flagUncoreMaxComputeFrequency, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagUncoreMinComputeFrequencyName).Changed { - setUncoreDieFrequency(false, true, flagUncoreMinComputeFrequency, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagUncoreMaxIOFrequencyName).Changed { - setUncoreDieFrequency(true, false, flagUncoreMaxIOFrequency, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagUncoreMinIOFrequencyName).Changed { - setUncoreDieFrequency(false, false, flagUncoreMinIOFrequency, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagTDPName).Changed { - setPower(flagTDP, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagEPBName).Changed { - setEpb(flagEPB, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagEPPName).Changed { - setEpp(flagEPP, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagGovernorName).Changed { - setGovernor(flagGovernor, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagELCName).Changed { - setElc(flagELC, myTarget, localTempDir) - } - if cmd.Flags().Lookup(flagPrefetcherL2HWName).Changed { - setPrefetcher(flagPrefetcherL2HW, myTarget, localTempDir, "L2 HW") // these prefetcher names must match the shortName in prefetcher_defs.go - } - if cmd.Flags().Lookup(flagPrefetcherL2AdjName).Changed { - setPrefetcher(flagPrefetcherL2Adj, myTarget, localTempDir, "L2 Adj") - } - if cmd.Flags().Lookup(flagPrefetcherDCUHWName).Changed { - setPrefetcher(flagPrefetcherDCUHW, myTarget, localTempDir, "DCU HW") - } - if cmd.Flags().Lookup(flagPrefetcherDCUIPName).Changed { - setPrefetcher(flagPrefetcherDCUIP, myTarget, localTempDir, "DCU IP") - } - if cmd.Flags().Lookup(flagPrefetcherDCUNPName).Changed { - setPrefetcher(flagPrefetcherDCUNP, myTarget, localTempDir, "DCU NP") - } - if cmd.Flags().Lookup(flagPrefetcherAMPName).Changed { - setPrefetcher(flagPrefetcherAMP, myTarget, localTempDir, "AMP") - } - if cmd.Flags().Lookup(flagPrefetcherLLCPPName).Changed { - setPrefetcher(flagPrefetcherLLCPP, myTarget, localTempDir, "LLCPP") - } - if cmd.Flags().Lookup(flagPrefetcherAOPName).Changed { - setPrefetcher(flagPrefetcherAOP, myTarget, localTempDir, "AOP") - } - if cmd.Flags().Lookup(flagPrefetcherHomelessName).Changed { - setPrefetcher(flagPrefetcherHomeless, myTarget, localTempDir, "Homeless") - } - if cmd.Flags().Lookup(flagPrefetcherLLCName).Changed { - setPrefetcher(flagPrefetcherLLC, myTarget, localTempDir, "LLC") - } - } // print config after making changes fmt.Println("") // blank line if err := printConfig(myTargets, localTempDir); err != nil { @@ -591,8 +192,7 @@ func printConfig(myTargets []target.Target, localTempDir string) (err error) { return } -func setCoreCount(cores int, myTarget target.Target, localTempDir string) (string, error) { - fmt.Printf("set core count per processor to %d on %s\n", cores, myTarget.GetName()) +func setCoreCount(cores int, myTarget target.Target, localTempDir string) error { setScript := script.ScriptDefinition{ Name: "set core count", ScriptTemplate: fmt.Sprintf(` @@ -681,11 +281,14 @@ done `, cores), Superuser: true, } - return runScript(myTarget, setScript, localTempDir) + _, err := runScript(myTarget, setScript, localTempDir) + if err != nil { + return fmt.Errorf("failed to set core count: %w", err) + } + return nil } -func setLlcSize(llcSize float64, myTarget target.Target, localTempDir string) { - fmt.Printf("set LLC size to %.2f MB on %s\n", llcSize, myTarget.GetName()) +func setLlcSize(llcSize float64, myTarget target.Target, localTempDir string) error { scripts := []script.ScriptDefinition{} scripts = append(scripts, script.GetScriptByName(script.LscpuScriptName)) scripts = append(scripts, script.GetScriptByName(script.LspciBitsScriptName)) @@ -694,42 +297,31 @@ func setLlcSize(llcSize float64, myTarget target.Target, localTempDir string) { outputs, err := script.RunScripts(myTarget, scripts, true, localTempDir) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to run scripts on target", slog.String("target", myTarget.GetName()), slog.String("error", err.Error())) - return + return fmt.Errorf("failed to run scripts on target: %w", err) } maximumLlcSize, _, err := report.GetL3LscpuMB(outputs) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to get maximum LLC size", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get maximum LLC size: %w", err) } // microarchitecture uarch := report.UarchFromOutput(outputs) cacheWays := report.GetCacheWays(uarch) if len(cacheWays) == 0 { - fmt.Fprintln(os.Stderr, "failed to get cache ways") - slog.Error("failed to get cache ways") - return + return fmt.Errorf("failed to get cache ways") } // current LLC size currentLlcSize, err := report.GetL3MSRMB(outputs) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to get LLC size", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get current LLC size: %w", err) } if currentLlcSize == llcSize { - fmt.Printf("LLC size is already set to %.2f MB\n", llcSize) - return + return fmt.Errorf("LLC size is already set to %.2f MB", llcSize) } // calculate the number of ways to set cachePerWay := maximumLlcSize / float64(len(cacheWays)) waysToSet := int(math.Ceil((llcSize / cachePerWay)) - 1) if waysToSet >= len(cacheWays) { - fmt.Fprintf(os.Stderr, "LLC size is too large, maximum is %.2f MB\n", maximumLlcSize) - slog.Error("LLC size is too large", slog.Float64("llc size", llcSize), slog.Float64("current llc size", currentLlcSize)) - return + return fmt.Errorf("LLC size is too large, maximum is %.2f MB", maximumLlcSize) } // set the LLC size setScript := script.ScriptDefinition{ @@ -743,35 +335,26 @@ func setLlcSize(llcSize float64, myTarget target.Target, localTempDir string) { } _, err = runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set LLC size: %v\n", err) + return fmt.Errorf("failed to set LLC size: %w", err) } + return nil } -func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDir string) { - fmt.Printf("set core frequency to %.1f GHz on %s\n", coreFrequency, myTarget.GetName()) +func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDir string) error { targetFamily, err := myTarget.GetFamily() if err != nil { - fmt.Fprintf(os.Stderr, "error getting target family: %v\n", err) - slog.Error("failed to get target family", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get target family: %w", err) } targetModel, err := myTarget.GetModel() if err != nil { - fmt.Fprintf(os.Stderr, "error getting target model: %v\n", err) - slog.Error("failed to get target model", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get target model: %w", err) } targetVendor, err := myTarget.GetVendor() if err != nil { - fmt.Fprintf(os.Stderr, "error getting target vendor: %v\n", err) - slog.Error("failed to get target vendor", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get target vendor: %w", err) } if targetVendor != "GenuineIntel" { - err := fmt.Errorf("core frequency setting not supported on %s due to vendor mismatch", myTarget.GetName()) - slog.Error(err.Error()) - fmt.Fprintf(os.Stderr, "Error: failed to set core frequency: %v\n", err) - 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) @@ -784,9 +367,7 @@ func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDi } output, err := runScript(myTarget, getScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to get pstate driver: %v\n", err) - slog.Error("failed to get pstate driver", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get pstate driver: %w", err) } if strings.Contains(output, "intel_pstate") { var value uint64 @@ -825,40 +406,22 @@ func setCoreFrequency(coreFrequency float64, myTarget target.Target, localTempDi } _, err = runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set core frequency: %v\n", err) + return fmt.Errorf("failed to set core frequency: %w", err) } + return nil } -func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float64, myTarget target.Target, localTempDir string) { - var minmax, dietype string - if maxFreq { - minmax = "max" - } else { - minmax = "min" - } - if computeDie { - dietype = "compute" - } else { - dietype = "I/O" - } - fmt.Printf("set uncore %s %s die frequency to %.1f GHz on %s\n", minmax, dietype, uncoreFrequency, myTarget.GetName()) +func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float64, myTarget target.Target, localTempDir string) error { targetFamily, err := myTarget.GetFamily() if err != nil { - fmt.Fprintf(os.Stderr, "error getting target family: %v\n", err) - slog.Error("failed to get target family", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get target family: %w", err) } targetModel, err := myTarget.GetModel() if err != nil { - fmt.Fprintf(os.Stderr, "error getting target model: %v\n", err) - slog.Error("failed to get target model", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get target model: %w", err) } if targetFamily != "6" || (targetFamily == "6" && targetModel != "173" && targetModel != "175" && targetModel != "221") { - err := fmt.Errorf("uncore frequency setting not supported on %s due to family/model mismatch", myTarget.GetName()) - slog.Error(err.Error()) - fmt.Fprintf(os.Stderr, "Error: failed to set uncore frequency: %v\n", err) - return + return fmt.Errorf("uncore frequency setting not supported on %s due to family/model mismatch", myTarget.GetName()) } type dieId struct { instance string @@ -870,9 +433,7 @@ func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float6 scripts = append(scripts, script.GetScriptByName(script.UncoreDieTypesFromTPMIScriptName)) outputs, err := script.RunScripts(myTarget, scripts, true, localTempDir) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to run scripts on target", slog.String("target", myTarget.GetName()), slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get uncore die types: %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") { @@ -906,20 +467,13 @@ func setUncoreDieFrequency(maxFreq bool, computeDie bool, uncoreFrequency float6 } _, err = runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set uncore frequency: %v\n", err) - return + return fmt.Errorf("failed to set uncore frequency: %w", err) } } + return nil } -func setUncoreFrequency(maxFreq bool, uncoreFrequency float64, myTarget target.Target, localTempDir string) { - var minmax string - if maxFreq { - minmax = "max" - } else { - minmax = "min" - } - fmt.Printf("set uncore %s frequency to %.1f GHz on %s\n", minmax, uncoreFrequency, myTarget.GetName()) +func setUncoreFrequency(maxFreq bool, uncoreFrequency float64, myTarget target.Target, localTempDir string) error { scripts := []script.ScriptDefinition{} scripts = append(scripts, script.ScriptDefinition{ Name: "get uncore frequency MSR", @@ -931,34 +485,23 @@ func setUncoreFrequency(maxFreq bool, uncoreFrequency float64, myTarget target.T }) outputs, err := script.RunScripts(myTarget, scripts, true, localTempDir) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to run scripts on target", slog.String("target", myTarget.GetName()), slog.String("error", err.Error())) - return + return fmt.Errorf("failed to read uncore frequency MSR: %w", err) } targetFamily, err := myTarget.GetFamily() if err != nil { - fmt.Fprintf(os.Stderr, "error getting target family: %v\n", err) - slog.Error("failed to get target family", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get target family: %w", err) } targetModel, err := myTarget.GetModel() if err != nil { - fmt.Fprintf(os.Stderr, "error getting target model: %v\n", err) - slog.Error("failed to get target model", slog.String("error", err.Error())) - return + return fmt.Errorf("failed to get target model: %w", err) } if targetFamily != "6" || (targetFamily == "6" && (targetModel == "173" || targetModel == "175" || targetModel == "221")) { // not Intel || not GNR, SRF, CWF - err := fmt.Errorf("uncore frequency setting not supported on %s due to family/model mismatch", myTarget.GetName()) - slog.Error(err.Error()) - fmt.Fprintf(os.Stderr, "Error: failed to set uncore frequency: %v\n", err) - return + return fmt.Errorf("uncore frequency setting not supported on %s due to family/model mismatch", myTarget.GetName()) } msrHex := strings.TrimSpace(outputs["get uncore frequency MSR"].Stdout) msrInt, err := strconv.ParseInt(msrHex, 16, 0) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to read or parse msr value", slog.String("msr", msrHex), slog.String("error", err.Error())) - return + return fmt.Errorf("failed to read uncore frequency MSR: %w", err) } newFreq := uint64((uncoreFrequency * 1000) / 100) var newVal uint64 @@ -983,12 +526,12 @@ func setUncoreFrequency(maxFreq bool, uncoreFrequency float64, myTarget target.T } _, err = runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set uncore frequency: %v\n", err) + return fmt.Errorf("failed to set uncore frequency: %w", err) } + return nil } -func setPower(power int, myTarget target.Target, localTempDir string) { - fmt.Printf("set power to %d Watts on %s\n", power, myTarget.GetName()) +func setTDP(power int, myTarget target.Target, localTempDir string) error { readScript := script.ScriptDefinition{ Name: "get power MSR", ScriptTemplate: "rdmsr 0x610", @@ -999,14 +542,12 @@ func setPower(power int, myTarget target.Target, localTempDir string) { } readOutput, err := script.RunScript(myTarget, readScript, localTempDir) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to run script on target", slog.String("target", myTarget.GetName()), slog.String("error", err.Error())) + return fmt.Errorf("failed to read power MSR: %w", err) } else { msrHex := strings.TrimSpace(readOutput.Stdout) msrInt, err := strconv.ParseInt(msrHex, 16, 0) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to parse msr value", slog.String("msr", msrHex), slog.String("error", err.Error())) + return fmt.Errorf("failed to parse power MSR: %w", err) } else { // mask out lower 14 bits newVal := uint64(msrInt) & 0xFFFFFFFFFFFFC000 @@ -1022,26 +563,24 @@ func setPower(power int, myTarget target.Target, localTempDir string) { } _, err := runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set power: %v\n", err) + return fmt.Errorf("failed to set power: %w", err) } } } + return nil } -func setEpb(epb int, myTarget target.Target, localTempDir string) { +func setEPB(epb int, myTarget target.Target, localTempDir string) error { epbSourceScript := script.GetScriptByName(script.EpbSourceScriptName) epbSourceOutput, err := runScript(myTarget, epbSourceScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to get EPB source: %v\n", 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 { - fmt.Fprintln(os.Stderr, err) - return + return fmt.Errorf("failed to parse EPB source: %w", err) } - fmt.Printf("set energy performance bias (EPB) to %d on %s\n", epb, myTarget.GetName()) var msr string var bitOffset uint if source == 0 { // 0 means the EPB is controlled by the OS @@ -1061,13 +600,11 @@ func setEpb(epb int, myTarget target.Target, localTempDir string) { } readOutput, err := runScript(myTarget, readScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to read MSR %s: %v\n", 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 { - fmt.Fprintln(os.Stderr, 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) @@ -1084,12 +621,12 @@ func setEpb(epb int, myTarget target.Target, localTempDir string) { } _, err = runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set EPB: %v\n", err) + return fmt.Errorf("failed to set EPB: %w", err) } + return nil } -func setEpp(epp int, myTarget target.Target, localTempDir string) { - fmt.Printf("set energy performance profile (EPP) to %d on %s\n", epp, myTarget.GetName()) +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 @@ -1104,13 +641,11 @@ func setEpp(epp int, myTarget target.Target, localTempDir string) { } stdout, err := runScript(myTarget, getScript, localTempDir) if err != nil { - 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 { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to parse msr value", slog.String("msr", stdout), slog.String("error", err.Error())) - 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 @@ -1127,8 +662,7 @@ func setEpp(epp int, myTarget target.Target, localTempDir string) { } _, err = runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set EPP: %v\n", 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 @@ -1142,14 +676,11 @@ func setEpp(epp int, myTarget target.Target, localTempDir string) { } stdout, err = runScript(myTarget, getScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to get pkg EPP: %v\n", 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 { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to parse msr value", slog.String("msr", stdout), slog.String("error", err.Error())) - 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 @@ -1166,12 +697,12 @@ func setEpp(epp int, myTarget target.Target, localTempDir string) { } _, err = runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set pkg EPP: %v\n", err) + return fmt.Errorf("failed to set EPP pkg: %w", err) } + return nil } -func setGovernor(governor string, myTarget target.Target, localTempDir string) { - fmt.Printf("set governor to %s on %s\n", governor, myTarget.GetName()) +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), @@ -1179,21 +710,19 @@ func setGovernor(governor string, myTarget target.Target, localTempDir string) { } _, err := runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set governor: %v\n", err) + return fmt.Errorf("failed to set governor: %w", err) } + return nil } -func setElc(elc string, myTarget target.Target, localTempDir string) { - fmt.Printf("set efficiency latency control (ELC) mode to %s on %s\n", elc, myTarget.GetName()) +func setELC(elc string, myTarget target.Target, localTempDir string) error { var mode string if elc == elcOptions[0] { mode = "latency-optimized-mode" } else if elc == elcOptions[1] { mode = "default" } else { - fmt.Fprintf(os.Stderr, "invalid elc mode: %s\n", elc) - slog.Error("invalid elc mode", slog.String("elc", elc)) - return + return fmt.Errorf("invalid ELC mode: %s", elc) } setScript := script.ScriptDefinition{ Name: "set elc", @@ -1205,17 +734,15 @@ func setElc(elc string, myTarget target.Target, localTempDir string) { } _, err := runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set ELC mode: %v\n", err) + return fmt.Errorf("failed to set ELC mode: %w", err) } + return nil } -func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir string, prefetcherType string) { - fmt.Printf("set %s prefetcher to %sd on %s\n", prefetcherType, enableDisable, myTarget.GetName()) +func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir string, prefetcherType string) error { pf, err := report.GetPrefetcherDefByName(prefetcherType) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to get prefetcher definition: %v\n", err) - slog.Error("failed to get prefetcher definition", slog.String("prefetcher", prefetcherType), slog.String("error", err.Error())) - 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 @@ -1225,21 +752,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) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to run scripts on target", slog.String("target", myTarget.GetName()), slog.String("error", err.Error())) - return + return fmt.Errorf("failed to run target identification scripts on target: %w", err) } uarch := report.UarchFromOutput(outputs) if uarch == "" { - fmt.Fprintln(os.Stderr, "failed to get microarchitecture") - slog.Error("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]) { - fmt.Fprintf(os.Stderr, "prefetcher %s is not supported on %s\n", prefetcherType, uarch) - slog.Error("prefetcher not supported on target", slog.String("prefetcher", prefetcherType), slog.String("uarch", 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{ @@ -1252,14 +773,11 @@ func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir st } stdout, err := runScript(myTarget, getScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to get prefetcher MSR: %v\n", err) - return + return fmt.Errorf("failed to read prefetcher MSR: %w", err) } msrValue, err := strconv.ParseUint(strings.TrimSpace(stdout), 16, 64) if err != nil { - fmt.Fprintln(os.Stderr, err) - slog.Error("failed to parse msr value", slog.String("msr", stdout), slog.String("error", err.Error())) - 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 int @@ -1268,9 +786,7 @@ func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir st } else if enableDisable == prefetcherOptions[1] { bitVal = 1 } else { - fmt.Fprintf(os.Stderr, "invalid prefetcher setting: %s\n", enableDisable) - slog.Error("invalid prefetcher setting", slog.String("prefetcher", enableDisable)) - return + return fmt.Errorf("invalid prefetcher setting: %s", enableDisable) } // mask out the prefetcher bit maskedValue := msrValue &^ (1 << pf.Bit) @@ -1287,8 +803,9 @@ func setPrefetcher(enableDisable string, myTarget target.Target, localTempDir st } _, err = runScript(myTarget, setScript, localTempDir) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to set %s prefetcher: %v\n", prefetcherType, err) + return fmt.Errorf("failed to set %s prefetcher: %w", prefetcherType, err) } + return nil } func runScript(myTarget target.Target, myScript script.ScriptDefinition, localTempDir string) (string, error) { diff --git a/cmd/config/flag.go b/cmd/config/flag.go new file mode 100644 index 00000000..8c4ca109 --- /dev/null +++ b/cmd/config/flag.go @@ -0,0 +1,72 @@ +package config + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "perfspect/internal/target" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// flagDefinition is a struct that defines a command line flag. +type flagDefinition struct { + pflag *pflag.Flag + intSetFunc func(int, target.Target, string) error + floatSetFunc func(float64, target.Target, string) error + stringSetFunc func(string, target.Target, string) error + validationFunc func(cmd *cobra.Command) bool + validationDescription string +} + +// GetName returns the name of the flag. +func (f *flagDefinition) GetName() string { + return f.pflag.Name +} + +// GetType returns the type of the flag. +func (f *flagDefinition) GetType() string { + return f.pflag.Value.Type() +} + +// GetValueAsString returns the value of the flag as a string. +func (f *flagDefinition) GetValueAsString() string { + return f.pflag.Value.String() +} + +// newIntFlag creates a new integer flag and adds it to the command. +func newIntFlag(cmd *cobra.Command, name string, defaultValue int, setFunc func(int, target.Target, string) error, help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition { + cmd.Flags().Int(name, defaultValue, help) + pFlag := cmd.Flags().Lookup(name) + return flagDefinition{ + pflag: pFlag, + intSetFunc: setFunc, + validationFunc: validationFunc, + validationDescription: validationDescription, + } +} + +// newInt64Flag creates a new int64 flag and adds it to the command. +func newFloat64Flag(cmd *cobra.Command, name string, defaultValue float64, setFunc func(float64, target.Target, string) error, help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition { + cmd.Flags().Float64(name, defaultValue, help) + pFlag := cmd.Flags().Lookup(name) + return flagDefinition{ + pflag: pFlag, + floatSetFunc: setFunc, + validationFunc: validationFunc, + validationDescription: validationDescription, + } +} + +// 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) error, help string, validationDescription string, validationFunc func(cmd *cobra.Command) bool) flagDefinition { + cmd.Flags().String(name, defaultValue, help) + pFlag := cmd.Flags().Lookup(name) + return flagDefinition{ + pflag: pFlag, + stringSetFunc: setFunc, + validationFunc: validationFunc, + validationDescription: validationDescription, + } +} diff --git a/cmd/config/flag_groups.go b/cmd/config/flag_groups.go new file mode 100644 index 00000000..309c2e81 --- /dev/null +++ b/cmd/config/flag_groups.go @@ -0,0 +1,328 @@ +package config + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "fmt" + "os" + "perfspect/internal/common" + "perfspect/internal/report" + "perfspect/internal/target" + "slices" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// flagGroup - structure to hold a group of flags +// groups are used to organize the flags for display in the help message +type flagGroup struct { + name string + flags []flagDefinition +} + +// flagGroups - list of flag groups +// initialized by initializeFlags +// and used by the config command +var flagGroups = []flagGroup{} + +// flag group names +const ( + flagGroupGeneralName = "General Options" + flagGroupUncoreFrequencyName = "Uncore Frequency Options" + flagGroupPrefetcherName = "Prefetcher Options" +) + +// general flag names +const ( + flagCoreCountName = "cores" + flagLLCSizeName = "llc" + flagAllCoreMaxFrequencyName = "core-max" + flagTDPName = "tdp" + flagEPBName = "epb" + flagEPPName = "epp" + flagGovernorName = "gov" + flagELCName = "elc" +) + +// uncore frequency flag names +const ( + flagUncoreMaxFrequencyName = "uncore-max" + flagUncoreMinFrequencyName = "uncore-min" + flagUncoreMaxComputeFrequencyName = "uncore-max-compute" + flagUncoreMinComputeFrequencyName = "uncore-min-compute" + flagUncoreMaxIOFrequencyName = "uncore-max-io" + flagUncoreMinIOFrequencyName = "uncore-min-io" +) + +// prefetcher flag names +const ( + flagPrefetcherL2HWName = "pref-l2hw" + flagPrefetcherL2AdjName = "pref-l2adj" + flagPrefetcherDCUHWName = "pref-dcuhw" + flagPrefetcherDCUIPName = "pref-dcuip" + flagPrefetcherDCUNPName = "pref-dcunp" + flagPrefetcherAMPName = "pref-amp" + flagPrefetcherLLCPPName = "pref-llcpp" + flagPrefetcherAOPName = "pref-aop" + flagPrefetcherHomelessName = "pref-homeless" + flagPrefetcherLLCName = "pref-llc" +) + +// governorOptions - list of valid governor options +var governorOptions = []string{"performance", "powersave"} + +// elcOptions - list of valid elc options +var elcOptions = []string{"latency-optimized", "default"} + +// prefetcherOptions - list of valid prefetcher options +var prefetcherOptions = []string{"enable", "disable"} + +// initializeFlags initializes the command line flags for the config command +// the global flagGroups variable is used to store the flags +func initializeFlags(cmd *cobra.Command) { + // general options + group := flagGroup{name: flagGroupGeneralName, flags: []flagDefinition{}} + group.flags = append(group.flags, + newIntFlag(cmd, flagCoreCountName, 0, setCoreCount, "number of physical cores per processor", "greater than 0", + func(cmd *cobra.Command) bool { value, _ := cmd.Flags().GetInt(flagCoreCountName); return value > 0 }), + newFloat64Flag(cmd, flagLLCSizeName, 0, setLlcSize, "LLC size in MB", "greater than 0", + func(cmd *cobra.Command) bool { value, _ := cmd.Flags().GetFloat64(flagLLCSizeName); return value > 0 }), + newFloat64Flag(cmd, flagAllCoreMaxFrequencyName, 0, setCoreFrequency, "all-core max frequency in GHz", "greater than 0.1", + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetFloat64(flagAllCoreMaxFrequencyName) + return value > 0.1 + }), + 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 }), + 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) + return value >= 0 && value <= 15 + }), + newIntFlag(cmd, flagEPPName, 0, setEPP, "energy perf profile from best performance (0) to most power savings (255)", "0-255", + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetInt(flagEPPName) + return value >= 0 && value <= 255 + }), + newStringFlag(cmd, flagGovernorName, "", setGovernor, "CPU scaling governor ("+strings.Join(governorOptions, ", ")+")", strings.Join(governorOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagGovernorName) + return slices.Contains(governorOptions, value) + }), + newStringFlag(cmd, flagELCName, "", setELC, "Efficiency Latency Control ("+strings.Join(elcOptions, ", ")+") [SRF+]", strings.Join(elcOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagELCName) + return slices.Contains(elcOptions, value) + })) + flagGroups = append(flagGroups, group) + // uncore frequency options + group = flagGroup{name: flagGroupUncoreFrequencyName, flags: []flagDefinition{}} + group.flags = append(group.flags, + newFloat64Flag(cmd, flagUncoreMaxFrequencyName, 0, + 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 { + value, _ := cmd.Flags().GetFloat64(flagUncoreMaxFrequencyName) + return value > 0.1 + }), + newFloat64Flag(cmd, flagUncoreMinFrequencyName, 0, + 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 { + value, _ := cmd.Flags().GetFloat64(flagUncoreMinFrequencyName) + return value > 0.1 + }), + newFloat64Flag(cmd, flagUncoreMaxComputeFrequencyName, 0, + 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 { + value, _ := cmd.Flags().GetFloat64(flagUncoreMaxComputeFrequencyName) + return value > 0.1 + }), + newFloat64Flag(cmd, flagUncoreMinComputeFrequencyName, 0, + 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 { + value, _ := cmd.Flags().GetFloat64(flagUncoreMinComputeFrequencyName) + return value > 0.1 + }), + newFloat64Flag(cmd, flagUncoreMaxIOFrequencyName, 0, + 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 { + value, _ := cmd.Flags().GetFloat64(flagUncoreMaxIOFrequencyName) + return value > 0.1 + }), + newFloat64Flag(cmd, flagUncoreMinIOFrequencyName, 0, + 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 { + value, _ := cmd.Flags().GetFloat64(flagUncoreMinIOFrequencyName) + return value > 0.1 + })) + flagGroups = append(flagGroups, group) + // prefetcher options + group = flagGroup{name: flagGroupPrefetcherName, flags: []flagDefinition{}} + group.flags = append(group.flags, + newStringFlag(cmd, flagPrefetcherL2HWName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherL2HWName) + }, + "L2 hardware prefetcher ("+strings.Join(prefetcherOptions, ", ")+")", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherL2HWName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherL2AdjName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherL2AdjName) + }, + "L2 adjacent cache line prefetcher ("+strings.Join(prefetcherOptions, ", ")+")", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherL2AdjName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherDCUHWName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherDCUHWName) + }, + "DCU hardware prefetcher ("+strings.Join(prefetcherOptions, ", ")+")", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherDCUHWName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherDCUIPName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherDCUIPName) + }, + "DCU instruction pointer prefetcher ("+strings.Join(prefetcherOptions, ", ")+")", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherDCUIPName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherDCUNPName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherDCUNPName) + }, + "DCU next page prefetcher ("+strings.Join(prefetcherOptions, ", ")+")", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherDCUNPName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherAMPName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherAMPName) + }, + "Adaptive multipath probability prefetcher ("+strings.Join(prefetcherOptions, ", ")+") [SPR,EMR,GNR]", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherAMPName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherLLCPPName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherLLCPPName) + }, + "LLC page prefetcher ("+strings.Join(prefetcherOptions, ", ")+") [GNR]", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherLLCPPName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherAOPName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherAOPName) + }, + "Array of pointers prefetcher ("+strings.Join(prefetcherOptions, ", ")+") [GNR]", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherAOPName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherHomelessName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherHomelessName) + }, + "Homeless prefetcher ("+strings.Join(prefetcherOptions, ", ")+") [SPR,EMR,GNR]", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherHomelessName) + return slices.Contains(prefetcherOptions, value) + }), + newStringFlag(cmd, flagPrefetcherLLCName, "", + func(value string, myTarget target.Target, localTempDir string) error { + return setPrefetcher(value, myTarget, localTempDir, report.PrefetcherLLCName) + }, + "Last level cache prefetcher ("+strings.Join(prefetcherOptions, ", ")+") [SPR,EMR,GNR]", strings.Join(prefetcherOptions, ", "), + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString(flagPrefetcherLLCName) + return slices.Contains(prefetcherOptions, value) + })) + flagGroups = append(flagGroups, group) + + common.AddTargetFlags(Cmd) + Cmd.SetUsageFunc(usageFunc) +} + +// usageFunc prints the usage information for the command +func usageFunc(cmd *cobra.Command) error { + cmd.Printf("Usage: %s [flags]\n\n", cmd.CommandPath()) + cmd.Printf("Examples:\n%s\n\n", cmd.Example) + cmd.Println("Flags:") + for _, group := range flagGroups { + cmd.Printf(" %s:\n", group.name) + for _, flag := range group.flags { + cmd.Printf(" --%-20s %s\n", flag.GetName(), flag.pflag.Usage) + } + } + + 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.Parent().PersistentFlags().VisitAll(func(pf *pflag.Flag) { + flagDefault := "" + if cmd.Parent().PersistentFlags().Lookup(pf.Name).DefValue != "" { + flagDefault = fmt.Sprintf(" (default: %s)", cmd.Flags().Lookup(pf.Name).DefValue) + } + cmd.Printf(" --%-20s %s%s\n", pf.Name, pf.Usage, flagDefault) + }) + return nil +} + +// validateFlags validates the command line flags for the config command +// operates on the global flagGroups variable +func validateFlags(cmd *cobra.Command, args []string) error { + for _, group := range flagGroups { + for _, flag := range group.flags { + if cmd.Flags().Lookup(flag.GetName()).Changed && flag.validationFunc != nil { + if !flag.validationFunc(cmd) { + err := fmt.Errorf("invalid flag value, --%s %s, valid values are %s", flag.GetName(), flag.GetValueAsString(), flag.validationDescription) + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + cmd.SilenceUsage = true + return err + } + } + } + } + // common target flags + if err := common.ValidateTargetFlags(cmd); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + return err + } + return nil +} diff --git a/cmd/config/flag_groups_test.go b/cmd/config/flag_groups_test.go new file mode 100644 index 00000000..e8bfb77f --- /dev/null +++ b/cmd/config/flag_groups_test.go @@ -0,0 +1,57 @@ +package config + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "bytes" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestValidateFlags(t *testing.T) { + // Create a mock command + cmd := &cobra.Command{ + Use: "test", + Run: func(cmd *cobra.Command, args []string) {}, + } + // Mock flag groups and flags + flagGroups = []flagGroup{} + group := flagGroup{ + name: "testGroup", + flags: []flagDefinition{}, + } + group.flags = append(group.flags, newStringFlag(cmd, + "testFlag", + "", + nil, + "A test flag", + "valid value", + func(cmd *cobra.Command) bool { + value, _ := cmd.Flags().GetString("testFlag") + return value == "validValue" + })) + flagGroups = append(flagGroups, group) + + // Test case: Invalid flag value + t.Run("InvalidFlagValue", func(t *testing.T) { + _ = cmd.Flags().Set("testFlag", "invalidValue") + var stderr bytes.Buffer + cmd.SetErr(&stderr) + + err := validateFlags(cmd, []string{}) + assert.Error(t, err) + }) + + // Test case: Valid flag value + t.Run("ValidFlagValue", func(t *testing.T) { + _ = cmd.Flags().Set("testFlag", "validValue") + var stderr bytes.Buffer + cmd.SetErr(&stderr) + + err := validateFlags(cmd, []string{}) + assert.NoError(t, err) + }) +} diff --git a/cmd/config/flag_test.go b/cmd/config/flag_test.go new file mode 100644 index 00000000..8e28b847 --- /dev/null +++ b/cmd/config/flag_test.go @@ -0,0 +1,60 @@ +package config + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" +) + +func TestFlagDefinition_GetName(t *testing.T) { + // Create a mock pflag.Flag + mockFlag := &pflag.Flag{ + Name: "test-flag", + } + + // Create a flagDefinition instance with the mock flag + flagDef := flagDefinition{ + pflag: mockFlag, + } + + // Call GetName and verify the result + result := flagDef.GetName() + assert.Equal(t, "test-flag", result, "GetName should return the correct flag name") +} +func TestFlagDefinition_GetType(t *testing.T) { + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + flagSet.String("test-flag", "default", "help") + // Lookup the flag to get the pflag.Flag instance + mockFlag := flagSet.Lookup("test-flag") + if mockFlag == nil { + t.Fatalf("Failed to create mock flag") + } + // Create a flagDefinition instance with the mock flag + flagDef := flagDefinition{ + pflag: mockFlag, + } + // Call GetType and verify the result + result := flagDef.GetType() + assert.Equal(t, "string", result, "GetType should return the correct flag type") +} + +func TestFlagDefinition_GetValueAsString(t *testing.T) { + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + flagSet.String("test-flag", "default", "help") + // Lookup the flag to get the pflag.Flag instance + mockFlag := flagSet.Lookup("test-flag") + if mockFlag == nil { + t.Fatalf("Failed to create mock flag") + } + // Create a flagDefinition instance with the mock flag + flagDef := flagDefinition{ + pflag: mockFlag, + } + // Call GetValueAsString and verify the result + result := flagDef.GetValueAsString() + assert.Equal(t, "default", result, "GetValueAsString should return the correct flag value as string") +} diff --git a/cmd/lock/lock.go b/cmd/lock/lock.go index e5ce4e54..78571656 100755 --- a/cmd/lock/lock.go +++ b/cmd/lock/lock.go @@ -137,12 +137,16 @@ func validateFlags(cmd *cobra.Command, args []string) error { return err } } - if flagDuration <= 0 { err := fmt.Errorf("duration must be greater than 0") fmt.Fprintf(os.Stderr, "Error: %v\n", err) return err } + if flagFrequency <= 0 { + err := fmt.Errorf("frequency must be greater than 0") + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + return err + } // common target flags if err := common.ValidateTargetFlags(cmd); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/cmd/telemetry/telemetry.go b/cmd/telemetry/telemetry.go index 4cbd286d..979a5fe6 100644 --- a/cmd/telemetry/telemetry.go +++ b/cmd/telemetry/telemetry.go @@ -227,6 +227,11 @@ func validateFlags(cmd *cobra.Command, args []string) error { return err } } + if flagInterval < 1 { + err := fmt.Errorf("interval must be 1 or greater") + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + return err + } if flagDuration < 0 { err := fmt.Errorf("duration must be 0 or greater") fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/go.mod b/go.mod index da3fb3df..6cc2002b 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/deckarep/golang-set/v2 v2.8.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 + github.com/stretchr/testify v1.8.4 github.com/xuri/excelize/v2 v2.9.0 golang.org/x/term v0.31.0 golang.org/x/text v0.24.0 @@ -24,8 +25,10 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect @@ -33,4 +36,5 @@ require ( golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sys v0.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/common/common.go b/internal/common/common.go index 731dbc0b..e27e18d5 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -16,6 +16,7 @@ import ( "perfspect/internal/script" "perfspect/internal/target" "perfspect/internal/util" + "strings" "syscall" "slices" @@ -57,7 +58,7 @@ func (tso *TargetScriptOutputs) GetTableNames() []string { const ( TableNameInsights = "Insights" - TableNamePerfspect = "PerfSpect Version" + TableNamePerfspect = "PerfSpect" ) type Category struct { @@ -163,15 +164,7 @@ func (rc *ReportingCommand) Run() error { } } multiSpinner.Start() - // get the data we need to generate reports - orderedTargetScriptOutputs, err = outputsFromTargets(rc.Cmd, myTargets, rc.TableNames, rc.ScriptParams, multiSpinner.Status, localTempDir) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - slog.Error(err.Error()) - rc.Cmd.SilenceUsage = true - return err - } - // Collect indices of targets to remove + // remove targets that had errors var indicesToRemove []int for i := range targetErrs { if targetErrs[i] != nil { @@ -179,13 +172,12 @@ func (rc *ReportingCommand) Run() error { indicesToRemove = append(indicesToRemove, i) } } - // Remove targets in reverse order of indices to avoid shifting issues for i := len(indicesToRemove) - 1; i >= 0; i-- { myTargets = slices.Delete(myTargets, indicesToRemove[i], indicesToRemove[i]+1) } - // check if we have any remaining targets to run the scripts on - if len(myTargets) == 0 { - err := fmt.Errorf("no targets remain") + // collect data from targets + orderedTargetScriptOutputs, err = outputsFromTargets(rc.Cmd, myTargets, rc.TableNames, rc.ScriptParams, multiSpinner.Status, localTempDir) + if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) slog.Error(err.Error()) rc.Cmd.SilenceUsage = true @@ -194,6 +186,13 @@ func (rc *ReportingCommand) Run() error { // stop the progress indicator multiSpinner.Finish() fmt.Println() + // exit with error if no targets remain + if len(myTargets) == 0 { + err := fmt.Errorf("no successful targets found") + slog.Error(err.Error()) + rc.Cmd.SilenceUsage = true + return err + } } // we have output data so create the output directory err := CreateOutputDir(outputDir) @@ -355,6 +354,8 @@ func (rc *ReportingCommand) createReports(appContext AppContext, orderedTargetSc }, Fields: []report.Field{ {Name: "Version", Values: []string{appContext.Version}}, + {Name: "Args", Values: []string{strings.Join(os.Args, " ")}}, + {Name: "OutputDir", Values: []string{appContext.OutputDir}}, }, }) // create the report(s) diff --git a/internal/common/targets.go b/internal/common/targets.go index 40120bb6..5841cb16 100644 --- a/internal/common/targets.go +++ b/internal/common/targets.go @@ -18,6 +18,8 @@ import ( "strconv" "strings" + "slices" + "github.com/spf13/cobra" "golang.org/x/term" "gopkg.in/yaml.v2" @@ -132,26 +134,24 @@ func GetTargets(cmd *cobra.Command, needsElevatedPrivileges bool, failIfCantElev } // create a temp directory on each target for targetIdx, myTarget := range targets { + // if we already have an error for this target, skip it if targetErrs[targetIdx] != nil { continue } _, err := myTarget.CreateTempDirectory(targetTempDirRoot) if err != nil { - targetErrs[targetIdx] = fmt.Errorf("failed to create temp directory on target") + targetErrs[targetIdx] = fmt.Errorf("failed to create temp directory on target: %v", err) slog.Error(targetErrs[targetIdx].Error(), slog.String("target", myTarget.GetName()), slog.String("error", err.Error())) continue } - // confirm that the temp directory was not created on a file system mounted with noexec - noExec, err := isNoExec(myTarget, myTarget.GetTempDirectory()) + // confirm that the temp directory was created on a file system that was not mounted with noexec + noExec, err := isDirNoExec(myTarget, myTarget.GetTempDirectory()) if err != nil { // log the error but don't reject the target just in case our check is wrong - slog.Error("failed to check if temp directory is mounted on 'noexec' file system", slog.String("target", myTarget.GetName()), slog.String("error", err.Error())) - continue - } - if noExec { + slog.Warn("failed to check if temp directory is mounted on 'noexec' file system", slog.String("target", myTarget.GetName()), slog.String("error", err.Error())) + } else if noExec { targetErrs[targetIdx] = fmt.Errorf("target's temp directory must not be on a file system mounted with the 'noexec' option, override the default with --tempdir") slog.Error(targetErrs[targetIdx].Error(), slog.String("target", myTarget.GetName())) - continue } } return @@ -375,52 +375,118 @@ func getHostArchitecture() (string, error) { } } -// isNoExec checks if the temporary directory is on a file system that is mounted with noexec. -func isNoExec(t target.Target, tempDir string) (bool, error) { - dfCmd := exec.Command("df", "-P", tempDir) +// fieldFromDfpOutput parses the output of the `df -P ` command and returns the specified field value. +// example output: +// +// Filesystem 1024-blocks Used Available Capacity Mounted on +// /dev/sda2 1858388360 17247372 1747419536 1% / +// +// Returns the value of the specified field from the second line of the output. +func fieldFromDfpOutput(dfOutput string, fieldName string) (string, error) { + lines := strings.Split(dfOutput, "\n") + if len(lines) < 2 { + return "", fmt.Errorf("unexpected output from df command: %s", dfOutput) + } + // find the field index from the header + headerFields := strings.Fields(lines[0]) + fieldIndex := -1 + for i, field := range headerFields { + if field == fieldName { + fieldIndex = i + break + } + } + if fieldIndex == -1 { + return "", fmt.Errorf("field %s not found in df output", fieldName) + } + // get the value from the second line (the actual data) + dfFields := strings.Fields(lines[1]) + if len(dfFields) <= fieldIndex { + return "", fmt.Errorf("unexpected output format from df command: %s", dfOutput) + } + return dfFields[fieldIndex], nil +} + +type mountRecord struct { + fileSystem string + mountPoint string + typeName string + options []string +} + +// parseMountOutput parses the output of the `mount` command and returns a slice of mountRecord structs. +// e.g., "sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)" +func parseMountOutput(mountOutput string) ([]mountRecord, error) { + var mounts []mountRecord + for line := range strings.SplitSeq(mountOutput, "\n") { + if line == "" { + continue + } + re := regexp.MustCompile(`^([^ ]+) on ([^ ]+) type ([^ ]+) \((.*)\)$`) + matches := re.FindStringSubmatch(line) + if len(matches) != 5 { + return nil, fmt.Errorf("unexpected output format from mount command: %s", line) + } + // create a mountRecord struct and append it to the slice + mount := mountRecord{ + fileSystem: matches[1], + mountPoint: matches[2], + typeName: matches[3], + options: strings.Split(matches[4], ","), + } + mounts = append(mounts, mount) + } + return mounts, nil +} + +// isDirNoExec checks if the target directory is on a file system that is mounted with noexec. +func isDirNoExec(t target.Target, dir string) (bool, error) { + dfCmd := exec.Command("df", "-P", dir) dfOutput, _, _, err := t.RunCommand(dfCmd, 0, true) if err != nil { err = fmt.Errorf("failed to run df command: %w", err) return false, err } + filesystem, err := fieldFromDfpOutput(dfOutput, "Filesystem") + if err != nil { + return false, err + } + mountedOn, err := fieldFromDfpOutput(dfOutput, "Mounted") + if err != nil { + return false, err + } mountCmd := exec.Command("mount") mountOutput, _, _, err := t.RunCommand(mountCmd, 0, true) if err != nil { err = fmt.Errorf("failed to run mount command: %w", err) return false, err } - // Parse the output of `df` to extract the device name - lines := strings.Split(dfOutput, "\n") - if len(lines) < 2 { - return false, fmt.Errorf("unexpected output from df command: %s", dfOutput) + mounts, err := parseMountOutput(mountOutput) + if err != nil { + return false, err } - dfFields := strings.Fields(lines[1]) // Second line contains the device info - if len(dfFields) < 6 { - return false, fmt.Errorf("unexpected output format from df command: %s", dfOutput) + if len(mounts) == 0 { + return false, fmt.Errorf("no mount records found") } - filesystem := dfFields[0] - mountedOn := dfFields[5] - // Search for the device in the mount output and check for "noexec" - var found bool - for line := range strings.SplitSeq(mountOutput, "\n") { - mountFields := strings.Fields(line) - if len(mountFields) < 6 { - continue // Skip lines that don't have enough fields + // Check if the filesystem is mounted with noexec + foundFilesystem := false + foundMountPoint := false + for _, mount := range mounts { + if mount.fileSystem == filesystem { + foundFilesystem = true } - device := mountFields[0] - mountPoint := mountFields[2] - mountOptions := strings.Join(mountFields[5:], " ") - if device == filesystem && mountPoint == mountedOn { - found = true - if strings.Contains(mountOptions, "noexec") { - return true, nil // Found "noexec" for the device - } else { - break - } + if mount.mountPoint == mountedOn { + foundMountPoint = true } + if mount.fileSystem == filesystem && mount.mountPoint == mountedOn { + return slices.Contains(mount.options, "noexec"), nil + } + } + if foundMountPoint { + return false, fmt.Errorf("mount point %s is found but filesystem %s is not found in mount records", mountedOn, filesystem) } - if !found { - return false, fmt.Errorf("device %s not found in mount output", filesystem) + if foundFilesystem { + return false, fmt.Errorf("filesystem %s is found but mount point %s is not found in mount records", filesystem, mountedOn) } - return false, nil // "noexec" not found + return false, fmt.Errorf("filesystem %s and mount point %s are not found in mount records", filesystem, mountedOn) } diff --git a/internal/common/targets_test.go b/internal/common/targets_test.go new file mode 100644 index 00000000..27c6914a --- /dev/null +++ b/internal/common/targets_test.go @@ -0,0 +1,144 @@ +package common + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFieldFromDfpOutput(t *testing.T) { + tests := []struct { + name string + dfOutput string + fieldName string + expected string + expectError bool + }{ + { + name: "Valid field extraction", + dfOutput: `Filesystem 1024-blocks Used Available Capacity Mounted on +/dev/sda2 1858388360 17247372 1747419536 1% /`, + fieldName: "Available", + expected: "1747419536", + expectError: false, + }, + { + name: "Field not found", + dfOutput: `Filesystem 1024-blocks Used Available Capacity Mounted on +/dev/sda2 1858388360 17247372 1747419536 1% /`, + fieldName: "NonExistentField", + expected: "", + expectError: true, + }, + { + name: "Invalid df output format", + dfOutput: `Filesystem 1024-blocks Used Available Capacity Mounted on`, + fieldName: "Available", + expected: "", + expectError: true, + }, + { + name: "Field index out of range", + dfOutput: `Filesystem 1024-blocks Used Available Capacity Mounted on +/dev/sda2 1858388360 17247372`, + fieldName: "Capacity", + expected: "", + expectError: true, + }, + { + name: "Empty df output", + dfOutput: ``, + fieldName: "Available", + expected: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := fieldFromDfpOutput(tt.dfOutput, tt.fieldName) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} +func TestParseMountOutput(t *testing.T) { + tests := []struct { + name string + mountOutput string + expected []mountRecord + expectError bool + }{ + { + name: "Valid mount output", + mountOutput: `sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime) +tmpfs on /run type tmpfs (rw,nosuid,nodev,mode=755)`, + expected: []mountRecord{ + { + fileSystem: "sysfs", + mountPoint: "/sys", + typeName: "sysfs", + options: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, + }, + { + fileSystem: "tmpfs", + mountPoint: "/run", + typeName: "tmpfs", + options: []string{"rw", "nosuid", "nodev", "mode=755"}, + }, + }, + expectError: false, + }, + { + name: "Invalid mount output format", + mountOutput: `invalid output line +tmpfs on /run type tmpfs (rw,nosuid,nodev,mode=755)`, + expected: nil, + expectError: true, + }, + { + name: "Empty mount output", + mountOutput: ``, + expected: nil, + expectError: false, + }, + { + name: "Unexpected format in one line", + mountOutput: `sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime) +invalid line format`, + expected: nil, + expectError: true, + }, + { + name: "Single valid mount record", + mountOutput: `proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)`, + expected: []mountRecord{ + { + fileSystem: "proc", + mountPoint: "/proc", + typeName: "proc", + options: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, + }, + }, + expectError: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseMountOutput(tt.mountOutput) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} diff --git a/internal/report/prefetcher_defs.go b/internal/report/prefetcher_defs.go index 3ea436bc..733c592b 100644 --- a/internal/report/prefetcher_defs.go +++ b/internal/report/prefetcher_defs.go @@ -21,72 +21,85 @@ const ( MsrPrefetchers = 0x6d ) +const ( + PrefetcherL2HWName = "L2 HW" + PrefetcherL2AdjName = "L2 Adj" + PrefetcherDCUHWName = "DCU HW" + PrefetcherDCUIPName = "DCU IP" + PrefetcherDCUNPName = "DCU NP" + PrefetcherAMPName = "AMP" + PrefetcherLLCPPName = "LLCPP" + PrefetcherAOPName = "AOP" + PrefetcherHomelessName = "Homeless" + PrefetcherLLCName = "LLC" +) + var prefetcherDefinitions = []PrefetcherDefinition{ { - ShortName: "L2 HW", + ShortName: PrefetcherL2HWName, Description: "L2 Hardware (MLC Streamer) fetches additional lines of code or data into the L2 cache.", Msr: MsrPrefetchControl, Bit: 0, Uarchs: []string{"all"}, }, { - ShortName: "L2 Adj", + ShortName: PrefetcherL2AdjName, Description: "L2 Adjacent Cache Line (MLC Spatial) fetches the cache line that comprises a cache line pair.", Msr: MsrPrefetchControl, Bit: 1, Uarchs: []string{"all"}, }, { - ShortName: "DCU HW", + ShortName: PrefetcherDCUHWName, Description: "DCU Hardware (DCU Streamer) fetches the next cache line into the L1 cache.", Msr: MsrPrefetchControl, Bit: 2, Uarchs: []string{"all"}, }, { - ShortName: "DCU IP", + ShortName: PrefetcherDCUIPName, Description: "DCU Instruction Pointer prefetcher uses sequential load history to determine the cache lines to prefetch.", Msr: MsrPrefetchControl, Bit: 3, Uarchs: []string{"all"}, }, { - ShortName: "DCU NP", + ShortName: PrefetcherDCUNPName, Description: "DCU Next Page is an L1 data cache prefetcher.", Msr: MsrPrefetchControl, Bit: 4, Uarchs: []string{"all"}, }, { - ShortName: "AMP", + ShortName: PrefetcherAMPName, Description: "Adaptive Multipath Probability (MLC AMP) predicts access patterns based on previous patterns and fetches the corresponding cache lines into the L2 cache.", Msr: MsrPrefetchControl, Bit: 5, Uarchs: []string{"SPR", "EMR", "GNR"}, }, { - ShortName: "LLCPP", + ShortName: PrefetcherLLCPPName, Description: "Last Level Cache Page (MLC LLC Page) Prefetcher", Msr: MsrPrefetchControl, Bit: 6, Uarchs: []string{"GNR"}, }, { - ShortName: "AOP", + ShortName: PrefetcherAOPName, Description: "L2 Array of Pointers (DCU AOP) Prefetcher", Msr: MsrPrefetchControl, Bit: 7, Uarchs: []string{"GNR"}, }, { - ShortName: "Homeless", + ShortName: PrefetcherHomelessName, Description: "Homeless prefetch allows early fetch of the demand miss into the MLC when we don’t have enough resources to track this demand in the L1 cache.", Msr: MsrPrefetchers, Bit: 14, Uarchs: []string{"SPR", "EMR", "GNR"}, }, { - ShortName: "LLC", + ShortName: PrefetcherLLCName, Description: "Last level cache gives the core prefetcher the ability to prefetch data directly into the LLC without necessarily filling into the L1 and L2 caches first.", Msr: MsrPrefetchers, Bit: 42, diff --git a/internal/report/table_defs.go b/internal/report/table_defs.go index d1dbcd86..69fb5dec 100644 --- a/internal/report/table_defs.go +++ b/internal/report/table_defs.go @@ -2006,7 +2006,7 @@ func frequencyBenchmarkTableValues(outputs map[string]script.ScriptOutput) []Fie frequencyBuckets, err := getSpecFrequencyBuckets(outputs) if err == nil && len(frequencyBuckets) >= 2 { // get the frequencies from the buckets - specSSEFreqs, err = frequencyBucketsToFrequencies(frequencyBuckets) + specSSEFreqs, err = expandTurboFrequencies(frequencyBuckets, "sse") if err != nil { slog.Error("unable to convert buckets to counts", slog.String("error", err.Error())) return []Field{} @@ -2392,12 +2392,12 @@ func codePathFrequencyTableValues(outputs map[string]script.ScriptOutput) []Fiel func kernelLockAnalysisTableValues(outputs map[string]script.ScriptOutput) []Field { fields := []Field{ - {Name: "Hotspot without Callstack", Values: []string{sectionValueFromOutput(outputs, "perf_hotspot_no_children")}}, - {Name: "Hotspot with Callstack", Values: []string{sectionValueFromOutput(outputs, "perf_hotspot_callgraph")}}, - {Name: "Cache2Cache without Callstack", Values: []string{sectionValueFromOutput(outputs, "perf_c2c_no_children")}}, - {Name: "Cache2Cache with CallStack", Values: []string{sectionValueFromOutput(outputs, "perf_c2c_callgraph")}}, - {Name: "Lock Contention", Values: []string{sectionValueFromOutput(outputs, "perf_lock_contention")}}, - {Name: "Perf Package Path", Values: []string{sectionValueFromOutput(outputs, "perf_package_path")}}, + {Name: "Hotspot without Callstack", Values: []string{sectionValueFromOutput(outputs[script.ProfileKernelLockScriptName].Stdout, "perf_hotspot_no_children")}}, + {Name: "Hotspot with Callstack", Values: []string{sectionValueFromOutput(outputs[script.ProfileKernelLockScriptName].Stdout, "perf_hotspot_callgraph")}}, + {Name: "Cache2Cache without Callstack", Values: []string{sectionValueFromOutput(outputs[script.ProfileKernelLockScriptName].Stdout, "perf_c2c_no_children")}}, + {Name: "Cache2Cache with CallStack", Values: []string{sectionValueFromOutput(outputs[script.ProfileKernelLockScriptName].Stdout, "perf_c2c_callgraph")}}, + {Name: "Lock Contention", Values: []string{sectionValueFromOutput(outputs[script.ProfileKernelLockScriptName].Stdout, "perf_lock_contention")}}, + {Name: "Perf Package Path", Values: []string{strings.TrimSpace(sectionValueFromOutput(outputs[script.ProfileKernelLockScriptName].Stdout, "perf_package_path"))}}, } return fields } diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index 1e93df1a..3f75b111 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -8,7 +8,6 @@ package report import ( "encoding/csv" "fmt" - "log" "log/slog" "regexp" "sort" @@ -17,6 +16,7 @@ import ( "time" "perfspect/internal/script" + "perfspect/internal/util" "slices" ) @@ -195,48 +195,30 @@ func baseFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { return "" } -// convertHexStringToDecimals converts a hex string to a slice of decimal values. -// -// formats: -// - "0x1212121212121212" -// - "1212121212121212" -// we need two hex characters for each decimal value -// some input strings may need to be padded with a leading zero -// always return a slice of 8 decimal values -func convertHexStringToDecimals(hexStr string) (decVals []int, err error) { - hexStr = strings.TrimPrefix(hexStr, "0x") - hexStr = strings.TrimSpace(hexStr) - // no more than 16 characters - if len(hexStr) > 16 { - err = fmt.Errorf("hex string too long: %s", hexStr) - return - } - // pad up to 16 characters - for range 16 - len(hexStr) { - hexStr = "0" + hexStr - } - re := regexp.MustCompile(`[0-9a-fA-F][0-9a-fA-F]`) - hexVals := re.FindAll([]byte(hexStr), -1) - if hexVals == nil { - err = fmt.Errorf("no hex values found in hex string") - return +// getFrequenciesFromHex +func getFrequenciesFromHex(hex string) ([]int, error) { + freqs, err := util.HexToIntList(hex) + if err != nil { + return nil, err } - if len(hexVals) != 8 { - err = fmt.Errorf("expected 8 hex values, got %d", len(hexVals)) - return + // 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 } - decVals = make([]int, len(hexVals)) - decValsIndex := len(decVals) - 1 - for _, hexVal := range hexVals { - var decVal int64 - decVal, err = strconv.ParseInt(string(hexVal), 16, 0) - if err != nil { - return - } - decVals[decValsIndex] = int(decVal) - decValsIndex-- + if len(bucketSizes) != 8 { + err = fmt.Errorf("expected 8 bucket sizes, got %d", len(bucketSizes)) + return nil, err } - return + // reverse the order of the core counts + slices.Reverse(bucketSizes) + return bucketSizes, nil } // getSpecFrequencyBuckets @@ -273,8 +255,11 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string if len(values) != len(fieldNames) { return nil, fmt.Errorf("unexpected output format") } - // get list of buckets - bucketCoreCounts, _ := convertHexStringToDecimals(values[0]) + // 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 @@ -309,9 +294,9 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string var freqs []int if isaHex != "0" { var err error - freqs, err = convertHexStringToDecimals(isaHex) + freqs, err = getFrequenciesFromHex(isaHex) if err != nil { - return nil, err + 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 @@ -363,6 +348,46 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string 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 @@ -1756,36 +1781,6 @@ func cveInfoFromOutput(outputs map[string]script.ScriptOutput) [][]string { return cves } -/* "1,3-5,8" -> [1,3,4,5,8] */ -func expandCPUList(cpuList string) (cpus []int) { - if cpuList != "" { - for token := range strings.SplitSeq(cpuList, ",") { - if strings.Contains(token, "-") { - subTokens := strings.Split(token, "-") - if len(subTokens) == 2 { - begin, errA := strconv.Atoi(subTokens[0]) - end, errB := strconv.Atoi(subTokens[1]) - if errA != nil || errB != nil { - slog.Warn("Failed to parse CPU affinity", slog.String("cpuList", cpuList)) - return - } - for i := begin; i <= end; i++ { - cpus = append(cpus, i) - } - } - } else { - cpu, err := strconv.Atoi(token) - if err != nil { - slog.Warn("CPU isn't an integer!", slog.String("cpuList", cpuList)) - return - } - cpus = append(cpus, cpu) - } - } - } - return -} - func turbostatSummaryRows(turboStatScriptOutput script.ScriptOutput, fieldNames []string) ([][]string, error) { var fieldValues [][]string // initialize indices with -1 @@ -1870,7 +1865,11 @@ func nicIRQMappingsFromOutput(outputs map[string]script.ScriptOutput) [][]string continue } cpuList := tokens[1] - cpus := expandCPUList(cpuList) + cpus, err := util.SelectiveIntRangeToIntList(cpuList) + if err != nil { + slog.Warn("failed to parse CPU list", slog.String("cpuList", cpuList), slog.String("error", err.Error())) + continue + } for _, cpu := range cpus { cpuIRQMappings[cpu] = append(cpuIRQMappings[cpu], irq) } @@ -2024,39 +2023,68 @@ func systemSummaryFromOutput(outputs map[string]script.ScriptOutput) string { return fmt.Sprintf(template, socketCount, cpuModel, coreCount, tdp, htOnOff, turboOnOff, installedMem, biosVersion, uCodeVersion, nics, disks, operatingSystem, kernelVersion, date) } -func getSectionsFromOutput(outputs map[string]script.ScriptOutput, scriptName string) map[string]string { - reHeader := regexp.MustCompile(`^##########\s+(.+)\s+##########$`) - sections := make(map[string]string, 0) - var header string - var sectionLines []string - lines := strings.Split(outputs[scriptName].Stdout, "\n") - lineCount := len(lines) - if lineCount == 1 && lines[0] == "" { - return sections - } - for idx, line := range lines { - match := reHeader.FindStringSubmatch(line) +// getSectionsFromOutput parses output into sections, where the section name +// is the key in a map and the section content is the value +// sections are delimited by lines of the form ##########
########## +// example: +// ##########
########## +//
+//
+// ##########
########## +//
+// +// returns a map of section name to section content +// if the output is empty or contains no section headers, returns an empty map +// if a section contains no content, the value for that section is an empty string +func getSectionsFromOutput(output string) map[string]string { + sections := make(map[string]string) + re := regexp.MustCompile(`^########## (.+?) ##########$`) + var sectionName string + for line := range strings.SplitSeq(output, "\n") { + // check if the line is a section header + match := re.FindStringSubmatch(line) if match != nil { - if header != "" { - sections[header] = strings.Join(sectionLines, "\n") - sectionLines = []string{} - } - header = match[1] - if _, ok := sections[header]; ok { - log.Panic("can't have same header twice") + // if the section name isn't in the map yet, add it + if _, ok := sections[match[1]]; !ok { + sections[match[1]] = "" } + // save the section name + sectionName = match[1] continue } - sectionLines = append(sectionLines, line) - if idx == lineCount-1 { - sections[header] = strings.Join(sectionLines, "\n") + if sectionName != "" { + sections[sectionName] += line + "\n" } } return sections } +// sectionValueFromOutput returns the content of a section from the output +// if the section doesn't exist, returns an empty string +// if the section exists but has no content, returns an empty string +func sectionValueFromOutput(output string, sectionName string) string { + sections := getSectionsFromOutput(output) + if len(sections) == 0 { + slog.Warn("no sections in output") + return "" + } + if _, ok := sections[sectionName]; !ok { + slog.Warn("section not found in output", slog.String("section", sectionName)) + return "" + } + if sections[sectionName] == "" { + slog.Warn("No content for section:", slog.String("section", sectionName)) + return "" + } + return sections[sectionName] +} + func javaFoldedFromOutput(outputs map[string]script.ScriptOutput) string { - sections := getSectionsFromOutput(outputs, script.ProfileJavaScriptName) + sections := getSectionsFromOutput(outputs[script.ProfileJavaScriptName].Stdout) + if len(sections) == 0 { + slog.Warn("no sections in java profiling output") + return "" + } javaFolded := make(map[string]string) re := regexp.MustCompile(`^async-profiler (\d+) (.*)$`) for header, stacks := range sections { @@ -2091,7 +2119,11 @@ func javaFoldedFromOutput(outputs map[string]script.ScriptOutput) string { } func systemFoldedFromOutput(outputs map[string]script.ScriptOutput) string { - sections := getSectionsFromOutput(outputs, script.ProfileSystemScriptName) + sections := getSectionsFromOutput(outputs[script.ProfileSystemScriptName].Stdout) + if len(sections) == 0 { + slog.Warn("no sections in system profiling output") + return "" + } var dwarfFolded, fpFolded string for header, content := range sections { if header == "perf_dwarf" { @@ -2109,13 +2141,3 @@ func systemFoldedFromOutput(outputs map[string]script.ScriptOutput) string { } return folded } - -func sectionValueFromOutput(outputs map[string]script.ScriptOutput, sectionName string) string { - sections := getSectionsFromOutput(outputs, script.ProfileKernelLockScriptName) - - value := sections[sectionName] - if value == "" { - slog.Warn("No content for section:", slog.String("warning", sectionName)) - } - return value -} diff --git a/internal/report/table_helpers_benchmarking.go b/internal/report/table_helpers_benchmarking.go index 7942cea7..0e8966e6 100644 --- a/internal/report/table_helpers_benchmarking.go +++ b/internal/report/table_helpers_benchmarking.go @@ -222,51 +222,3 @@ func avxTurboFrequenciesFromOutput(output string) (instructionFreqs map[string][ } return } - -// frequencyBucketsToFrequencies creates a slice of SSE frequencies from the spec core frequency buckets -// input: the first column is the bucket range, e.g. 1-44, the second (or third) column is the spec sse frequency -func frequencyBucketsToFrequencies(specCoreFreqs [][]string) (freqs []string, err error) { - if len(specCoreFreqs) < 2 || len(specCoreFreqs[0]) < 2 { - err = fmt.Errorf("unable to parse core frequency buckets") - return - } - rangeIdx := 0 // the first column is the bucket, e.g., 1-44 - var sseIdx int - for i := range specCoreFreqs[0] { - if strings.Contains(strings.ToUpper(specCoreFreqs[0][i]), "SSE") { - sseIdx = i - break - } - } - if sseIdx == 0 { - err = fmt.Errorf("unable to find SSE frequency column") - return - } - for i := 1; i < len(specCoreFreqs); i++ { - bucketRange := strings.TrimSpace(specCoreFreqs[i][rangeIdx]) - // parse the bucketParts into start/end parts - bucketParts := strings.Split(bucketRange, "-") - if len(bucketParts) != 2 { - err = fmt.Errorf("unable to parse bucket range %s", bucketRange) - return - } - // parse the start and end parts into integers - var start int - var end int - start, err = strconv.Atoi(strings.TrimSpace(bucketParts[0])) - if err != nil { - err = fmt.Errorf("unable to parse start %s", bucketParts[0]) - return - } - end, err = strconv.Atoi(strings.TrimSpace(bucketParts[1])) - if err != nil { - err = fmt.Errorf("unable to parse end %s", bucketParts[1]) - return - } - // add the core count to the list - for j := start; j <= end; j++ { - freqs = append(freqs, specCoreFreqs[i][sseIdx]) - } - } - return -} diff --git a/internal/report/table_helpers_test.go b/internal/report/table_helpers_test.go new file mode 100644 index 00000000..fe9803a5 --- /dev/null +++ b/internal/report/table_helpers_test.go @@ -0,0 +1,353 @@ +package report + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "reflect" + "testing" +) + +func TestGetFrequenciesFromMSR(t *testing.T) { + tests := []struct { + name string + msr string + want []int + expectErr bool + }{ + { + name: "Valid MSR with multiple frequencies", + msr: "0x1A2B3C4D", + want: []int{0x4D, 0x3C, 0x2B, 0x1A}, + expectErr: false, + }, + { + name: "Valid MSR with single frequency", + msr: "0x1A", + want: []int{0x1A}, + expectErr: false, + }, + { + name: "Empty MSR string", + msr: "", + want: nil, + expectErr: true, + }, + { + name: "Invalid MSR string", + msr: "invalid_hex", + want: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getFrequenciesFromHex(tt.msr) + if (err != nil) != tt.expectErr { + t.Errorf("getFrequenciesFromMSR() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getFrequenciesFromMSR() = %v, want %v", got, tt.want) + } + }) + } +} +func TestGetBucketSizesFromMSR(t *testing.T) { + tests := []struct { + name string + msr string + want []int + expectErr bool + }{ + { + name: "Valid MSR with 8 bucket sizes", + msr: "0x0102030405060708", + want: []int{8, 7, 6, 5, 4, 3, 2, 1}, + expectErr: false, + }, + { + name: "Valid MSR with reversed order", + msr: "0x0807060504030201", + want: []int{1, 2, 3, 4, 5, 6, 7, 8}, + expectErr: false, + }, + { + name: "Invalid MSR string", + msr: "invalid_hex", + want: nil, + expectErr: true, + }, + { + name: "MSR with less than 8 bucket sizes", + msr: "0x01020304", + want: nil, + expectErr: true, + }, + { + name: "MSR with more than 8 bucket sizes", + msr: "0x010203040506070809", + want: nil, + expectErr: true, + }, + { + name: "Empty MSR string", + msr: "", + want: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getBucketSizesFromHex(tt.msr) + if (err != nil) != tt.expectErr { + t.Errorf("getBucketSizesFromMSR() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getBucketSizesFromMSR() = %v, want %v", got, tt.want) + } + }) + } +} +func TestExpandTurboFrequencies(t *testing.T) { + tests := []struct { + name string + buckets [][]string + isa string + want []string + expectErr bool + }{ + { + name: "Valid input with single bucket", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "3.5", "3.2"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.5", "3.5"}, + expectErr: false, + }, + { + name: "Valid input with multiple buckets", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-2", "3.5", "3.2"}, + {"3-4", "3.6", "3.3"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.6", "3.6"}, + expectErr: false, + }, + { + name: "ISA column not found", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "3.5", "3.2"}, + }, + isa: "AVX512", + want: nil, + expectErr: true, + }, + { + name: "Empty buckets", + buckets: [][]string{ + {}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Invalid bucket range", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-", "3.5", "3.2"}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Empty frequency value", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "", "3.2"}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Whitespace in bucket range", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {" 1-4 ", "3.5", "3.2"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.5", "3.5"}, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := expandTurboFrequencies(tt.buckets, tt.isa) + if (err != nil) != tt.expectErr { + t.Errorf("expandTurboFrequencies() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("expandTurboFrequencies() = %v, want %v", got, tt.want) + } + }) + } +} +func TestGetSectionsFromOutput(t *testing.T) { + tests := []struct { + name string + output string + want map[string]string + }{ + { + name: "Valid sections with content", + output: `########## Section A ########## +Content A1 +Content A2 +########## Section B ########## +Content B1 +Content B2 +########## Section C ########## +Content C1`, + want: map[string]string{ + "Section A": "Content A1\nContent A2\n", + "Section B": "Content B1\nContent B2\n", + "Section C": "Content C1\n", + }, + }, + { + name: "Valid sections with empty content", + output: `########## Section A ########## +########## Section B ########## +########## Section C ##########`, + want: map[string]string{ + "Section A": "", + "Section B": "", + "Section C": "", + }, + }, + { + name: "No sections", + output: "No section headers here", + want: map[string]string{}, + }, + { + name: "Empty output", + output: ``, + want: map[string]string{}, + }, + { + name: "Empty lines in output", + output: "\n\n\n", + want: map[string]string{}, + }, + { + name: "Section with trailing newlines", + output: `########## Section A ########## + +Content A1 + +########## Section B ########## +Content B1`, + want: map[string]string{ + "Section A": "\nContent A1\n\n", + "Section B": "Content B1\n", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getSectionsFromOutput(tt.output) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getSectionsFromOutput() = %v, want %v", got, tt.want) + } + }) + } +} +func TestSectionValueFromOutput(t *testing.T) { + tests := []struct { + name string + output string + sectionName string + want string + }{ + { + name: "Section A exists with content", + output: `########## Section A ########## +Content A1 +Content A2 +########## Section B ########## +Content B1 +Content B2`, + sectionName: "Section A", + want: "Content A1\nContent A2\n", + }, + { + name: "Section B exists with content", + output: `########## Section A ########## +Content A1 +Content A2 +########## Section B ########## +Content B1 +Content B2`, + sectionName: "Section B", + want: "Content B1\nContent B2\n", + }, + { + name: "Section exists with no content", + output: `########## Section A ########## +########## Section B ########## +Content B1`, + sectionName: "Section A", + want: "", + }, + { + name: "Section does not exist", + output: `########## Section A ########## +Content A1 +########## Section B ########## +Content B1`, + sectionName: "Section C", + want: "", + }, + { + name: "Empty output", + output: "", + sectionName: "Section A", + want: "", + }, + { + name: "Section with trailing newlines", + output: `########## Section A ########## + +Content A1 + +########## Section B ########## +Content B1`, + sectionName: "Section A", + want: "\nContent A1\n\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := sectionValueFromOutput(tt.output, tt.sectionName) + if got != tt.want { + t.Errorf("sectionValueFromOutput() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/target/local_target_test.go b/internal/target/local_target_test.go new file mode 100644 index 00000000..630b0bea --- /dev/null +++ b/internal/target/local_target_test.go @@ -0,0 +1,76 @@ +package target + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "os" + "slices" + "strings" + "testing" +) + +type MockLocalTarget struct { + LocalTarget +} + +func TestGetUserPath(t *testing.T) { + // Backup and defer restore of original PATH environment variable + originalPath := os.Getenv("PATH") + defer os.Setenv("PATH", originalPath) + + tests := []struct { + name string + envPath string + expectedPaths []string + }{ + { + name: "Valid paths in PATH", + envPath: "/usr/bin:/bin:/usr/local/bin", + expectedPaths: []string{"/usr/bin", "/bin", "/usr/local/bin"}, + }, + { + name: "Invalid paths in PATH", + envPath: "/invalid/path:/another/invalid:/usr/bin", + expectedPaths: []string{"/usr/bin"}, + }, + { + name: "Empty PATH", + envPath: "", + expectedPaths: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set the PATH environment variable for the test + os.Setenv("PATH", tt.envPath) + + // Create a mock LocalTarget + mockTarget := &MockLocalTarget{} + + // Call GetUserPath + result, err := mockTarget.GetUserPath() + if err != nil { + t.Fatalf("GetUserPath returned an error: %v", err) + } + + // Split the result into paths + resultPaths := strings.Split(result, ":") // returns a slice containing a single empty string, if result is empty + if len(resultPaths) == 1 && resultPaths[0] == "" { + resultPaths = []string{} + } + + // Compare the result with the expected paths + if len(resultPaths) != len(tt.expectedPaths) { + t.Errorf("Expected %d paths, got %d", len(tt.expectedPaths), len(resultPaths)) + } + + for _, expectedPath := range tt.expectedPaths { + if !slices.Contains(resultPaths, expectedPath) { + t.Errorf("Expected path %s not found in result", expectedPath) + } + } + }) + } +} diff --git a/internal/util/util.go b/internal/util/util.go index 857c8d3f..c2176719 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -428,3 +428,96 @@ func SignalChildren(sig os.Signal) { } } } + +// IsValidHex checks if a string is a valid hex string +// Valid hex strings are non-empty, optionally prefixed with "0x" or "0X", +// and contain only valid hex characters (0-9, a-f, A-F). +func IsValidHex(hexStr string) bool { + // Check if the string starts with "0x" or "0X" + if strings.HasPrefix(hexStr, "0x") || strings.HasPrefix(hexStr, "0X") { + hexStr = hexStr[2:] + } + // Check if the string can be parsed as a hex number + _, err := strconv.ParseUint(hexStr, 16, 64) + return err == nil +} + +// HexToIntList converts hex string to a list of integers 16 bits (2 hex chars) +// at a time. The hex string can, optionally, be prefixed with "0x" or "0X". +// For example, "0x1234", "0X1234", and "1234" will be converted to [0x12, 0x34]. +// If the hex string is not valid, an error is returned. +func HexToIntList(hexStr string) ([]int, error) { + if !IsValidHex(hexStr) { + return nil, fmt.Errorf("invalid hex string: %s", hexStr) + } + // Remove the "0x" or "0X" prefix if present + if strings.HasPrefix(hexStr, "0x") || strings.HasPrefix(hexStr, "0X") { + hexStr = hexStr[2:] + } + // Pad the hex string with a leading zero if necessary + if len(hexStr)%2 != 0 { + hexStr = "0" + hexStr + } + // Convert the hex string to a list of integers + intList := make([]int, len(hexStr)/2) + for i := 0; i < len(hexStr); i += 2 { + // Convert each pair of hex characters to an integer + val, err := strconv.ParseInt(hexStr[i:i+2], 16, 16) + if err != nil { + return nil, fmt.Errorf("failed to convert hex to int: %s", err) + } + intList[i/2] = int(val) + } + return intList, nil +} + +// IntRangeToIntList expands a string representing a range of integers into a slice of integers. +// The function returns a slice of integers representing the expanded range. +// For example, "1-3" will be expanded to [1, 2, 3]. And, "5" will be expanded to [5]. +// If the input string is not in a valid format, it returns an error. +func IntRangeToIntList(input string) ([]int, error) { + // check input format matches "start-end", or "start" + re := regexp.MustCompile(`^(\d+)(?:-(\d+))?$`) + matches := re.FindStringSubmatch(input) + if len(matches) == 0 { + err := fmt.Errorf("invalid input format: %s", input) + return nil, err + } + start, err := strconv.Atoi(matches[1]) + if err != nil { + return nil, fmt.Errorf("invalid start value: %s", matches[1]) + } + // if end value is empty, return a slice with the start value + if matches[2] == "" { + return []int{start}, nil + } + // if end value is provided, parse it + end, err := strconv.Atoi(matches[2]) + if err != nil { + return nil, fmt.Errorf("invalid end value: %s", matches[2]) + } + if start > end { + return nil, fmt.Errorf("start value is greater than end value: %d > %d", start, end) + } + // create a slice of integers from start to end + result := make([]int, end-start+1) + for i := start; i <= end; i++ { + result[i-start] = i + } + return result, nil +} + +// SelectiveIntRangeToIntList expands a string representing a selective range of integers into a slice of integers. +// For example "1-3,7,9,11-13" will be expanded to [1, 2, 3, 7, 9, 11, 12, 13]. +// An error is returned if the input string is not in a valid format. +func SelectiveIntRangeToIntList(input string) ([]int, error) { + var result []int + for r := range strings.SplitSeq(input, ",") { + ints, err := IntRangeToIntList(r) + if err != nil { + return nil, err + } + result = append(result, ints...) + } + return result, nil +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go index 72c3c6d3..17adc27e 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -3,7 +3,10 @@ package util // Copyright (C) 2021-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause -import "testing" +import ( + "slices" + "testing" +) func TestCompareVersions(t *testing.T) { tests := []struct { @@ -32,3 +35,117 @@ func TestCompareVersions(t *testing.T) { } } } +func TestIsValidHex(t *testing.T) { + tests := []struct { + hexStr string + expected bool + }{ + {"0x1a2b3c", true}, // Valid hex with "0x" prefix + {"0X1A2B3C", true}, // Valid hex with "0X" prefix + {"1a2b3c", true}, // Valid hex without prefix + {"1A2B3C", true}, // Valid uppercase hex without prefix + {"0x", false}, // Invalid hex, only prefix + {"", false}, // Empty string + {"0xGHIJKL", false}, // Invalid hex with non-hex characters + {"GHIJKL", false}, // Invalid hex without prefix + {"12345", true}, // Valid numeric hex + {"0x12345", true}, // Valid numeric hex with + {" 12345 ", false}, // Invalid hex with spaces + } + + for _, test := range tests { + result := IsValidHex(test.hexStr) + if result != test.expected { + t.Errorf("expected %v, got %v for hex string %s", test.expected, result, test.hexStr) + } + } +} +func TestHexToIntList(t *testing.T) { + tests := []struct { + hexStr string + expected []int + err bool + }{ + {"0x1a2b3c", []int{26, 43, 60}, false}, // Valid hex with "0x" prefix + {"1a2b3c", []int{26, 43, 60}, false}, // Valid hex without prefix + {"0X1A2B3C", []int{26, 43, 60}, false}, // Valid hex with "0X" prefix + {"1A2B3C", []int{26, 43, 60}, false}, // Valid uppercase hex without prefix + {"0x123", []int{1, 35}, false}, // Valid hex with odd length + {"123", []int{1, 35}, false}, // Valid hex without prefix and odd length + {"0x", nil, true}, // Invalid hex, only prefix + {"", nil, true}, // Empty string + {"0xGHIJKL", nil, true}, // Invalid hex with non-hex characters + {"GHIJKL", nil, true}, // Invalid hex without prefix + {"12345", []int{1, 35, 69}, false}, // Valid numeric hex + {"0x12345", []int{1, 35, 69}, false}, // Valid numeric hex with prefix + {" 12345 ", nil, true}, // Invalid hex with spaces + } + + for _, test := range tests { + result, err := HexToIntList(test.hexStr) + if (err != nil) != test.err { + t.Errorf("expected error: %v, got: %v for hex string %s", test.err, err != nil, test.hexStr) + } + if !test.err && !slices.Equal(result, test.expected) { + t.Errorf("expected %v, got %v for hex string %s", test.expected, result, test.hexStr) + } + } +} +func TestIntRangeToIntList(t *testing.T) { + tests := []struct { + input string + expected []int + err bool + }{ + {"1-5", []int{1, 2, 3, 4, 5}, false}, // Valid range + {"10-15", []int{10, 11, 12, 13, 14, 15}, false}, // Valid range + {"5-5", []int{5}, false}, // Single value range + {"", []int{}, true}, // Empty input + {"5-3", nil, true}, // Invalid range (start > end) + {"abc-def", nil, true}, // Invalid input format + {"1-", nil, true}, // Missing end value + {"-5", nil, true}, // Missing start value + {"1-5-10", nil, true}, // Invalid format with extra dash + {"1-abc", nil, true}, // Invalid end value + {"abc-5", nil, true}, // Invalid start value + {"3", []int{3}, false}, // Single value without range + } + + for _, test := range tests { + result, err := IntRangeToIntList(test.input) + if (err != nil) != test.err { + t.Errorf("expected error: %v, got: %v for input %s, err: %v", test.err, err != nil, test.input, err) + } + if !test.err && !slices.Equal(result, test.expected) { + t.Errorf("expected %v, got %v for input %s", test.expected, result, test.input) + } + } +} +func TestSelectiveIntRangeToIntList(t *testing.T) { + tests := []struct { + input string + expected []int + err bool + }{ + {"1-3,5,7-9", []int{1, 2, 3, 5, 7, 8, 9}, false}, // Valid mixed ranges and single values + {"10-12,15,20-22", []int{10, 11, 12, 15, 20, 21, 22}, false}, // Valid mixed ranges + {"5", []int{5}, false}, // Single value + {"1-3,5-5,7", []int{1, 2, 3, 5, 7}, false}, // Mixed ranges with single value range + {"", nil, true}, // Empty input + {"1-3,abc,7-9", nil, true}, // Invalid input with non-numeric value + {"1-3,5-2,7-9", nil, true}, // Invalid range (start > end) + {"1-3,,7-9", nil, true}, // Invalid format with empty segment + {"1-3,7-9-", nil, true}, // Invalid format with trailing dash + {"1-3,7-abc", nil, true}, // Invalid range with non-numeric end + } + + for _, test := range tests { + result, err := SelectiveIntRangeToIntList(test.input) + if (err != nil) != test.err { + t.Errorf("expected error: %v, got: %v for input %s, err: %v", test.err, err != nil, test.input, err) + } + if !test.err && !slices.Equal(result, test.expected) { + t.Errorf("expected %v, got %v for input %s", test.expected, result, test.input) + } + } +} diff --git a/scripts/copyright_go.sh b/scripts/copyright_go.sh index c0484ea6..12539d35 100755 --- a/scripts/copyright_go.sh +++ b/scripts/copyright_go.sh @@ -15,7 +15,7 @@ find . -name "*.go" | while read -r file; do while IFS= read -r line; do echo "$line" if [[ $line == package* ]]; then - echo -e "$COPYRIGHT_HEADER" + echo -e "\n$COPYRIGHT_HEADER" fi done } < "$file" > temp_file && mv temp_file "$file"