diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 050676d5..86984da1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,8 @@ PerfSpect is a performance analysis tool for Linux systems written in Go. It provides several commands: - `metrics`: Collects CPU performance metrics using hardware performance counters -- `report`: Generates system configuration and health (performance) from collected data +- `report`: Generates system configuration and health reports from collected data +- `benchmark`: Runs performance micro-benchmarks to evaluate system health - `telemetry`: Gathers system telemetry data - `flamegraph`: Creates CPU flamegraphs - `lock`: Analyzes lock contention diff --git a/README.md b/README.md index 365fb781..06284bb1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Usage: | ------- | ----------- | | [`metrics`](#metrics-command) | CPU core and uncore metrics | | [`report`](#report-command) | System configuration and health | +| [`benchmark`](#benchmark-command) | Performance benchmarks | | [`telemetry`](#telemetry-command) | System telemetry | | [`flamegraph`](#flamegraph-command) | Software call-stacks as flamegraphs | | [`lock`](#lock-command) | Software hot spot, cache-to-cache and lock contention | @@ -87,22 +88,31 @@ Vendor: Intel Corporation Version: EGSDCRB1.SYS.1752.P05.2401050248 Release Date: 01/05/2024 -##### Report Benchmarks -To assist in evaluating the health of target systems, the `report` command can run a series of micro-benchmarks by applying the `--benchmark` flag, for example, `perfspect report --benchmark all` The benchmark results will be reported along with the target's configuration details. + +#### Benchmark Command +The `benchmark` command runs performance micro-benchmarks to evaluate system health and performance characteristics. All benchmarks are run by default unless specific benchmarks are selected. A brief system summary is included in the output by default. > [!IMPORTANT] > Benchmarks should be run on idle systems to ensure accurate measurements and to avoid interfering with active workloads. -| benchmark | Description | +**Examples:** +
+$ ./perfspect benchmark                  # Run all benchmarks with system summary
+$ ./perfspect benchmark --speed --power  # Run specific benchmarks
+$ ./perfspect benchmark --no-summary     # Exclude system summary from output
+
+ +See `perfspect benchmark -h` for all options. + +| Benchmark | Description | | --------- | ----------- | -| all | runs all benchmarks | | speed | runs each [stress-ng](https://github.com/ColinIanKing/stress-ng) cpu-method for 1s each, reports the geo-metric mean of all results. | | power | runs stress-ng to load all cpus to 100% for 60s. Uses [turbostat](https://github.com/torvalds/linux/tree/master/tools/power/x86/turbostat) to measure power. | | temperature | runs the same micro benchmark as 'power', but extracts maximum temperature from turbostat output. | | frequency | runs [avx-turbo](https://github.com/travisdowns/avx-turbo) to measure scalar and AVX frequencies across processor's cores. **Note:** Runtime increases with core count. | | memory | runs [Intel(r) Memory Latency Checker](https://www.intel.com/content/www/us/en/download/736633/intel-memory-latency-checker-intel-mlc.html) (MLC) to measure memory bandwidth and latency across a load range. **Note: MLC is not included with PerfSpect.** It can be downloaded from [here](https://www.intel.com/content/www/us/en/download/736633/intel-memory-latency-checker-intel-mlc.html). Once downloaded, extract the Linux executable and place it in the perfspect/tools/x86_64 directory. | | numa | runs Intel(r) Memory Latency Checker(MLC) to measure bandwidth between NUMA nodes. See Note above about downloading MLC. | -| storage | runs [fio](https://github.com/axboe/fio) for 2 minutes in read/write mode with a single worker to measure single-thread read and write bandwidth. Use the --storage-dir flag to override the default location. Minimum 5GB disk space required to run test. | +| storage | runs [fio](https://github.com/axboe/fio) for 2 minutes across multiple I/O patterns to measure storage latency, IOPs, and bandwidth. Use --storage-dir to override the default location (/tmp). Minimum 32GB disk space required. | #### Telemetry Command The `telemetry` command reports CPU utilization, instruction mix, disk stats, network stats, and more on the specified target(s). All telemetry types are collected by default. To choose telemetry types, see the additional command line options (`perfspect telemetry -h`). diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go new file mode 100644 index 00000000..b299ec2e --- /dev/null +++ b/cmd/benchmark/benchmark.go @@ -0,0 +1,439 @@ +// Package benchmark is a subcommand of the root command. It runs performance benchmarks on target(s). +package benchmark + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "fmt" + "log/slog" + "os" + "slices" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/xuri/excelize/v2" + + "perfspect/internal/common" + "perfspect/internal/cpus" + "perfspect/internal/report" + "perfspect/internal/script" + "perfspect/internal/table" + "perfspect/internal/util" +) + +const cmdName = "benchmark" + +var examples = []string{ + fmt.Sprintf(" Run all benchmarks: $ %s %s", common.AppName, cmdName), + fmt.Sprintf(" Run specific benchmarks: $ %s %s --speed --power", common.AppName, cmdName), + fmt.Sprintf(" Benchmark remote target: $ %s %s --target 192.168.1.1 --user fred --key fred_key", common.AppName, cmdName), + fmt.Sprintf(" Benchmark multiple targets:$ %s %s --targets targets.yaml", common.AppName, cmdName), +} + +var Cmd = &cobra.Command{ + Use: cmdName, + Aliases: []string{"bench"}, + Short: "Run performance benchmarks on target(s)", + Example: strings.Join(examples, "\n"), + RunE: runCmd, + PreRunE: validateFlags, + GroupID: "primary", + Args: cobra.NoArgs, + SilenceErrors: true, +} + +// flag vars +var ( + flagAll bool + + flagSpeed bool + flagPower bool + flagTemperature bool + flagFrequency bool + flagMemory bool + flagNuma bool + flagStorage bool + + flagNoSystemSummary bool + + flagStorageDir string +) + +// flag names +const ( + flagAllName = "all" + + flagSpeedName = "speed" + flagPowerName = "power" + flagTemperatureName = "temperature" + flagFrequencyName = "frequency" + flagMemoryName = "memory" + flagNumaName = "numa" + flagStorageName = "storage" + + flagNoSystemSummaryName = "no-summary" + + flagStorageDirName = "storage-dir" +) + +var benchmarkSummaryTableName = "Benchmark Summary" + +var categories = []common.Category{ + {FlagName: flagSpeedName, FlagVar: &flagSpeed, DefaultValue: false, Help: "CPU speed benchmark", Tables: []table.TableDefinition{tableDefinitions[SpeedBenchmarkTableName]}}, + {FlagName: flagPowerName, FlagVar: &flagPower, DefaultValue: false, Help: "power consumption benchmark", Tables: []table.TableDefinition{tableDefinitions[PowerBenchmarkTableName]}}, + {FlagName: flagTemperatureName, FlagVar: &flagTemperature, DefaultValue: false, Help: "temperature benchmark", Tables: []table.TableDefinition{tableDefinitions[TemperatureBenchmarkTableName]}}, + {FlagName: flagFrequencyName, FlagVar: &flagFrequency, DefaultValue: false, Help: "turbo frequency benchmark", Tables: []table.TableDefinition{tableDefinitions[FrequencyBenchmarkTableName]}}, + {FlagName: flagMemoryName, FlagVar: &flagMemory, DefaultValue: false, Help: "memory latency and bandwidth benchmark", Tables: []table.TableDefinition{tableDefinitions[MemoryBenchmarkTableName]}}, + {FlagName: flagNumaName, FlagVar: &flagNuma, DefaultValue: false, Help: "NUMA bandwidth matrix benchmark", Tables: []table.TableDefinition{tableDefinitions[NUMABenchmarkTableName]}}, + {FlagName: flagStorageName, FlagVar: &flagStorage, DefaultValue: false, Help: "storage performance benchmark", Tables: []table.TableDefinition{tableDefinitions[StorageBenchmarkTableName]}}, +} + +func init() { + // set up benchmark flags + for _, benchmark := range categories { + Cmd.Flags().BoolVar(benchmark.FlagVar, benchmark.FlagName, benchmark.DefaultValue, benchmark.Help) + } + // set up other flags + Cmd.Flags().StringVar(&common.FlagInput, common.FlagInputName, "", "") + Cmd.Flags().BoolVar(&flagAll, flagAllName, true, "") + Cmd.Flags().StringSliceVar(&common.FlagFormat, common.FlagFormatName, []string{report.FormatAll}, "") + Cmd.Flags().BoolVar(&flagNoSystemSummary, flagNoSystemSummaryName, false, "") + Cmd.Flags().StringVar(&flagStorageDir, flagStorageDirName, "/tmp", "") + + 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 { + flagDefault := "" + if cmd.Flags().Lookup(flag.Name).DefValue != "" { + flagDefault = fmt.Sprintf(" (default: %s)", cmd.Flags().Lookup(flag.Name).DefValue) + } + cmd.Printf(" --%-20s %s%s\n", flag.Name, flag.Help, flagDefault) + } + } + 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 { + var groups []common.FlagGroup + flags := []common.Flag{ + { + Name: flagAllName, + Help: "run all benchmarks", + }, + } + for _, benchmark := range categories { + flags = append(flags, common.Flag{ + Name: benchmark.FlagName, + Help: benchmark.Help, + }) + } + groups = append(groups, common.FlagGroup{ + GroupName: "Benchmark Options", + Flags: flags, + }) + flags = []common.Flag{ + { + Name: flagNoSystemSummaryName, + Help: "do not include system summary in output", + }, + { + Name: flagStorageDirName, + Help: "existing directory where storage performance benchmark data will be temporarily stored", + }, + { + Name: common.FlagFormatName, + Help: fmt.Sprintf("choose output format(s) from: %s", strings.Join(append([]string{report.FormatAll}, report.FormatOptions...), ", ")), + }, + } + groups = append(groups, common.FlagGroup{ + GroupName: "Other Options", + Flags: flags, + }) + groups = append(groups, common.GetTargetFlagGroup()) + flags = []common.Flag{ + { + Name: common.FlagInputName, + Help: "\".raw\" file, or directory containing \".raw\" files. Will skip data collection and use raw data for reports.", + }, + } + groups = append(groups, common.FlagGroup{ + GroupName: "Advanced Options", + Flags: flags, + }) + return groups +} + +func validateFlags(cmd *cobra.Command, args []string) error { + // clear flagAll if any benchmarks are selected + if flagAll { + for _, benchmark := range categories { + if benchmark.FlagVar != nil && *benchmark.FlagVar { + flagAll = false + break + } + } + } + // validate format options + for _, format := range common.FlagFormat { + formatOptions := append([]string{report.FormatAll}, report.FormatOptions...) + if !slices.Contains(formatOptions, format) { + return common.FlagValidationError(cmd, fmt.Sprintf("format options are: %s", strings.Join(formatOptions, ", "))) + } + } + // validate storage dir + if flagStorageDir != "" { + if !util.IsValidDirectoryName(flagStorageDir) { + return common.FlagValidationError(cmd, fmt.Sprintf("invalid storage directory name: %s", flagStorageDir)) + } + // if no target is specified, i.e., we have a local target only, check if the directory exists + if !cmd.Flags().Lookup("targets").Changed && !cmd.Flags().Lookup("target").Changed { + if _, err := os.Stat(flagStorageDir); os.IsNotExist(err) { + return common.FlagValidationError(cmd, fmt.Sprintf("storage dir does not exist: %s", flagStorageDir)) + } + } + } + // common target flags + if err := common.ValidateTargetFlags(cmd); err != nil { + return common.FlagValidationError(cmd, err.Error()) + } + return nil +} + +func runCmd(cmd *cobra.Command, args []string) error { + var tables []table.TableDefinition + // add system summary table if not disabled + if !flagNoSystemSummary { + tables = append(tables, common.TableDefinitions[common.BriefSysSummaryTableName]) + } + // add benchmark tables + selectedBenchmarkCount := 0 + for _, benchmark := range categories { + if *benchmark.FlagVar || flagAll { + tables = append(tables, benchmark.Tables...) + selectedBenchmarkCount++ + } + } + // include benchmark summary table if all benchmarks are selected + var summaryFunc common.SummaryFunc + if selectedBenchmarkCount == len(categories) { + summaryFunc = benchmarkSummaryFromTableValues + } + + reportingCommand := common.ReportingCommand{ + Cmd: cmd, + ScriptParams: map[string]string{"StorageDir": flagStorageDir}, + Tables: tables, + SummaryFunc: summaryFunc, + SummaryTableName: benchmarkSummaryTableName, + SummaryBeforeTableName: SpeedBenchmarkTableName, + InsightsFunc: nil, + } + + report.RegisterHTMLRenderer(FrequencyBenchmarkTableName, frequencyBenchmarkTableHtmlRenderer) + report.RegisterHTMLRenderer(MemoryBenchmarkTableName, memoryBenchmarkTableHtmlRenderer) + + report.RegisterHTMLMultiTargetRenderer(MemoryBenchmarkTableName, memoryBenchmarkTableMultiTargetHtmlRenderer) + + return reportingCommand.Run() +} + +func benchmarkSummaryFromTableValues(allTableValues []table.TableValues, outputs map[string]script.ScriptOutput) table.TableValues { + maxFreq := getValueFromTableValues(getTableValues(allTableValues, FrequencyBenchmarkTableName), "SSE", 0) + if maxFreq != "" { + maxFreq = maxFreq + " GHz" + } + allCoreMaxFreq := getValueFromTableValues(getTableValues(allTableValues, FrequencyBenchmarkTableName), "SSE", -1) + if allCoreMaxFreq != "" { + allCoreMaxFreq = allCoreMaxFreq + " GHz" + } + // get the maximum memory bandwidth from the memory latency table + memLatTableValues := getTableValues(allTableValues, MemoryBenchmarkTableName) + var bandwidthValues []string + if len(memLatTableValues.Fields) > 1 { + bandwidthValues = memLatTableValues.Fields[1].Values + } + maxBandwidth := 0.0 + for _, bandwidthValue := range bandwidthValues { + bandwidth, err := strconv.ParseFloat(bandwidthValue, 64) + if err != nil { + slog.Error("unexpected value in memory bandwidth", slog.String("error", err.Error()), slog.Float64("value", bandwidth)) + break + } + if bandwidth > maxBandwidth { + maxBandwidth = bandwidth + } + } + maxMemBW := "" + if maxBandwidth != 0 { + maxMemBW = fmt.Sprintf("%.1f GB/s", maxBandwidth) + } + // get the minimum memory latency + minLatency := getValueFromTableValues(getTableValues(allTableValues, MemoryBenchmarkTableName), "Latency (ns)", 0) + if minLatency != "" { + minLatency = minLatency + " ns" + } + + report.RegisterHTMLRenderer(benchmarkSummaryTableName, summaryHTMLTableRenderer) + report.RegisterTextRenderer(benchmarkSummaryTableName, summaryTextTableRenderer) + report.RegisterXlsxRenderer(benchmarkSummaryTableName, summaryXlsxTableRenderer) + + return table.TableValues{ + TableDefinition: table.TableDefinition{ + Name: benchmarkSummaryTableName, + HasRows: false, + MenuLabel: benchmarkSummaryTableName, + }, + Fields: []table.Field{ + {Name: "CPU Speed", Values: []string{getValueFromTableValues(getTableValues(allTableValues, SpeedBenchmarkTableName), "Ops/s", 0) + " Ops/s"}}, + {Name: "Single-core Maximum frequency", Values: []string{maxFreq}}, + {Name: "All-core Maximum frequency", Values: []string{allCoreMaxFreq}}, + {Name: "Maximum Power", Values: []string{getValueFromTableValues(getTableValues(allTableValues, PowerBenchmarkTableName), "Maximum Power", 0)}}, + {Name: "Maximum Temperature", Values: []string{getValueFromTableValues(getTableValues(allTableValues, TemperatureBenchmarkTableName), "Maximum Temperature", 0)}}, + {Name: "Minimum Power", Values: []string{getValueFromTableValues(getTableValues(allTableValues, PowerBenchmarkTableName), "Minimum Power", 0)}}, + {Name: "Memory Peak Bandwidth", Values: []string{maxMemBW}}, + {Name: "Memory Minimum Latency", Values: []string{minLatency}}, + {Name: "Microarchitecture", Values: []string{common.UarchFromOutput(outputs)}}, + {Name: "Sockets", Values: []string{common.ValFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Socket\(s\):\s*(.+)$`)}}, + }, + } +} + +// getTableValues returns the table values for a table with a given name +func getTableValues(allTableValues []table.TableValues, tableName string) table.TableValues { + for _, tv := range allTableValues { + if tv.Name == tableName { + return tv + } + } + return table.TableValues{} +} + +// getValueFromTableValues returns the value of a field in a table +// if row is -1, it returns the last value +func getValueFromTableValues(tv table.TableValues, fieldName string, row int) string { + for _, fv := range tv.Fields { + if fv.Name == fieldName { + if row == -1 { // return the last value + if len(fv.Values) == 0 { + return "" + } + return fv.Values[len(fv.Values)-1] + } + if len(fv.Values) > row { + return fv.Values[row] + } + break + } + } + return "" +} + +// ReferenceData is a struct that holds reference data for a microarchitecture +type ReferenceData struct { + Description string + CPUSpeed float64 + SingleCoreFreq float64 + AllCoreFreq float64 + MaxPower float64 + MaxTemp float64 + MinPower float64 + MemPeakBandwidth float64 + MemMinLatency float64 +} + +// ReferenceDataKey is a struct that holds the key for reference data +type ReferenceDataKey struct { + Microarchitecture string + Sockets string +} + +// referenceData is a map of reference data for microarchitectures +var referenceData = map[ReferenceDataKey]ReferenceData{ + {cpus.UarchBDX, "2"}: {Description: "Reference (Intel 2S Xeon E5-2699 v4)", CPUSpeed: 403415, SingleCoreFreq: 3509, AllCoreFreq: 2980, MaxPower: 289.9, MaxTemp: 0, MinPower: 0, MemPeakBandwidth: 138.1, MemMinLatency: 78}, + {cpus.UarchSKX, "2"}: {Description: "Reference (Intel 2S Xeon 8180)", CPUSpeed: 585157, SingleCoreFreq: 3758, AllCoreFreq: 3107, MaxPower: 429.07, MaxTemp: 0, MinPower: 0, MemPeakBandwidth: 225.1, MemMinLatency: 71}, + {cpus.UarchCLX, "2"}: {Description: "Reference (Intel 2S Xeon 8280)", CPUSpeed: 548644, SingleCoreFreq: 3928, AllCoreFreq: 3926, MaxPower: 415.93, MaxTemp: 0, MinPower: 0, MemPeakBandwidth: 223.9, MemMinLatency: 72}, + {cpus.UarchICX, "2"}: {Description: "Reference (Intel 2S Xeon 8380)", CPUSpeed: 933644, SingleCoreFreq: 3334, AllCoreFreq: 2950, MaxPower: 552, MaxTemp: 0, MinPower: 175.38, MemPeakBandwidth: 350.7, MemMinLatency: 70}, + {cpus.UarchSPR_XCC, "2"}: {Description: "Reference (Intel 2S Xeon 8480+)", CPUSpeed: 1678712, SingleCoreFreq: 3776, AllCoreFreq: 2996, MaxPower: 698.35, MaxTemp: 0, MinPower: 249.21, MemPeakBandwidth: 524.6, MemMinLatency: 111.8}, + {cpus.UarchSPR_XCC, "1"}: {Description: "Reference (Intel 1S Xeon 8480+)", CPUSpeed: 845743, SingleCoreFreq: 3783, AllCoreFreq: 2999, MaxPower: 334.68, MaxTemp: 0, MinPower: 163.79, MemPeakBandwidth: 264.0, MemMinLatency: 112.2}, + {cpus.UarchEMR_XCC, "2"}: {Description: "Reference (Intel 2S Xeon 8592V)", CPUSpeed: 1789534, SingleCoreFreq: 3862, AllCoreFreq: 2898, MaxPower: 664.4, MaxTemp: 0, MinPower: 166.36, MemPeakBandwidth: 553.5, MemMinLatency: 92.0}, + {cpus.UarchSRF_SP, "2"}: {Description: "Reference (Intel 2S Xeon 6780E)", CPUSpeed: 3022446, SingleCoreFreq: 3001, AllCoreFreq: 3001, MaxPower: 583.97, MaxTemp: 0, MinPower: 123.34, MemPeakBandwidth: 534.3, MemMinLatency: 129.25}, + {cpus.UarchGNR_X2, "2"}: {Description: "Reference (Intel 2S Xeon 6787P)", CPUSpeed: 3178562, SingleCoreFreq: 3797, AllCoreFreq: 3199, MaxPower: 679, MaxTemp: 0, MinPower: 248.49, MemPeakBandwidth: 749.6, MemMinLatency: 117.51}, +} + +// getFieldIndex returns the index of a field in a list of fields +func getFieldIndex(fields []table.Field, fieldName string) (int, error) { + for i, field := range fields { + if field.Name == fieldName { + return i, nil + } + } + return -1, fmt.Errorf("field not found: %s", fieldName) +} + +// summaryHTMLTableRenderer is a custom HTML table renderer for the summary table +// it removes the Microarchitecture and Sockets fields and adds a reference table +func summaryHTMLTableRenderer(tv table.TableValues, targetName string) string { + uarchFieldIdx, err := getFieldIndex(tv.Fields, "Microarchitecture") + if err != nil { + panic(err) + } + socketsFieldIdx, err := getFieldIndex(tv.Fields, "Sockets") + if err != nil { + panic(err) + } + // if we have reference data that matches the microarchitecture and sockets, use it + if len(tv.Fields[uarchFieldIdx].Values) > 0 && len(tv.Fields[socketsFieldIdx].Values) > 0 { + if refData, ok := referenceData[ReferenceDataKey{tv.Fields[uarchFieldIdx].Values[0], tv.Fields[socketsFieldIdx].Values[0]}]; ok { + // remove microarchitecture and sockets fields + fields := tv.Fields[:len(tv.Fields)-2] + refTableValues := table.TableValues{ + Fields: []table.Field{ + {Name: "CPU Speed", Values: []string{fmt.Sprintf("%.0f Ops/s", refData.CPUSpeed)}}, + {Name: "Single-core Maximum frequency", Values: []string{fmt.Sprintf("%.0f MHz", refData.SingleCoreFreq)}}, + {Name: "All-core Maximum frequency", Values: []string{fmt.Sprintf("%.0f MHz", refData.AllCoreFreq)}}, + {Name: "Maximum Power", Values: []string{fmt.Sprintf("%.0f W", refData.MaxPower)}}, + {Name: "Maximum Temperature", Values: []string{fmt.Sprintf("%.0f C", refData.MaxTemp)}}, + {Name: "Minimum Power", Values: []string{fmt.Sprintf("%.0f W", refData.MinPower)}}, + {Name: "Memory Peak Bandwidth", Values: []string{fmt.Sprintf("%.0f GB/s", refData.MemPeakBandwidth)}}, + {Name: "Memory Minimum Latency", Values: []string{fmt.Sprintf("%.0f ns", refData.MemMinLatency)}}, + }, + } + return report.RenderMultiTargetTableValuesAsHTML([]table.TableValues{{TableDefinition: tv.TableDefinition, Fields: fields}, refTableValues}, []string{targetName, refData.Description}) + } + } + // remove microarchitecture and sockets fields + fields := tv.Fields[:len(tv.Fields)-2] + return report.DefaultHTMLTableRendererFunc(table.TableValues{TableDefinition: tv.TableDefinition, Fields: fields}) +} + +func summaryXlsxTableRenderer(tv table.TableValues, f *excelize.File, targetName string, row *int) { + // remove microarchitecture and sockets fields + fields := tv.Fields[:len(tv.Fields)-2] + report.DefaultXlsxTableRendererFunc(table.TableValues{TableDefinition: tv.TableDefinition, Fields: fields}, f, report.XlsxPrimarySheetName, row) +} + +func summaryTextTableRenderer(tv table.TableValues) string { + // remove microarchitecture and sockets fields + fields := tv.Fields[:len(tv.Fields)-2] + return report.DefaultTextTableRendererFunc(table.TableValues{TableDefinition: tv.TableDefinition, Fields: fields}) +} diff --git a/cmd/benchmark/benchmark_renderers.go b/cmd/benchmark/benchmark_renderers.go new file mode 100644 index 00000000..f0c4bb82 --- /dev/null +++ b/cmd/benchmark/benchmark_renderers.go @@ -0,0 +1,111 @@ +package benchmark + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "fmt" + "log/slog" + "strconv" + + "perfspect/internal/report" + "perfspect/internal/table" + "perfspect/internal/util" +) + +func renderFrequencyTable(tableValues table.TableValues) (out string) { + var rows [][]string + headers := []string{""} + valuesStyles := [][]string{} + for i := range tableValues.Fields[0].Values { + headers = append(headers, fmt.Sprintf("%d", i+1)) + } + for _, field := range tableValues.Fields[1:] { + row := append([]string{report.CreateFieldNameWithDescription(field.Name, field.Description)}, field.Values...) + rows = append(rows, row) + valuesStyles = append(valuesStyles, []string{"font-weight:bold"}) + } + out = report.RenderHTMLTable(headers, rows, "pure-table pure-table-striped", valuesStyles) + return +} + +func coreTurboFrequencyTableHTMLRenderer(tableValues table.TableValues) string { + data := [][]report.ScatterPoint{} + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + points := []report.ScatterPoint{} + for i, val := range field.Values { + if val == "" { + break + } + freq, err := strconv.ParseFloat(val, 64) + if err != nil { + slog.Error("error parsing frequency", slog.String("error", err.Error())) + return "" + } + points = append(points, report.ScatterPoint{X: float64(i + 1), Y: freq}) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("turboFrequency%d", util.RandUint(10000)), + XaxisText: "Core Count", + YaxisText: "Frequency (GHz)", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "4", + SuggestedMin: "2", + SuggestedMax: "4", + } + out := report.RenderScatterChart(data, datasetNames, chartConfig) + out += "\n" + out += renderFrequencyTable(tableValues) + return out +} + +func frequencyBenchmarkTableHtmlRenderer(tableValues table.TableValues, targetName string) string { + return coreTurboFrequencyTableHTMLRenderer(tableValues) +} + +func memoryBenchmarkTableHtmlRenderer(tableValues table.TableValues, targetName string) string { + return memoryBenchmarkTableMultiTargetHtmlRenderer([]table.TableValues{tableValues}, []string{targetName}) +} + +func memoryBenchmarkTableMultiTargetHtmlRenderer(allTableValues []table.TableValues, targetNames []string) string { + data := [][]report.ScatterPoint{} + datasetNames := []string{} + for targetIdx, tableValues := range allTableValues { + points := []report.ScatterPoint{} + for valIdx := range tableValues.Fields[0].Values { + latency, err := strconv.ParseFloat(tableValues.Fields[0].Values[valIdx], 64) + if err != nil { + slog.Error("error parsing latency", slog.String("error", err.Error())) + return "" + } + bandwidth, err := strconv.ParseFloat(tableValues.Fields[1].Values[valIdx], 64) + if err != nil { + slog.Error("error parsing bandwidth", slog.String("error", err.Error())) + return "" + } + points = append(points, report.ScatterPoint{X: bandwidth, Y: latency}) + } + data = append(data, points) + datasetNames = append(datasetNames, targetNames[targetIdx]) + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("latencyBandwidth%d", util.RandUint(10000)), + XaxisText: "Bandwidth (GB/s)", + YaxisText: "Latency (ns)", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "4", + SuggestedMin: "0", + SuggestedMax: "0", + } + return report.RenderScatterChart(data, datasetNames, chartConfig) +} diff --git a/cmd/benchmark/benchmark_tables.go b/cmd/benchmark/benchmark_tables.go new file mode 100644 index 00000000..51639d6a --- /dev/null +++ b/cmd/benchmark/benchmark_tables.go @@ -0,0 +1,327 @@ +package benchmark + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "fmt" + "log/slog" + "strconv" + "strings" + + "perfspect/internal/common" + "perfspect/internal/cpus" + "perfspect/internal/script" + "perfspect/internal/table" +) + +// table names +const ( + // benchmark table names + SpeedBenchmarkTableName = "Speed Benchmark" + PowerBenchmarkTableName = "Power Benchmark" + TemperatureBenchmarkTableName = "Temperature Benchmark" + FrequencyBenchmarkTableName = "Frequency Benchmark" + MemoryBenchmarkTableName = "Memory Benchmark" + NUMABenchmarkTableName = "NUMA Benchmark" + StorageBenchmarkTableName = "Storage Benchmark" +) + +var tableDefinitions = map[string]table.TableDefinition{ + SpeedBenchmarkTableName: { + Name: SpeedBenchmarkTableName, + MenuLabel: SpeedBenchmarkTableName, + HasRows: false, + ScriptNames: []string{ + script.SpeedBenchmarkScriptName, + }, + FieldsFunc: speedBenchmarkTableValues}, + PowerBenchmarkTableName: { + Name: PowerBenchmarkTableName, + MenuLabel: PowerBenchmarkTableName, + Architectures: []string{cpus.X86Architecture}, + HasRows: false, + ScriptNames: []string{ + script.IdlePowerBenchmarkScriptName, + script.PowerBenchmarkScriptName, + }, + FieldsFunc: powerBenchmarkTableValues}, + TemperatureBenchmarkTableName: { + Name: TemperatureBenchmarkTableName, + MenuLabel: TemperatureBenchmarkTableName, + Architectures: []string{cpus.X86Architecture}, + HasRows: false, + ScriptNames: []string{ + script.PowerBenchmarkScriptName, + }, + FieldsFunc: temperatureBenchmarkTableValues}, + FrequencyBenchmarkTableName: { + Name: FrequencyBenchmarkTableName, + MenuLabel: FrequencyBenchmarkTableName, + Architectures: []string{cpus.X86Architecture}, + HasRows: true, + ScriptNames: []string{ + script.SpecCoreFrequenciesScriptName, + script.LscpuScriptName, + script.LspciBitsScriptName, + script.LspciDevicesScriptName, + script.FrequencyBenchmarkScriptName, + }, + FieldsFunc: frequencyBenchmarkTableValues}, + MemoryBenchmarkTableName: { + Name: MemoryBenchmarkTableName, + MenuLabel: MemoryBenchmarkTableName, + Architectures: []string{cpus.X86Architecture}, + HasRows: true, + ScriptNames: []string{ + script.MemoryBenchmarkScriptName, + }, + NoDataFound: "No memory benchmark data found. Please see the GitHub repository README for instructions on how to install Intel Memory Latency Checker (mlc).", + FieldsFunc: memoryBenchmarkTableValues}, + NUMABenchmarkTableName: { + Name: NUMABenchmarkTableName, + MenuLabel: NUMABenchmarkTableName, + Architectures: []string{cpus.X86Architecture}, + HasRows: true, + ScriptNames: []string{ + script.NumaBenchmarkScriptName, + }, + NoDataFound: "No NUMA benchmark data found. Please see the GitHub repository README for instructions on how to install Intel Memory Latency Checker (mlc).", + FieldsFunc: numaBenchmarkTableValues}, + StorageBenchmarkTableName: { + Name: StorageBenchmarkTableName, + MenuLabel: StorageBenchmarkTableName, + HasRows: true, + ScriptNames: []string{ + script.StorageBenchmarkScriptName, + }, + FieldsFunc: storageBenchmarkTableValues}, +} + +func speedBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { + return []table.Field{ + {Name: "Ops/s", Values: []string{cpuSpeedFromOutput(outputs)}}, + } +} + +func powerBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { + return []table.Field{ + {Name: "Maximum Power", Values: []string{common.MaxTotalPackagePowerFromOutput(outputs[script.PowerBenchmarkScriptName].Stdout)}}, + {Name: "Minimum Power", Values: []string{common.MinTotalPackagePowerFromOutput(outputs[script.IdlePowerBenchmarkScriptName].Stdout)}}, + } +} + +func temperatureBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { + return []table.Field{ + {Name: "Maximum Temperature", Values: []string{common.MaxPackageTemperatureFromOutput(outputs[script.PowerBenchmarkScriptName].Stdout)}}, + } +} + +func frequencyBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { + // get the sse, avx256, and avx512 frequencies from the avx-turbo output + instructionFreqs, err := avxTurboFrequenciesFromOutput(outputs[script.FrequencyBenchmarkScriptName].Stdout) + if err != nil { + slog.Warn("unable to get avx turbo frequencies", slog.String("error", err.Error())) + return []table.Field{} + } + // we're expecting scalar_iadd, avx256_fma, avx512_fma + scalarIaddFreqs := instructionFreqs["scalar_iadd"] + avx256FmaFreqs := instructionFreqs["avx256_fma"] + avx512FmaFreqs := instructionFreqs["avx512_fma"] + // stop if we don't have any scalar_iadd frequencies + if len(scalarIaddFreqs) == 0 { + slog.Warn("no scalar_iadd frequencies found") + return []table.Field{} + } + // get the spec core frequencies from the spec output + var specSSEFreqs []string + frequencyBuckets, err := common.GetSpecFrequencyBuckets(outputs) + if err == nil && len(frequencyBuckets) >= 2 { + // get the frequencies from the buckets + specSSEFreqs, err = common.ExpandTurboFrequencies(frequencyBuckets, "sse") + if err != nil { + slog.Error("unable to convert buckets to counts", slog.String("error", err.Error())) + return []table.Field{} + } + // trim the spec frequencies to the length of the scalar_iadd frequencies + // this can happen when the actual core count is less than the number of cores in the spec + if len(scalarIaddFreqs) < len(specSSEFreqs) { + specSSEFreqs = specSSEFreqs[:len(scalarIaddFreqs)] + } + // pad the spec frequencies with the last value if they are shorter than the scalar_iadd frequencies + // this can happen when the first die has fewer cores than other dies + if len(specSSEFreqs) < len(scalarIaddFreqs) { + diff := len(scalarIaddFreqs) - len(specSSEFreqs) + for range diff { + specSSEFreqs = append(specSSEFreqs, specSSEFreqs[len(specSSEFreqs)-1]) + } + } + } + // create the fields + fields := []table.Field{ + {Name: "cores"}, + } + coresIdx := 0 // always the first field + var specSSEFieldIdx int + var scalarIaddFieldIdx int + var avx2FieldIdx int + var avx512FieldIdx int + if len(specSSEFreqs) > 0 { + fields = append(fields, table.Field{Name: "SSE (expected)", Description: "The expected frequency, when running SSE instructions, for the given number of active cores."}) + specSSEFieldIdx = len(fields) - 1 + } + if len(scalarIaddFreqs) > 0 { + fields = append(fields, table.Field{Name: "SSE", Description: "The measured frequency, when running SSE instructions, for the given number of active cores."}) + scalarIaddFieldIdx = len(fields) - 1 + } + if len(avx256FmaFreqs) > 0 { + fields = append(fields, table.Field{Name: "AVX2", Description: "The measured frequency, when running AVX2 instructions, for the given number of active cores."}) + avx2FieldIdx = len(fields) - 1 + } + if len(avx512FmaFreqs) > 0 { + fields = append(fields, table.Field{Name: "AVX512", Description: "The measured frequency, when running AVX512 instructions, for the given number of active cores."}) + avx512FieldIdx = len(fields) - 1 + } + // add the data to the fields + for i := range scalarIaddFreqs { // scalarIaddFreqs is required + fields[coresIdx].Values = append(fields[coresIdx].Values, fmt.Sprintf("%d", i+1)) + if specSSEFieldIdx > 0 { + if len(specSSEFreqs) > i { + fields[specSSEFieldIdx].Values = append(fields[specSSEFieldIdx].Values, specSSEFreqs[i]) + } else { + fields[specSSEFieldIdx].Values = append(fields[specSSEFieldIdx].Values, "") + } + } + if scalarIaddFieldIdx > 0 { + if len(scalarIaddFreqs) > i { + fields[scalarIaddFieldIdx].Values = append(fields[scalarIaddFieldIdx].Values, fmt.Sprintf("%.1f", scalarIaddFreqs[i])) + } else { + fields[scalarIaddFieldIdx].Values = append(fields[scalarIaddFieldIdx].Values, "") + } + } + if avx2FieldIdx > 0 { + if len(avx256FmaFreqs) > i { + fields[avx2FieldIdx].Values = append(fields[avx2FieldIdx].Values, fmt.Sprintf("%.1f", avx256FmaFreqs[i])) + } else { + fields[avx2FieldIdx].Values = append(fields[avx2FieldIdx].Values, "") + } + } + if avx512FieldIdx > 0 { + if len(avx512FmaFreqs) > i { + fields[avx512FieldIdx].Values = append(fields[avx512FieldIdx].Values, fmt.Sprintf("%.1f", avx512FmaFreqs[i])) + } else { + fields[avx512FieldIdx].Values = append(fields[avx512FieldIdx].Values, "") + } + } + } + return fields +} + +func memoryBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { + fields := []table.Field{ + {Name: "Latency (ns)"}, + {Name: "Bandwidth (GB/s)"}, + } + /* MLC Output: + Inject Latency Bandwidth + Delay (ns) MB/sec + ========================== + 00000 261.65 225060.9 + 00002 261.63 225040.5 + 00008 261.54 225073.3 + ... + */ + latencyBandwidthPairs := common.ValsArrayFromRegexSubmatch(outputs[script.MemoryBenchmarkScriptName].Stdout, `\s*[0-9]*\s*([0-9]*\.[0-9]+)\s*([0-9]*\.[0-9]+)`) + for _, latencyBandwidth := range latencyBandwidthPairs { + latency := latencyBandwidth[0] + bandwidth, err := strconv.ParseFloat(latencyBandwidth[1], 32) + if err != nil { + slog.Error(fmt.Sprintf("Unable to convert bandwidth to float: %s", latencyBandwidth[1])) + continue + } + // insert into beginning of list + fields[0].Values = append([]string{latency}, fields[0].Values...) + fields[1].Values = append([]string{fmt.Sprintf("%.1f", bandwidth/1000)}, fields[1].Values...) + } + if len(fields[0].Values) == 0 { + return []table.Field{} + } + return fields +} + +func numaBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { + fields := []table.Field{ + {Name: "Node"}, + } + /* MLC Output: + Numa node + Numa node 0 1 + 0 175610.3 55579.7 + 1 55575.2 175656.7 + */ + nodeBandwidthsPairs := common.ValsArrayFromRegexSubmatch(outputs[script.NumaBenchmarkScriptName].Stdout, `^\s+(\d)\s+(\d.*)$`) + // add 1 field per numa node + for _, nodeBandwidthsPair := range nodeBandwidthsPairs { + fields = append(fields, table.Field{Name: nodeBandwidthsPair[0]}) + } + // add rows + for _, nodeBandwidthsPair := range nodeBandwidthsPairs { + fields[0].Values = append(fields[0].Values, nodeBandwidthsPair[0]) + bandwidths := strings.Split(strings.TrimSpace(nodeBandwidthsPair[1]), "\t") + if len(bandwidths) != len(nodeBandwidthsPairs) { + slog.Warn(fmt.Sprintf("Mismatched number of bandwidths for numa node %s, %s", nodeBandwidthsPair[0], nodeBandwidthsPair[1])) + return []table.Field{} + } + for i, bw := range bandwidths { + bw = strings.TrimSpace(bw) + val, err := strconv.ParseFloat(bw, 64) + if err != nil { + slog.Error(fmt.Sprintf("Unable to convert bandwidth to float: %s", bw)) + continue + } + fields[i+1].Values = append(fields[i+1].Values, fmt.Sprintf("%.1f", val/1000)) + } + } + if len(fields[0].Values) == 0 { + return []table.Field{} + } + return fields +} + +// formatOrEmpty formats a value and returns an empty string if the formatted value is "0". +func formatOrEmpty(format string, value any) string { + s := fmt.Sprintf(format, value) + if s == "0" { + return "" + } + return s +} + +func storageBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { + fioData, err := storagePerfFromOutput(outputs) + if err != nil { + slog.Warn("failed to get storage benchmark data", slog.String("error", err.Error())) + return []table.Field{} + } + // Initialize the fields for metrics (column headers) + fields := []table.Field{ + {Name: "Job"}, + {Name: "Read Latency (us)"}, + {Name: "Read IOPs"}, + {Name: "Read Bandwidth (MiB/s)"}, + {Name: "Write Latency (us)"}, + {Name: "Write IOPs"}, + {Name: "Write Bandwidth (MiB/s)"}, + } + // For each FIO job, create a new row and populate its values + for _, job := range fioData.Jobs { + fields[0].Values = append(fields[0].Values, job.Jobname) + fields[1].Values = append(fields[1].Values, formatOrEmpty("%.0f", job.Read.LatNs.Mean/1000)) + fields[2].Values = append(fields[2].Values, formatOrEmpty("%.0f", job.Read.IopsMean)) + fields[3].Values = append(fields[3].Values, formatOrEmpty("%d", job.Read.Bw/1024)) + fields[4].Values = append(fields[4].Values, formatOrEmpty("%.0f", job.Write.LatNs.Mean/1000)) + fields[5].Values = append(fields[5].Values, formatOrEmpty("%.0f", job.Write.IopsMean)) + fields[6].Values = append(fields[6].Values, formatOrEmpty("%d", job.Write.Bw/1024)) + } + return fields +} diff --git a/cmd/report/benchmarking.go b/cmd/benchmark/benchmarking.go similarity index 99% rename from cmd/report/benchmarking.go rename to cmd/benchmark/benchmarking.go index 10a8327e..483d840c 100644 --- a/cmd/report/benchmarking.go +++ b/cmd/benchmark/benchmarking.go @@ -1,4 +1,4 @@ -package report +package benchmark // Copyright (C) 2021-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause diff --git a/cmd/report/report.go b/cmd/report/report.go index b240750a..e96940fe 100644 --- a/cmd/report/report.go +++ b/cmd/report/report.go @@ -6,22 +6,15 @@ package report import ( "fmt" - "log/slog" - "os" "slices" - "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/xuri/excelize/v2" "perfspect/internal/common" - "perfspect/internal/cpus" "perfspect/internal/report" - "perfspect/internal/script" "perfspect/internal/table" - "perfspect/internal/util" ) const cmdName = "report" @@ -30,8 +23,6 @@ var examples = []string{ fmt.Sprintf(" Data from local host: $ %s %s", common.AppName, cmdName), fmt.Sprintf(" Specific data from local host: $ %s %s --bios --os --cpu --format html,json", common.AppName, cmdName), fmt.Sprintf(" All data from remote target: $ %s %s --target 192.168.1.1 --user fred --key fred_key", common.AppName, cmdName), - fmt.Sprintf(" Run all benchmarks: $ %s %s --benchmark all", common.AppName, cmdName), - fmt.Sprintf(" Run specific benchmarks: $ %s %s --benchmark speed,power", common.AppName, cmdName), fmt.Sprintf(" Data from multiple targets: $ %s %s --targets targets.yaml", common.AppName, cmdName), } @@ -82,9 +73,6 @@ var ( flagPmu bool flagSystemEventLog bool flagKernelLog bool - - flagBenchmark []string - flagStorageDir string ) // flag names @@ -123,36 +111,8 @@ const ( flagPmuName = "pmu" flagSystemEventLogName = "sel" flagKernelLogName = "kernellog" - - flagBenchmarkName = "benchmark" - flagStorageDirName = "storage-dir" ) -var benchmarkOptions = []string{ - "speed", - "power", - "temperature", - "frequency", - "memory", - "numa", - "storage", -} - -var benchmarkAll = "all" - -// map benchmark flag values, e.g., "--benchmark speed,power" to associated tables -var benchmarkTables = map[string][]table.TableDefinition{ - "speed": {tableDefinitions[SpeedBenchmarkTableName]}, - "power": {tableDefinitions[PowerBenchmarkTableName]}, - "temperature": {tableDefinitions[TemperatureBenchmarkTableName]}, - "frequency": {tableDefinitions[FrequencyBenchmarkTableName]}, - "memory": {tableDefinitions[MemoryBenchmarkTableName]}, - "numa": {tableDefinitions[NUMABenchmarkTableName]}, - "storage": {tableDefinitions[StorageBenchmarkTableName]}, -} - -var benchmarkSummaryTableName = "Benchmark Summary" - // categories maps flag names to tables that will be included in report var categories = []common.Category{ {FlagName: flagSystemSummaryName, FlagVar: &flagSystemSummary, Help: "System Summary", Tables: []table.TableDefinition{tableDefinitions[SystemSummaryTableName]}}, @@ -198,8 +158,6 @@ func init() { Cmd.Flags().StringVar(&common.FlagInput, common.FlagInputName, "", "") Cmd.Flags().BoolVar(&flagAll, flagAllName, true, "") Cmd.Flags().StringSliceVar(&common.FlagFormat, common.FlagFormatName, []string{report.FormatAll}, "") - Cmd.Flags().StringSliceVar(&flagBenchmark, flagBenchmarkName, []string{}, "") - Cmd.Flags().StringVar(&flagStorageDir, flagStorageDirName, "/tmp", "") common.AddTargetFlags(Cmd) @@ -254,14 +212,6 @@ func getFlagGroups() []common.FlagGroup { Name: common.FlagFormatName, Help: fmt.Sprintf("choose output format(s) from: %s", strings.Join(append([]string{report.FormatAll}, report.FormatOptions...), ", ")), }, - { - Name: flagBenchmarkName, - Help: fmt.Sprintf("choose benchmark(s) to include in report from: %s", strings.Join(append([]string{benchmarkAll}, benchmarkOptions...), ", ")), - }, - { - Name: flagStorageDirName, - Help: "existing directory where storage performance benchmark data will be temporarily stored", - }, } groups = append(groups, common.FlagGroup{ GroupName: "Other Options", @@ -298,30 +248,6 @@ func validateFlags(cmd *cobra.Command, args []string) error { return common.FlagValidationError(cmd, fmt.Sprintf("format options are: %s", strings.Join(formatOptions, ", "))) } } - // validate benchmark options - for _, benchmark := range flagBenchmark { - options := append([]string{benchmarkAll}, benchmarkOptions...) - if !slices.Contains(options, benchmark) { - return common.FlagValidationError(cmd, fmt.Sprintf("benchmark options are: %s", strings.Join(options, ", "))) - } - } - // if benchmark all is selected, replace it with all benchmark options - if slices.Contains(flagBenchmark, benchmarkAll) { - flagBenchmark = benchmarkOptions - } - - // validate storage dir - if flagStorageDir != "" { - if !util.IsValidDirectoryName(flagStorageDir) { - return common.FlagValidationError(cmd, fmt.Sprintf("invalid storage directory name: %s", flagStorageDir)) - } - // if no target is specified, i.e., we have a local target only, check if the directory exists - if !cmd.Flags().Lookup("targets").Changed && !cmd.Flags().Lookup("target").Changed { - if _, err := os.Stat(flagStorageDir); os.IsNotExist(err) { - return common.FlagValidationError(cmd, fmt.Sprintf("storage dir does not exist: %s", flagStorageDir)) - } - } - } // common target flags if err := common.ValidateTargetFlags(cmd); err != nil { return common.FlagValidationError(cmd, err.Error()) @@ -337,15 +263,6 @@ func runCmd(cmd *cobra.Command, args []string) error { tables = append(tables, cat.Tables...) } } - // add benchmark tables - for _, benchmarkFlagValue := range flagBenchmark { - tables = append(tables, benchmarkTables[benchmarkFlagValue]...) - } - // include benchmark summary table if all benchmark options are selected - var summaryFunc common.SummaryFunc - if len(flagBenchmark) == len(benchmarkOptions) { - summaryFunc = benchmarkSummaryFromTableValues - } // include insights table if all categories are selected var insightsFunc common.InsightsFunc if flagAll { @@ -353,201 +270,12 @@ func runCmd(cmd *cobra.Command, args []string) error { } reportingCommand := common.ReportingCommand{ Cmd: cmd, - ScriptParams: map[string]string{"StorageDir": flagStorageDir}, Tables: tables, - SummaryFunc: summaryFunc, - SummaryTableName: benchmarkSummaryTableName, - SummaryBeforeTableName: SpeedBenchmarkTableName, InsightsFunc: insightsFunc, SystemSummaryTableName: SystemSummaryTableName, } report.RegisterHTMLRenderer(DIMMTableName, dimmTableHTMLRenderer) - report.RegisterHTMLRenderer(FrequencyBenchmarkTableName, frequencyBenchmarkTableHtmlRenderer) - report.RegisterHTMLRenderer(MemoryBenchmarkTableName, memoryBenchmarkTableHtmlRenderer) - - report.RegisterHTMLMultiTargetRenderer(MemoryBenchmarkTableName, memoryBenchmarkTableMultiTargetHtmlRenderer) return reportingCommand.Run() } - -func benchmarkSummaryFromTableValues(allTableValues []table.TableValues, outputs map[string]script.ScriptOutput) table.TableValues { - maxFreq := getValueFromTableValues(getTableValues(allTableValues, FrequencyBenchmarkTableName), "SSE", 0) - if maxFreq != "" { - maxFreq = maxFreq + " GHz" - } - allCoreMaxFreq := getValueFromTableValues(getTableValues(allTableValues, FrequencyBenchmarkTableName), "SSE", -1) - if allCoreMaxFreq != "" { - allCoreMaxFreq = allCoreMaxFreq + " GHz" - } - // get the maximum memory bandwidth from the memory latency table - memLatTableValues := getTableValues(allTableValues, MemoryBenchmarkTableName) - var bandwidthValues []string - if len(memLatTableValues.Fields) > 1 { - bandwidthValues = memLatTableValues.Fields[1].Values - } - maxBandwidth := 0.0 - for _, bandwidthValue := range bandwidthValues { - bandwidth, err := strconv.ParseFloat(bandwidthValue, 64) - if err != nil { - slog.Error("unexpected value in memory bandwidth", slog.String("error", err.Error()), slog.Float64("value", bandwidth)) - break - } - if bandwidth > maxBandwidth { - maxBandwidth = bandwidth - } - } - maxMemBW := "" - if maxBandwidth != 0 { - maxMemBW = fmt.Sprintf("%.1f GB/s", maxBandwidth) - } - // get the minimum memory latency - minLatency := getValueFromTableValues(getTableValues(allTableValues, MemoryBenchmarkTableName), "Latency (ns)", 0) - if minLatency != "" { - minLatency = minLatency + " ns" - } - - report.RegisterHTMLRenderer(benchmarkSummaryTableName, summaryHTMLTableRenderer) - report.RegisterTextRenderer(benchmarkSummaryTableName, summaryTextTableRenderer) - report.RegisterXlsxRenderer(benchmarkSummaryTableName, summaryXlsxTableRenderer) - - return table.TableValues{ - TableDefinition: table.TableDefinition{ - Name: benchmarkSummaryTableName, - HasRows: false, - MenuLabel: benchmarkSummaryTableName, - }, - Fields: []table.Field{ - {Name: "CPU Speed", Values: []string{getValueFromTableValues(getTableValues(allTableValues, SpeedBenchmarkTableName), "Ops/s", 0) + " Ops/s"}}, - {Name: "Single-core Maximum frequency", Values: []string{maxFreq}}, - {Name: "All-core Maximum frequency", Values: []string{allCoreMaxFreq}}, - {Name: "Maximum Power", Values: []string{getValueFromTableValues(getTableValues(allTableValues, PowerBenchmarkTableName), "Maximum Power", 0)}}, - {Name: "Maximum Temperature", Values: []string{getValueFromTableValues(getTableValues(allTableValues, TemperatureBenchmarkTableName), "Maximum Temperature", 0)}}, - {Name: "Minimum Power", Values: []string{getValueFromTableValues(getTableValues(allTableValues, PowerBenchmarkTableName), "Minimum Power", 0)}}, - {Name: "Memory Peak Bandwidth", Values: []string{maxMemBW}}, - {Name: "Memory Minimum Latency", Values: []string{minLatency}}, - {Name: "Microarchitecture", Values: []string{getValueFromTableValues(getTableValues(allTableValues, SystemSummaryTableName), "Microarchitecture", 0)}}, - {Name: "Sockets", Values: []string{getValueFromTableValues(getTableValues(allTableValues, SystemSummaryTableName), "Sockets", 0)}}, - }, - } -} - -// getTableValues returns the table values for a table with a given name -func getTableValues(allTableValues []table.TableValues, tableName string) table.TableValues { - for _, tv := range allTableValues { - if tv.Name == tableName { - return tv - } - } - return table.TableValues{} -} - -// getValueFromTableValues returns the value of a field in a table -// if row is -1, it returns the last value -func getValueFromTableValues(tv table.TableValues, fieldName string, row int) string { - for _, fv := range tv.Fields { - if fv.Name == fieldName { - if row == -1 { // return the last value - if len(fv.Values) == 0 { - return "" - } - return fv.Values[len(fv.Values)-1] - } - if len(fv.Values) > row { - return fv.Values[row] - } - break - } - } - return "" -} - -// ReferenceData is a struct that holds reference data for a microarchitecture -type ReferenceData struct { - Description string - CPUSpeed float64 - SingleCoreFreq float64 - AllCoreFreq float64 - MaxPower float64 - MaxTemp float64 - MinPower float64 - MemPeakBandwidth float64 - MemMinLatency float64 -} - -// ReferenceDataKey is a struct that holds the key for reference data -type ReferenceDataKey struct { - Microarchitecture string - Sockets string -} - -// referenceData is a map of reference data for microarchitectures -var referenceData = map[ReferenceDataKey]ReferenceData{ - {cpus.UarchBDX, "2"}: {Description: "Reference (Intel 2S Xeon E5-2699 v4)", CPUSpeed: 403415, SingleCoreFreq: 3509, AllCoreFreq: 2980, MaxPower: 289.9, MaxTemp: 0, MinPower: 0, MemPeakBandwidth: 138.1, MemMinLatency: 78}, - {cpus.UarchSKX, "2"}: {Description: "Reference (Intel 2S Xeon 8180)", CPUSpeed: 585157, SingleCoreFreq: 3758, AllCoreFreq: 3107, MaxPower: 429.07, MaxTemp: 0, MinPower: 0, MemPeakBandwidth: 225.1, MemMinLatency: 71}, - {cpus.UarchCLX, "2"}: {Description: "Reference (Intel 2S Xeon 8280)", CPUSpeed: 548644, SingleCoreFreq: 3928, AllCoreFreq: 3926, MaxPower: 415.93, MaxTemp: 0, MinPower: 0, MemPeakBandwidth: 223.9, MemMinLatency: 72}, - {cpus.UarchICX, "2"}: {Description: "Reference (Intel 2S Xeon 8380)", CPUSpeed: 933644, SingleCoreFreq: 3334, AllCoreFreq: 2950, MaxPower: 552, MaxTemp: 0, MinPower: 175.38, MemPeakBandwidth: 350.7, MemMinLatency: 70}, - {cpus.UarchSPR_XCC, "2"}: {Description: "Reference (Intel 2S Xeon 8480+)", CPUSpeed: 1678712, SingleCoreFreq: 3776, AllCoreFreq: 2996, MaxPower: 698.35, MaxTemp: 0, MinPower: 249.21, MemPeakBandwidth: 524.6, MemMinLatency: 111.8}, - {cpus.UarchSPR_XCC, "1"}: {Description: "Reference (Intel 1S Xeon 8480+)", CPUSpeed: 845743, SingleCoreFreq: 3783, AllCoreFreq: 2999, MaxPower: 334.68, MaxTemp: 0, MinPower: 163.79, MemPeakBandwidth: 264.0, MemMinLatency: 112.2}, - {cpus.UarchEMR_XCC, "2"}: {Description: "Reference (Intel 2S Xeon 8592V)", CPUSpeed: 1789534, SingleCoreFreq: 3862, AllCoreFreq: 2898, MaxPower: 664.4, MaxTemp: 0, MinPower: 166.36, MemPeakBandwidth: 553.5, MemMinLatency: 92.0}, - {cpus.UarchSRF_SP, "2"}: {Description: "Reference (Intel 2S Xeon 6780E)", CPUSpeed: 3022446, SingleCoreFreq: 3001, AllCoreFreq: 3001, MaxPower: 583.97, MaxTemp: 0, MinPower: 123.34, MemPeakBandwidth: 534.3, MemMinLatency: 129.25}, - {cpus.UarchGNR_X2, "2"}: {Description: "Reference (Intel 2S Xeon 6787P)", CPUSpeed: 3178562, SingleCoreFreq: 3797, AllCoreFreq: 3199, MaxPower: 679, MaxTemp: 0, MinPower: 248.49, MemPeakBandwidth: 749.6, MemMinLatency: 117.51}, -} - -// getFieldIndex returns the index of a field in a list of fields -func getFieldIndex(fields []table.Field, fieldName string) (int, error) { - for i, field := range fields { - if field.Name == fieldName { - return i, nil - } - } - return -1, fmt.Errorf("field not found: %s", fieldName) -} - -// summaryHTMLTableRenderer is a custom HTML table renderer for the summary table -// it removes the Microarchitecture and Sockets fields and adds a reference table -func summaryHTMLTableRenderer(tv table.TableValues, targetName string) string { - uarchFieldIdx, err := getFieldIndex(tv.Fields, "Microarchitecture") - if err != nil { - panic(err) - } - socketsFieldIdx, err := getFieldIndex(tv.Fields, "Sockets") - if err != nil { - panic(err) - } - // if we have reference data that matches the microarchitecture and sockets, use it - if len(tv.Fields[uarchFieldIdx].Values) > 0 && len(tv.Fields[socketsFieldIdx].Values) > 0 { - if refData, ok := referenceData[ReferenceDataKey{tv.Fields[uarchFieldIdx].Values[0], tv.Fields[socketsFieldIdx].Values[0]}]; ok { - // remove microarchitecture and sockets fields - fields := tv.Fields[:len(tv.Fields)-2] - refTableValues := table.TableValues{ - Fields: []table.Field{ - {Name: "CPU Speed", Values: []string{fmt.Sprintf("%.0f Ops/s", refData.CPUSpeed)}}, - {Name: "Single-core Maximum frequency", Values: []string{fmt.Sprintf("%.0f MHz", refData.SingleCoreFreq)}}, - {Name: "All-core Maximum frequency", Values: []string{fmt.Sprintf("%.0f MHz", refData.AllCoreFreq)}}, - {Name: "Maximum Power", Values: []string{fmt.Sprintf("%.0f W", refData.MaxPower)}}, - {Name: "Maximum Temperature", Values: []string{fmt.Sprintf("%.0f C", refData.MaxTemp)}}, - {Name: "Minimum Power", Values: []string{fmt.Sprintf("%.0f W", refData.MinPower)}}, - {Name: "Memory Peak Bandwidth", Values: []string{fmt.Sprintf("%.0f GB/s", refData.MemPeakBandwidth)}}, - {Name: "Memory Minimum Latency", Values: []string{fmt.Sprintf("%.0f ns", refData.MemMinLatency)}}, - }, - } - return report.RenderMultiTargetTableValuesAsHTML([]table.TableValues{{TableDefinition: tv.TableDefinition, Fields: fields}, refTableValues}, []string{targetName, refData.Description}) - } - } - // remove microarchitecture and sockets fields - fields := tv.Fields[:len(tv.Fields)-2] - return report.DefaultHTMLTableRendererFunc(table.TableValues{TableDefinition: tv.TableDefinition, Fields: fields}) -} - -func summaryXlsxTableRenderer(tv table.TableValues, f *excelize.File, targetName string, row *int) { - // remove microarchitecture and sockets fields - fields := tv.Fields[:len(tv.Fields)-2] - report.DefaultXlsxTableRendererFunc(table.TableValues{TableDefinition: tv.TableDefinition, Fields: fields}, f, report.XlsxPrimarySheetName, row) -} - -func summaryTextTableRenderer(tv table.TableValues) string { - // remove microarchitecture and sockets fields - fields := tv.Fields[:len(tv.Fields)-2] - return report.DefaultTextTableRendererFunc(table.TableValues{TableDefinition: tv.TableDefinition, Fields: fields}) -} diff --git a/cmd/report/report_tables.go b/cmd/report/report_tables.go index 5c243cb6..8db5fdcd 100644 --- a/cmd/report/report_tables.go +++ b/cmd/report/report_tables.go @@ -19,7 +19,6 @@ import ( "perfspect/internal/report" "perfspect/internal/script" "perfspect/internal/table" - "perfspect/internal/util" ) const ( @@ -62,14 +61,6 @@ const ( SystemEventLogTableName = "System Event Log" KernelLogTableName = "Kernel Log" SystemSummaryTableName = "System Summary" - // benchmark table names - SpeedBenchmarkTableName = "Speed Benchmark" - PowerBenchmarkTableName = "Power Benchmark" - TemperatureBenchmarkTableName = "Temperature Benchmark" - FrequencyBenchmarkTableName = "Frequency Benchmark" - MemoryBenchmarkTableName = "Memory Benchmark" - NUMABenchmarkTableName = "NUMA Benchmark" - StorageBenchmarkTableName = "Storage Benchmark" ) // menu labels @@ -463,77 +454,6 @@ var tableDefinitions = map[string]table.TableDefinition{ script.ArmDmidecodePartScriptName, }, FieldsFunc: systemSummaryTableValues}, - // - // benchmarking tables - // - SpeedBenchmarkTableName: { - Name: SpeedBenchmarkTableName, - MenuLabel: SpeedBenchmarkTableName, - HasRows: false, - ScriptNames: []string{ - script.SpeedBenchmarkScriptName, - }, - FieldsFunc: speedBenchmarkTableValues}, - PowerBenchmarkTableName: { - Name: PowerBenchmarkTableName, - MenuLabel: PowerBenchmarkTableName, - Architectures: []string{cpus.X86Architecture}, - HasRows: false, - ScriptNames: []string{ - script.IdlePowerBenchmarkScriptName, - script.PowerBenchmarkScriptName, - }, - FieldsFunc: powerBenchmarkTableValues}, - TemperatureBenchmarkTableName: { - Name: TemperatureBenchmarkTableName, - MenuLabel: TemperatureBenchmarkTableName, - Architectures: []string{cpus.X86Architecture}, - HasRows: false, - ScriptNames: []string{ - script.PowerBenchmarkScriptName, - }, - FieldsFunc: temperatureBenchmarkTableValues}, - FrequencyBenchmarkTableName: { - Name: FrequencyBenchmarkTableName, - MenuLabel: FrequencyBenchmarkTableName, - Architectures: []string{cpus.X86Architecture}, - HasRows: true, - ScriptNames: []string{ - script.SpecCoreFrequenciesScriptName, - script.LscpuScriptName, - script.LspciBitsScriptName, - script.LspciDevicesScriptName, - script.FrequencyBenchmarkScriptName, - }, - FieldsFunc: frequencyBenchmarkTableValues}, - MemoryBenchmarkTableName: { - Name: MemoryBenchmarkTableName, - MenuLabel: MemoryBenchmarkTableName, - Architectures: []string{cpus.X86Architecture}, - HasRows: true, - ScriptNames: []string{ - script.MemoryBenchmarkScriptName, - }, - NoDataFound: "No memory benchmark data found. Please see the GitHub repository README for instructions on how to install Intel Memory Latency Checker (mlc).", - FieldsFunc: memoryBenchmarkTableValues}, - NUMABenchmarkTableName: { - Name: NUMABenchmarkTableName, - MenuLabel: NUMABenchmarkTableName, - Architectures: []string{cpus.X86Architecture}, - HasRows: true, - ScriptNames: []string{ - script.NumaBenchmarkScriptName, - }, - NoDataFound: "No NUMA benchmark data found. Please see the GitHub repository README for instructions on how to install Intel Memory Latency Checker (mlc).", - FieldsFunc: numaBenchmarkTableValues}, - StorageBenchmarkTableName: { - Name: StorageBenchmarkTableName, - MenuLabel: StorageBenchmarkTableName, - HasRows: true, - ScriptNames: []string{ - script.StorageBenchmarkScriptName, - }, - FieldsFunc: storageBenchmarkTableValues}, } // @@ -1770,237 +1690,6 @@ func systemSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Fi {Name: "System Summary", Values: []string{systemSummaryFromOutput(outputs)}}, } } - -// benchmarking - -func speedBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { - return []table.Field{ - {Name: "Ops/s", Values: []string{cpuSpeedFromOutput(outputs)}}, - } -} - -func powerBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { - return []table.Field{ - {Name: "Maximum Power", Values: []string{common.MaxTotalPackagePowerFromOutput(outputs[script.PowerBenchmarkScriptName].Stdout)}}, - {Name: "Minimum Power", Values: []string{common.MinTotalPackagePowerFromOutput(outputs[script.IdlePowerBenchmarkScriptName].Stdout)}}, - } -} - -func temperatureBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { - return []table.Field{ - {Name: "Maximum Temperature", Values: []string{common.MaxPackageTemperatureFromOutput(outputs[script.PowerBenchmarkScriptName].Stdout)}}, - } -} - -func frequencyBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { - // get the sse, avx256, and avx512 frequencies from the avx-turbo output - instructionFreqs, err := avxTurboFrequenciesFromOutput(outputs[script.FrequencyBenchmarkScriptName].Stdout) - if err != nil { - slog.Warn("unable to get avx turbo frequencies", slog.String("error", err.Error())) - return []table.Field{} - } - // we're expecting scalar_iadd, avx256_fma, avx512_fma - scalarIaddFreqs := instructionFreqs["scalar_iadd"] - avx256FmaFreqs := instructionFreqs["avx256_fma"] - avx512FmaFreqs := instructionFreqs["avx512_fma"] - // stop if we don't have any scalar_iadd frequencies - if len(scalarIaddFreqs) == 0 { - slog.Warn("no scalar_iadd frequencies found") - return []table.Field{} - } - // get the spec core frequencies from the spec output - var specSSEFreqs []string - frequencyBuckets, err := common.GetSpecFrequencyBuckets(outputs) - if err == nil && len(frequencyBuckets) >= 2 { - // get the frequencies from the buckets - specSSEFreqs, err = common.ExpandTurboFrequencies(frequencyBuckets, "sse") - if err != nil { - slog.Error("unable to convert buckets to counts", slog.String("error", err.Error())) - return []table.Field{} - } - // trim the spec frequencies to the length of the scalar_iadd frequencies - // this can happen when the actual core count is less than the number of cores in the spec - if len(scalarIaddFreqs) < len(specSSEFreqs) { - specSSEFreqs = specSSEFreqs[:len(scalarIaddFreqs)] - } - // pad the spec frequencies with the last value if they are shorter than the scalar_iadd frequencies - // this can happen when the first die has fewer cores than other dies - if len(specSSEFreqs) < len(scalarIaddFreqs) { - diff := len(scalarIaddFreqs) - len(specSSEFreqs) - for range diff { - specSSEFreqs = append(specSSEFreqs, specSSEFreqs[len(specSSEFreqs)-1]) - } - } - } - // create the fields - fields := []table.Field{ - {Name: "cores"}, - } - coresIdx := 0 // always the first field - var specSSEFieldIdx int - var scalarIaddFieldIdx int - var avx2FieldIdx int - var avx512FieldIdx int - if len(specSSEFreqs) > 0 { - fields = append(fields, table.Field{Name: "SSE (expected)", Description: "The expected frequency, when running SSE instructions, for the given number of active cores."}) - specSSEFieldIdx = len(fields) - 1 - } - if len(scalarIaddFreqs) > 0 { - fields = append(fields, table.Field{Name: "SSE", Description: "The measured frequency, when running SSE instructions, for the given number of active cores."}) - scalarIaddFieldIdx = len(fields) - 1 - } - if len(avx256FmaFreqs) > 0 { - fields = append(fields, table.Field{Name: "AVX2", Description: "The measured frequency, when running AVX2 instructions, for the given number of active cores."}) - avx2FieldIdx = len(fields) - 1 - } - if len(avx512FmaFreqs) > 0 { - fields = append(fields, table.Field{Name: "AVX512", Description: "The measured frequency, when running AVX512 instructions, for the given number of active cores."}) - avx512FieldIdx = len(fields) - 1 - } - // add the data to the fields - for i := range scalarIaddFreqs { // scalarIaddFreqs is required - fields[coresIdx].Values = append(fields[coresIdx].Values, fmt.Sprintf("%d", i+1)) - if specSSEFieldIdx > 0 { - if len(specSSEFreqs) > i { - fields[specSSEFieldIdx].Values = append(fields[specSSEFieldIdx].Values, specSSEFreqs[i]) - } else { - fields[specSSEFieldIdx].Values = append(fields[specSSEFieldIdx].Values, "") - } - } - if scalarIaddFieldIdx > 0 { - if len(scalarIaddFreqs) > i { - fields[scalarIaddFieldIdx].Values = append(fields[scalarIaddFieldIdx].Values, fmt.Sprintf("%.1f", scalarIaddFreqs[i])) - } else { - fields[scalarIaddFieldIdx].Values = append(fields[scalarIaddFieldIdx].Values, "") - } - } - if avx2FieldIdx > 0 { - if len(avx256FmaFreqs) > i { - fields[avx2FieldIdx].Values = append(fields[avx2FieldIdx].Values, fmt.Sprintf("%.1f", avx256FmaFreqs[i])) - } else { - fields[avx2FieldIdx].Values = append(fields[avx2FieldIdx].Values, "") - } - } - if avx512FieldIdx > 0 { - if len(avx512FmaFreqs) > i { - fields[avx512FieldIdx].Values = append(fields[avx512FieldIdx].Values, fmt.Sprintf("%.1f", avx512FmaFreqs[i])) - } else { - fields[avx512FieldIdx].Values = append(fields[avx512FieldIdx].Values, "") - } - } - } - return fields -} - -func memoryBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { - fields := []table.Field{ - {Name: "Latency (ns)"}, - {Name: "Bandwidth (GB/s)"}, - } - /* MLC Output: - Inject Latency Bandwidth - Delay (ns) MB/sec - ========================== - 00000 261.65 225060.9 - 00002 261.63 225040.5 - 00008 261.54 225073.3 - ... - */ - latencyBandwidthPairs := common.ValsArrayFromRegexSubmatch(outputs[script.MemoryBenchmarkScriptName].Stdout, `\s*[0-9]*\s*([0-9]*\.[0-9]+)\s*([0-9]*\.[0-9]+)`) - for _, latencyBandwidth := range latencyBandwidthPairs { - latency := latencyBandwidth[0] - bandwidth, err := strconv.ParseFloat(latencyBandwidth[1], 32) - if err != nil { - slog.Error(fmt.Sprintf("Unable to convert bandwidth to float: %s", latencyBandwidth[1])) - continue - } - // insert into beginning of list - fields[0].Values = append([]string{latency}, fields[0].Values...) - fields[1].Values = append([]string{fmt.Sprintf("%.1f", bandwidth/1000)}, fields[1].Values...) - } - if len(fields[0].Values) == 0 { - return []table.Field{} - } - return fields -} - -func numaBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { - fields := []table.Field{ - {Name: "Node"}, - } - /* MLC Output: - Numa node - Numa node 0 1 - 0 175610.3 55579.7 - 1 55575.2 175656.7 - */ - nodeBandwidthsPairs := common.ValsArrayFromRegexSubmatch(outputs[script.NumaBenchmarkScriptName].Stdout, `^\s+(\d)\s+(\d.*)$`) - // add 1 field per numa node - for _, nodeBandwidthsPair := range nodeBandwidthsPairs { - fields = append(fields, table.Field{Name: nodeBandwidthsPair[0]}) - } - // add rows - for _, nodeBandwidthsPair := range nodeBandwidthsPairs { - fields[0].Values = append(fields[0].Values, nodeBandwidthsPair[0]) - bandwidths := strings.Split(strings.TrimSpace(nodeBandwidthsPair[1]), "\t") - if len(bandwidths) != len(nodeBandwidthsPairs) { - slog.Warn(fmt.Sprintf("Mismatched number of bandwidths for numa node %s, %s", nodeBandwidthsPair[0], nodeBandwidthsPair[1])) - return []table.Field{} - } - for i, bw := range bandwidths { - bw = strings.TrimSpace(bw) - val, err := strconv.ParseFloat(bw, 64) - if err != nil { - slog.Error(fmt.Sprintf("Unable to convert bandwidth to float: %s", bw)) - continue - } - fields[i+1].Values = append(fields[i+1].Values, fmt.Sprintf("%.1f", val/1000)) - } - } - if len(fields[0].Values) == 0 { - return []table.Field{} - } - return fields -} - -// formatOrEmpty formats a value and returns an empty string if the formatted value is "0". -func formatOrEmpty(format string, value any) string { - s := fmt.Sprintf(format, value) - if s == "0" { - return "" - } - return s -} - -func storageBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { - fioData, err := storagePerfFromOutput(outputs) - if err != nil { - slog.Warn("failed to get storage benchmark data", slog.String("error", err.Error())) - return []table.Field{} - } - // Initialize the fields for metrics (column headers) - fields := []table.Field{ - {Name: "Job"}, - {Name: "Read Latency (us)"}, - {Name: "Read IOPs"}, - {Name: "Read Bandwidth (MiB/s)"}, - {Name: "Write Latency (us)"}, - {Name: "Write IOPs"}, - {Name: "Write Bandwidth (MiB/s)"}, - } - // For each FIO job, create a new row and populate its values - for _, job := range fioData.Jobs { - fields[0].Values = append(fields[0].Values, job.Jobname) - fields[1].Values = append(fields[1].Values, formatOrEmpty("%.0f", job.Read.LatNs.Mean/1000)) - fields[2].Values = append(fields[2].Values, formatOrEmpty("%.0f", job.Read.IopsMean)) - fields[3].Values = append(fields[3].Values, formatOrEmpty("%d", job.Read.Bw/1024)) - fields[4].Values = append(fields[4].Values, formatOrEmpty("%.0f", job.Write.LatNs.Mean/1000)) - fields[5].Values = append(fields[5].Values, formatOrEmpty("%.0f", job.Write.IopsMean)) - fields[6].Values = append(fields[6].Values, formatOrEmpty("%d", job.Write.Bw/1024)) - } - return fields -} - func dimmDetails(dimm []string) (details string) { if strings.Contains(dimm[SizeIdx], "No") { details = "No Module Installed" @@ -2119,100 +1808,3 @@ func dimmTableHTMLRenderer(tableValues table.TableValues, targetName string) str } return report.RenderHTMLTable(socketTableHeaders, socketTableValues, "pure-table pure-table-bordered", [][]string{}) } - -func renderFrequencyTable(tableValues table.TableValues) (out string) { - var rows [][]string - headers := []string{""} - valuesStyles := [][]string{} - for i := range tableValues.Fields[0].Values { - headers = append(headers, fmt.Sprintf("%d", i+1)) - } - for _, field := range tableValues.Fields[1:] { - row := append([]string{report.CreateFieldNameWithDescription(field.Name, field.Description)}, field.Values...) - rows = append(rows, row) - valuesStyles = append(valuesStyles, []string{"font-weight:bold"}) - } - out = report.RenderHTMLTable(headers, rows, "pure-table pure-table-striped", valuesStyles) - return -} - -func coreTurboFrequencyTableHTMLRenderer(tableValues table.TableValues) string { - data := [][]report.ScatterPoint{} - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - points := []report.ScatterPoint{} - for i, val := range field.Values { - if val == "" { - break - } - freq, err := strconv.ParseFloat(val, 64) - if err != nil { - slog.Error("error parsing frequency", slog.String("error", err.Error())) - return "" - } - points = append(points, report.ScatterPoint{X: float64(i + 1), Y: freq}) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, field.Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("turboFrequency%d", util.RandUint(10000)), - XaxisText: "Core Count", - YaxisText: "Frequency (GHz)", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "4", - SuggestedMin: "2", - SuggestedMax: "4", - } - out := report.RenderScatterChart(data, datasetNames, chartConfig) - out += "\n" - out += renderFrequencyTable(tableValues) - return out -} - -func frequencyBenchmarkTableHtmlRenderer(tableValues table.TableValues, targetName string) string { - return coreTurboFrequencyTableHTMLRenderer(tableValues) -} - -func memoryBenchmarkTableHtmlRenderer(tableValues table.TableValues, targetName string) string { - return memoryBenchmarkTableMultiTargetHtmlRenderer([]table.TableValues{tableValues}, []string{targetName}) -} - -func memoryBenchmarkTableMultiTargetHtmlRenderer(allTableValues []table.TableValues, targetNames []string) string { - data := [][]report.ScatterPoint{} - datasetNames := []string{} - for targetIdx, tableValues := range allTableValues { - points := []report.ScatterPoint{} - for valIdx := range tableValues.Fields[0].Values { - latency, err := strconv.ParseFloat(tableValues.Fields[0].Values[valIdx], 64) - if err != nil { - slog.Error("error parsing latency", slog.String("error", err.Error())) - return "" - } - bandwidth, err := strconv.ParseFloat(tableValues.Fields[1].Values[valIdx], 64) - if err != nil { - slog.Error("error parsing bandwidth", slog.String("error", err.Error())) - return "" - } - points = append(points, report.ScatterPoint{X: bandwidth, Y: latency}) - } - data = append(data, points) - datasetNames = append(datasetNames, targetNames[targetIdx]) - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("latencyBandwidth%d", util.RandUint(10000)), - XaxisText: "Bandwidth (GB/s)", - YaxisText: "Latency (ns)", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "4", - SuggestedMin: "0", - SuggestedMax: "0", - } - return report.RenderScatterChart(data, datasetNames, chartConfig) -} diff --git a/cmd/root.go b/cmd/root.go index 17b0854e..66f8461e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ import ( "syscall" "time" + "perfspect/cmd/benchmark" "perfspect/cmd/config" "perfspect/cmd/flamegraph" "perfspect/cmd/lock" @@ -114,6 +115,7 @@ Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} rootCmd.CompletionOptions.HiddenDefaultCmd = true rootCmd.AddGroup([]*cobra.Group{{ID: "primary", Title: "Commands:"}}...) rootCmd.AddCommand(report.Cmd) + rootCmd.AddCommand(benchmark.Cmd) rootCmd.AddCommand(metrics.Cmd) rootCmd.AddCommand(telemetry.Cmd) rootCmd.AddCommand(flamegraph.Cmd) diff --git a/internal/common/turbostat.go b/internal/common/turbostat.go index 54b6c77a..04b2be90 100644 --- a/internal/common/turbostat.go +++ b/internal/common/turbostat.go @@ -240,6 +240,11 @@ func MaxTotalPackagePowerFromOutput(turbostatOutput string) string { slog.Warn("unable to parse power value", slog.String("value", wattStr), slog.String("error", err.Error())) continue } + // Filter out anomalous high readings. Turbostat sometimes reports very high power values that are not realistic. + if watt > 10000 { + slog.Warn("ignoring anomalous high power reading", slog.String("value", wattStr)) + continue + } if watt > maxPower { maxPower = watt } @@ -316,6 +321,11 @@ func MaxPackageTemperatureFromOutput(turbostatOutput string) string { slog.Warn("unable to parse temperature value", slog.String("value", tempStr), slog.String("error", err.Error())) continue } + // Filter out anomalous high readings. Turbostat sometimes reports very high temperature values that are not realistic. + if temp > 200 { + slog.Warn("ignoring anomalous high temperature reading", slog.String("value", tempStr)) + continue + } if temp > maxTemp { maxTemp = temp }