From deff13fafe7eea2cf165f943bb851e28bc05833c Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:25:54 -0800 Subject: [PATCH 01/24] Extract benchmark functionality into standalone command - Create new 'benchmark' command under cmd/benchmark/ - Implement benchmark flags following telemetry command pattern: * --all (default) - run all benchmarks * --speed, --power, --temperature, --frequency, --memory, --numa, --storage - Move benchmark table definitions and processing functions to new command - Move benchmark HTML renderers to new command - Remove benchmark functionality from report command: * Remove --benchmark flag * Remove benchmark table definitions * Remove benchmark data processing functions * Clean up unused imports - Register benchmark command in root command The benchmark command now provides a cleaner separation of concerns, following the same flag pattern as the telemetry command. Usage: perfspect benchmark # Runs all benchmarks perfspect benchmark --speed --power # Runs specific benchmarks perfspect benchmark --target # Benchmark remote target --- cmd/benchmark/benchmark.go | 426 +++++++++++++++++++++++++++ cmd/benchmark/benchmark_renderers.go | 111 +++++++ cmd/benchmark/benchmark_tables.go | 345 ++++++++++++++++++++++ cmd/benchmark/benchmarking.go | 203 +++++++++++++ cmd/report/report.go | 272 ----------------- cmd/report/report_tables.go | 407 ------------------------- cmd/root.go | 2 + 7 files changed, 1087 insertions(+), 679 deletions(-) create mode 100644 cmd/benchmark/benchmark.go create mode 100644 cmd/benchmark/benchmark_renderers.go create mode 100644 cmd/benchmark/benchmark_tables.go create mode 100644 cmd/benchmark/benchmarking.go diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go new file mode 100644 index 00000000..5da5bcfb --- /dev/null +++ b/cmd/benchmark/benchmark.go @@ -0,0 +1,426 @@ +// 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, + 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 + + flagStorageDir string +) + +// flag names +const ( + flagAllName = "all" + + flagSpeedName = "speed" + flagPowerName = "power" + flagTemperatureName = "temperature" + flagFrequencyName = "frequency" + flagMemoryName = "memory" + flagNumaName = "numa" + flagStorageName = "storage" + + flagStorageDirName = "storage-dir" +) + +var benchmarkSummaryTableName = "Benchmark Summary" + +var benchmarks = []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 benchmarks { + 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().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 benchmarks { + 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: 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 benchmarks { + 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 { + tables := []table.TableDefinition{} + // add benchmark tables + selectedBenchmarkCount := 0 + for _, benchmark := range benchmarks { + 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(benchmarks) { + summaryFunc = benchmarkSummaryFromTableValues + } + + reportingCommand := common.ReportingCommand{ + Cmd: cmd, + ScriptParams: map[string]string{"StorageDir": flagStorageDir}, + Tables: tables, + SummaryFunc: summaryFunc, + SummaryTableName: benchmarkSummaryTableName, + SummaryBeforeTableName: SpeedBenchmarkTableName, + InsightsFunc: nil, + SystemSummaryTableName: SystemSummaryTableName, + } + + 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/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..eb29dd6b --- /dev/null +++ b/cmd/benchmark/benchmark_tables.go @@ -0,0 +1,345 @@ +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" + SystemSummaryTableName = "System Summary" +) + +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}, + SystemSummaryTableName: { + Name: SystemSummaryTableName, + MenuLabel: SystemSummaryTableName, + HasRows: false, + ScriptNames: []string{ + script.LscpuScriptName, + script.UnameScriptName, + }, + FieldsFunc: systemSummaryTableValues}, +} + +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 systemSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Field { + lscpuOutput := outputs[script.LscpuScriptName].Stdout + return []table.Field{ + {Name: "Microarchitecture", Values: []string{common.UarchFromOutput(outputs)}}, + {Name: "Sockets", Values: []string{common.ValFromRegexSubmatch(lscpuOutput, `^Socket\(s\):\s*(.+)$`)}}, + } +} diff --git a/cmd/benchmark/benchmarking.go b/cmd/benchmark/benchmarking.go new file mode 100644 index 00000000..483d840c --- /dev/null +++ b/cmd/benchmark/benchmarking.go @@ -0,0 +1,203 @@ +package benchmark + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "encoding/json" + "fmt" + "log/slog" + "perfspect/internal/script" + "perfspect/internal/util" + "strconv" + "strings" +) + +// fioOutput is the top-level struct for the FIO JSON report. +// ref: https://fio.readthedocs.io/en/latest/fio_doc.html#json-output +type fioOutput struct { + FioVersion string `json:"fio version"` + Timestamp int64 `json:"timestamp"` + TimestampMs int64 `json:"timestamp_ms"` + Time string `json:"time"` + Jobs []fioJob `json:"jobs"` +} + +// Job represents a single job's results within the FIO report. +type fioJob struct { + Jobname string `json:"jobname"` + Groupid int `json:"groupid"` + JobStart int64 `json:"job_start"` + Error int `json:"error"` + Eta int `json:"eta"` + Elapsed int `json:"elapsed"` + Read fioIOStats `json:"read"` + Write fioIOStats `json:"write"` + Trim fioIOStats `json:"trim"` + JobRuntime int `json:"job_runtime"` + UsrCPU float64 `json:"usr_cpu"` + SysCPU float64 `json:"sys_cpu"` + Ctx int `json:"ctx"` + Majf int `json:"majf"` + Minf int `json:"minf"` + IodepthLevel map[string]float64 `json:"iodepth_level"` + IodepthSubmit map[string]float64 `json:"iodepth_submit"` + IodepthComplete map[string]float64 `json:"iodepth_complete"` + LatencyNs map[string]float64 `json:"latency_ns"` + LatencyUs map[string]float64 `json:"latency_us"` + LatencyMs map[string]float64 `json:"latency_ms"` + LatencyDepth int `json:"latency_depth"` + LatencyTarget int `json:"latency_target"` + LatencyPercentile float64 `json:"latency_percentile"` + LatencyWindow int `json:"latency_window"` +} + +// IOStats holds the detailed I/O statistics for read, write, or trim operations. +type fioIOStats struct { + IoBytes int64 `json:"io_bytes"` + IoKbytes int64 `json:"io_kbytes"` + BwBytes int64 `json:"bw_bytes"` + Bw int64 `json:"bw"` + Iops float64 `json:"iops"` + Runtime int `json:"runtime"` + TotalIos int `json:"total_ios"` + ShortIos int `json:"short_ios"` + DropIos int `json:"drop_ios"` + SlatNs fioLatencyStats `json:"slat_ns"` + ClatNs fioLatencyStatsPercentiles `json:"clat_ns"` + LatNs fioLatencyStats `json:"lat_ns"` + BwMin int `json:"bw_min"` + BwMax int `json:"bw_max"` + BwAgg float64 `json:"bw_agg"` + BwMean float64 `json:"bw_mean"` + BwDev float64 `json:"bw_dev"` + BwSamples int `json:"bw_samples"` + IopsMin int `json:"iops_min"` + IopsMax int `json:"iops_max"` + IopsMean float64 `json:"iops_mean"` + IopsStddev float64 `json:"iops_stddev"` + IopsSamples int `json:"iops_samples"` +} + +// fioLatencyStats holds basic latency metrics. +type fioLatencyStats struct { + Min int64 `json:"min"` + Max int64 `json:"max"` + Mean float64 `json:"mean"` + Stddev float64 `json:"stddev"` + N int `json:"N"` +} + +// LatencyStatsPercentiles holds latency metrics including percentiles. +type fioLatencyStatsPercentiles struct { + Min int64 `json:"min"` + Max int64 `json:"max"` + Mean float64 `json:"mean"` + Stddev float64 `json:"stddev"` + N int `json:"N"` + Percentile map[string]int64 `json:"percentile"` +} + +func cpuSpeedFromOutput(outputs map[string]script.ScriptOutput) string { + var vals []float64 + for line := range strings.SplitSeq(strings.TrimSpace(outputs[script.SpeedBenchmarkScriptName].Stdout), "\n") { + tokens := strings.Split(line, " ") + if len(tokens) != 2 { + slog.Error("unexpected stress-ng output format", slog.String("line", line)) + return "" + } + fv, err := strconv.ParseFloat(tokens[1], 64) + if err != nil { + slog.Error("unexpected value in 2nd token (%s), expected float in line: %s", slog.String("token", tokens[1]), slog.String("line", line)) + return "" + } + vals = append(vals, fv) + } + if len(vals) == 0 { + slog.Warn("no values detected in stress-ng output") + return "" + } + return fmt.Sprintf("%.0f", util.GeoMean(vals)) +} + +func storagePerfFromOutput(outputs map[string]script.ScriptOutput) (fioOutput, error) { + output := outputs[script.StorageBenchmarkScriptName].Stdout + if output == "" { + return fioOutput{}, fmt.Errorf("no output from storage benchmark") + } + if strings.Contains(output, "ERROR:") { + return fioOutput{}, fmt.Errorf("failed to run storage benchmark: %s", output) + } + i := strings.Index(output, "{\n \"fio version\"") + if i >= 0 { + output = output[i:] + } else { + outputLen := min(len(output), 100) + slog.Info("fio output snip", "output", output[:outputLen], "stderr", outputs[script.StorageBenchmarkScriptName].Stderr) + return fioOutput{}, fmt.Errorf("unable to find fio output") + } + var fioData fioOutput + if err := json.Unmarshal([]byte(output), &fioData); err != nil { + return fioOutput{}, fmt.Errorf("error unmarshalling JSON: %w", err) + } + if len(fioData.Jobs) == 0 { + return fioOutput{}, fmt.Errorf("no jobs found in storage benchmark output") + } + return fioData, nil +} + +// avxTurboFrequenciesFromOutput parses the output of avx-turbo and returns the turbo frequencies as a map of instruction type to frequencies +// Sample avx-turbo output +// ... +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 1 | scalar_iadd | Scalar integer adds | 1.000 | 3901 | 1.95 | 3900 | 1.00 +// 1 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 +// 1 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 2 | scalar_iadd | Scalar integer adds | 1.000 | 3901, 3901 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// 2 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// 2 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 3 | scalar_iadd | Scalar integer adds | 1.000 | 3900, 3901, 3901 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// 3 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 975, 975 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// 3 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 975, 974 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// ... +func avxTurboFrequenciesFromOutput(output string) (instructionFreqs map[string][]float64, err error) { + instructionFreqs = make(map[string][]float64) + started := false + for line := range strings.SplitSeq(output, "\n") { + if strings.HasPrefix(line, "Cores | ID") { + started = true + continue + } + if !started { + continue + } + if line == "" { + started = false + continue + } + fields := strings.Split(line, "|") + if len(fields) < 7 { + err = fmt.Errorf("avx-turbo unable to measure frequencies") + return + } + freqs := strings.Split(fields[6], ",") + var sumFreqs float64 + for _, freq := range freqs { + var f float64 + f, err = strconv.ParseFloat(strings.TrimSpace(freq), 64) + if err != nil { + return + } + sumFreqs += f + } + avgFreq := sumFreqs / float64(len(freqs)) + instructionType := strings.TrimSpace(fields[1]) + if _, ok := instructionFreqs[instructionType]; !ok { + instructionFreqs[instructionType] = []float64{} + } + instructionFreqs[instructionType] = append(instructionFreqs[instructionType], avgFreq/1000.0) + } + return +} diff --git a/cmd/report/report.go b/cmd/report/report.go index 27a26b3e..ae015cd9 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 fc3dceaa..cfa64ae8 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 @@ -464,77 +455,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}, } // @@ -1771,237 +1691,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" @@ -2121,99 +1810,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 584ed825..76b29fee 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ import ( "syscall" "time" + "perfspect/cmd/benchmark" "perfspect/cmd/config" "perfspect/cmd/flame" "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(flame.Cmd) From 6a03e8a90059293e9535a432cbf0b8c371af7011 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:34:39 -0800 Subject: [PATCH 02/24] Add system summary table to benchmark command - Add --no-summary flag to optionally exclude system summary - Include Brief System Summary table by default (like telemetry command) - System summary provides quick overview of target system configuration - Follows same pattern as telemetry command for consistency The system summary table shows key system information including: - Host name, time, CPU model, microarchitecture, TDP - Sockets, cores, hyperthreading, CPUs, NUMA nodes - Scaling driver/governor, C-states, frequencies - Energy settings, memory, NIC, disk, OS, kernel --- cmd/benchmark/benchmark.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go index 5da5bcfb..45a4fd43 100644 --- a/cmd/benchmark/benchmark.go +++ b/cmd/benchmark/benchmark.go @@ -56,6 +56,8 @@ var ( flagNuma bool flagStorage bool + flagNoSystemSummary bool + flagStorageDir string ) @@ -71,6 +73,8 @@ const ( flagNumaName = "numa" flagStorageName = "storage" + flagNoSystemSummaryName = "no-summary" + flagStorageDirName = "storage-dir" ) @@ -95,6 +99,7 @@ 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().BoolVar(&flagNoSystemSummary, flagNoSystemSummaryName, false, "") Cmd.Flags().StringVar(&flagStorageDir, flagStorageDirName, "/tmp", "") common.AddTargetFlags(Cmd) @@ -146,6 +151,10 @@ func getFlagGroups() []common.FlagGroup { 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", @@ -210,7 +219,11 @@ func validateFlags(cmd *cobra.Command, args []string) error { } func runCmd(cmd *cobra.Command, args []string) error { - tables := []table.TableDefinition{} + 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 benchmarks { From ee21fc8f9711ff214e0e1194f59e91b733763e44 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:42:58 -0800 Subject: [PATCH 03/24] remove duplicate Signed-off-by: Harper, Jason M --- cmd/report/benchmarking.go | 203 ------------------------------------- 1 file changed, 203 deletions(-) delete mode 100644 cmd/report/benchmarking.go diff --git a/cmd/report/benchmarking.go b/cmd/report/benchmarking.go deleted file mode 100644 index 10a8327e..00000000 --- a/cmd/report/benchmarking.go +++ /dev/null @@ -1,203 +0,0 @@ -package report - -// Copyright (C) 2021-2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause - -import ( - "encoding/json" - "fmt" - "log/slog" - "perfspect/internal/script" - "perfspect/internal/util" - "strconv" - "strings" -) - -// fioOutput is the top-level struct for the FIO JSON report. -// ref: https://fio.readthedocs.io/en/latest/fio_doc.html#json-output -type fioOutput struct { - FioVersion string `json:"fio version"` - Timestamp int64 `json:"timestamp"` - TimestampMs int64 `json:"timestamp_ms"` - Time string `json:"time"` - Jobs []fioJob `json:"jobs"` -} - -// Job represents a single job's results within the FIO report. -type fioJob struct { - Jobname string `json:"jobname"` - Groupid int `json:"groupid"` - JobStart int64 `json:"job_start"` - Error int `json:"error"` - Eta int `json:"eta"` - Elapsed int `json:"elapsed"` - Read fioIOStats `json:"read"` - Write fioIOStats `json:"write"` - Trim fioIOStats `json:"trim"` - JobRuntime int `json:"job_runtime"` - UsrCPU float64 `json:"usr_cpu"` - SysCPU float64 `json:"sys_cpu"` - Ctx int `json:"ctx"` - Majf int `json:"majf"` - Minf int `json:"minf"` - IodepthLevel map[string]float64 `json:"iodepth_level"` - IodepthSubmit map[string]float64 `json:"iodepth_submit"` - IodepthComplete map[string]float64 `json:"iodepth_complete"` - LatencyNs map[string]float64 `json:"latency_ns"` - LatencyUs map[string]float64 `json:"latency_us"` - LatencyMs map[string]float64 `json:"latency_ms"` - LatencyDepth int `json:"latency_depth"` - LatencyTarget int `json:"latency_target"` - LatencyPercentile float64 `json:"latency_percentile"` - LatencyWindow int `json:"latency_window"` -} - -// IOStats holds the detailed I/O statistics for read, write, or trim operations. -type fioIOStats struct { - IoBytes int64 `json:"io_bytes"` - IoKbytes int64 `json:"io_kbytes"` - BwBytes int64 `json:"bw_bytes"` - Bw int64 `json:"bw"` - Iops float64 `json:"iops"` - Runtime int `json:"runtime"` - TotalIos int `json:"total_ios"` - ShortIos int `json:"short_ios"` - DropIos int `json:"drop_ios"` - SlatNs fioLatencyStats `json:"slat_ns"` - ClatNs fioLatencyStatsPercentiles `json:"clat_ns"` - LatNs fioLatencyStats `json:"lat_ns"` - BwMin int `json:"bw_min"` - BwMax int `json:"bw_max"` - BwAgg float64 `json:"bw_agg"` - BwMean float64 `json:"bw_mean"` - BwDev float64 `json:"bw_dev"` - BwSamples int `json:"bw_samples"` - IopsMin int `json:"iops_min"` - IopsMax int `json:"iops_max"` - IopsMean float64 `json:"iops_mean"` - IopsStddev float64 `json:"iops_stddev"` - IopsSamples int `json:"iops_samples"` -} - -// fioLatencyStats holds basic latency metrics. -type fioLatencyStats struct { - Min int64 `json:"min"` - Max int64 `json:"max"` - Mean float64 `json:"mean"` - Stddev float64 `json:"stddev"` - N int `json:"N"` -} - -// LatencyStatsPercentiles holds latency metrics including percentiles. -type fioLatencyStatsPercentiles struct { - Min int64 `json:"min"` - Max int64 `json:"max"` - Mean float64 `json:"mean"` - Stddev float64 `json:"stddev"` - N int `json:"N"` - Percentile map[string]int64 `json:"percentile"` -} - -func cpuSpeedFromOutput(outputs map[string]script.ScriptOutput) string { - var vals []float64 - for line := range strings.SplitSeq(strings.TrimSpace(outputs[script.SpeedBenchmarkScriptName].Stdout), "\n") { - tokens := strings.Split(line, " ") - if len(tokens) != 2 { - slog.Error("unexpected stress-ng output format", slog.String("line", line)) - return "" - } - fv, err := strconv.ParseFloat(tokens[1], 64) - if err != nil { - slog.Error("unexpected value in 2nd token (%s), expected float in line: %s", slog.String("token", tokens[1]), slog.String("line", line)) - return "" - } - vals = append(vals, fv) - } - if len(vals) == 0 { - slog.Warn("no values detected in stress-ng output") - return "" - } - return fmt.Sprintf("%.0f", util.GeoMean(vals)) -} - -func storagePerfFromOutput(outputs map[string]script.ScriptOutput) (fioOutput, error) { - output := outputs[script.StorageBenchmarkScriptName].Stdout - if output == "" { - return fioOutput{}, fmt.Errorf("no output from storage benchmark") - } - if strings.Contains(output, "ERROR:") { - return fioOutput{}, fmt.Errorf("failed to run storage benchmark: %s", output) - } - i := strings.Index(output, "{\n \"fio version\"") - if i >= 0 { - output = output[i:] - } else { - outputLen := min(len(output), 100) - slog.Info("fio output snip", "output", output[:outputLen], "stderr", outputs[script.StorageBenchmarkScriptName].Stderr) - return fioOutput{}, fmt.Errorf("unable to find fio output") - } - var fioData fioOutput - if err := json.Unmarshal([]byte(output), &fioData); err != nil { - return fioOutput{}, fmt.Errorf("error unmarshalling JSON: %w", err) - } - if len(fioData.Jobs) == 0 { - return fioOutput{}, fmt.Errorf("no jobs found in storage benchmark output") - } - return fioData, nil -} - -// avxTurboFrequenciesFromOutput parses the output of avx-turbo and returns the turbo frequencies as a map of instruction type to frequencies -// Sample avx-turbo output -// ... -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 1 | scalar_iadd | Scalar integer adds | 1.000 | 3901 | 1.95 | 3900 | 1.00 -// 1 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 -// 1 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 2 | scalar_iadd | Scalar integer adds | 1.000 | 3901, 3901 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// 2 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// 2 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 3 | scalar_iadd | Scalar integer adds | 1.000 | 3900, 3901, 3901 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// 3 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 975, 975 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// 3 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 975, 974 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// ... -func avxTurboFrequenciesFromOutput(output string) (instructionFreqs map[string][]float64, err error) { - instructionFreqs = make(map[string][]float64) - started := false - for line := range strings.SplitSeq(output, "\n") { - if strings.HasPrefix(line, "Cores | ID") { - started = true - continue - } - if !started { - continue - } - if line == "" { - started = false - continue - } - fields := strings.Split(line, "|") - if len(fields) < 7 { - err = fmt.Errorf("avx-turbo unable to measure frequencies") - return - } - freqs := strings.Split(fields[6], ",") - var sumFreqs float64 - for _, freq := range freqs { - var f float64 - f, err = strconv.ParseFloat(strings.TrimSpace(freq), 64) - if err != nil { - return - } - sumFreqs += f - } - avgFreq := sumFreqs / float64(len(freqs)) - instructionType := strings.TrimSpace(fields[1]) - if _, ok := instructionFreqs[instructionType]; !ok { - instructionFreqs[instructionType] = []float64{} - } - instructionFreqs[instructionType] = append(instructionFreqs[instructionType], avgFreq/1000.0) - } - return -} From a9090901cfa9faa4ea8ac8a250918fd45e203e23 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:43:06 -0800 Subject: [PATCH 04/24] formatting Signed-off-by: Harper, Jason M --- cmd/report/report_tables.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/report/report_tables.go b/cmd/report/report_tables.go index cfa64ae8..79a51c08 100644 --- a/cmd/report/report_tables.go +++ b/cmd/report/report_tables.go @@ -1809,4 +1809,3 @@ func dimmTableHTMLRenderer(tableValues table.TableValues, targetName string) str } return report.RenderHTMLTable(socketTableHeaders, socketTableValues, "pure-table pure-table-bordered", [][]string{}) } - From bdb652f9ea86a9eba9889539ff9dac0e420f94b3 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:49:46 -0800 Subject: [PATCH 05/24] Update documentation for new benchmark command - Add benchmark command to commands table in README.md - Create new Benchmark Command section with detailed descriptions - Remove benchmark subsection from Report Command documentation - Update copilot-instructions.md to include benchmark command - Document --no-summary flag for excluding system summary The benchmark command is now properly documented as a standalone command rather than a flag within the report command. --- .github/copilot-instructions.md | 3 ++- README.md | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 96fc9e8b..417d4686 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 - `flame`: Creates CPU flamegraphs - `lock`: Analyzes lock contention diff --git a/README.md b/README.md index 7e312dfd..1a23527d 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 | | [`flame`](#flame-command) | Software call-stacks as flamegraphs | | [`lock`](#lock-command) | Software hot spot, cache-to-cache and lock contention | @@ -87,15 +88,24 @@ 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. | From 594cbba0240ca541b1f4d636e9747bd773dbf6f0 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:56:23 -0800 Subject: [PATCH 06/24] Add alias 'bench' to benchmark command Signed-off-by: Harper, Jason M --- cmd/benchmark/benchmark.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go index 45a4fd43..c23050e0 100644 --- a/cmd/benchmark/benchmark.go +++ b/cmd/benchmark/benchmark.go @@ -35,6 +35,7 @@ var examples = []string{ var Cmd = &cobra.Command{ Use: cmdName, + Aliases: []string{"bench"}, Short: "Run performance benchmarks on target(s)", Example: strings.Join(examples, "\n"), RunE: runCmd, From 45869dec919329293b77bff343b302e59131b9af Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:25:54 -0800 Subject: [PATCH 07/24] Extract benchmark functionality into standalone command - Create new 'benchmark' command under cmd/benchmark/ - Implement benchmark flags following telemetry command pattern: * --all (default) - run all benchmarks * --speed, --power, --temperature, --frequency, --memory, --numa, --storage - Move benchmark table definitions and processing functions to new command - Move benchmark HTML renderers to new command - Remove benchmark functionality from report command: * Remove --benchmark flag * Remove benchmark table definitions * Remove benchmark data processing functions * Clean up unused imports - Register benchmark command in root command The benchmark command now provides a cleaner separation of concerns, following the same flag pattern as the telemetry command. Usage: perfspect benchmark # Runs all benchmarks perfspect benchmark --speed --power # Runs specific benchmarks perfspect benchmark --target # Benchmark remote target --- cmd/benchmark/benchmark.go | 426 +++++++++++++++++++++++++++ cmd/benchmark/benchmark_renderers.go | 111 +++++++ cmd/benchmark/benchmark_tables.go | 345 ++++++++++++++++++++++ cmd/benchmark/benchmarking.go | 203 +++++++++++++ cmd/report/report.go | 272 ----------------- cmd/report/report_tables.go | 407 ------------------------- cmd/root.go | 2 + 7 files changed, 1087 insertions(+), 679 deletions(-) create mode 100644 cmd/benchmark/benchmark.go create mode 100644 cmd/benchmark/benchmark_renderers.go create mode 100644 cmd/benchmark/benchmark_tables.go create mode 100644 cmd/benchmark/benchmarking.go diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go new file mode 100644 index 00000000..5da5bcfb --- /dev/null +++ b/cmd/benchmark/benchmark.go @@ -0,0 +1,426 @@ +// 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, + 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 + + flagStorageDir string +) + +// flag names +const ( + flagAllName = "all" + + flagSpeedName = "speed" + flagPowerName = "power" + flagTemperatureName = "temperature" + flagFrequencyName = "frequency" + flagMemoryName = "memory" + flagNumaName = "numa" + flagStorageName = "storage" + + flagStorageDirName = "storage-dir" +) + +var benchmarkSummaryTableName = "Benchmark Summary" + +var benchmarks = []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 benchmarks { + 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().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 benchmarks { + 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: 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 benchmarks { + 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 { + tables := []table.TableDefinition{} + // add benchmark tables + selectedBenchmarkCount := 0 + for _, benchmark := range benchmarks { + 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(benchmarks) { + summaryFunc = benchmarkSummaryFromTableValues + } + + reportingCommand := common.ReportingCommand{ + Cmd: cmd, + ScriptParams: map[string]string{"StorageDir": flagStorageDir}, + Tables: tables, + SummaryFunc: summaryFunc, + SummaryTableName: benchmarkSummaryTableName, + SummaryBeforeTableName: SpeedBenchmarkTableName, + InsightsFunc: nil, + SystemSummaryTableName: SystemSummaryTableName, + } + + 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/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..eb29dd6b --- /dev/null +++ b/cmd/benchmark/benchmark_tables.go @@ -0,0 +1,345 @@ +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" + SystemSummaryTableName = "System Summary" +) + +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}, + SystemSummaryTableName: { + Name: SystemSummaryTableName, + MenuLabel: SystemSummaryTableName, + HasRows: false, + ScriptNames: []string{ + script.LscpuScriptName, + script.UnameScriptName, + }, + FieldsFunc: systemSummaryTableValues}, +} + +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 systemSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Field { + lscpuOutput := outputs[script.LscpuScriptName].Stdout + return []table.Field{ + {Name: "Microarchitecture", Values: []string{common.UarchFromOutput(outputs)}}, + {Name: "Sockets", Values: []string{common.ValFromRegexSubmatch(lscpuOutput, `^Socket\(s\):\s*(.+)$`)}}, + } +} diff --git a/cmd/benchmark/benchmarking.go b/cmd/benchmark/benchmarking.go new file mode 100644 index 00000000..483d840c --- /dev/null +++ b/cmd/benchmark/benchmarking.go @@ -0,0 +1,203 @@ +package benchmark + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "encoding/json" + "fmt" + "log/slog" + "perfspect/internal/script" + "perfspect/internal/util" + "strconv" + "strings" +) + +// fioOutput is the top-level struct for the FIO JSON report. +// ref: https://fio.readthedocs.io/en/latest/fio_doc.html#json-output +type fioOutput struct { + FioVersion string `json:"fio version"` + Timestamp int64 `json:"timestamp"` + TimestampMs int64 `json:"timestamp_ms"` + Time string `json:"time"` + Jobs []fioJob `json:"jobs"` +} + +// Job represents a single job's results within the FIO report. +type fioJob struct { + Jobname string `json:"jobname"` + Groupid int `json:"groupid"` + JobStart int64 `json:"job_start"` + Error int `json:"error"` + Eta int `json:"eta"` + Elapsed int `json:"elapsed"` + Read fioIOStats `json:"read"` + Write fioIOStats `json:"write"` + Trim fioIOStats `json:"trim"` + JobRuntime int `json:"job_runtime"` + UsrCPU float64 `json:"usr_cpu"` + SysCPU float64 `json:"sys_cpu"` + Ctx int `json:"ctx"` + Majf int `json:"majf"` + Minf int `json:"minf"` + IodepthLevel map[string]float64 `json:"iodepth_level"` + IodepthSubmit map[string]float64 `json:"iodepth_submit"` + IodepthComplete map[string]float64 `json:"iodepth_complete"` + LatencyNs map[string]float64 `json:"latency_ns"` + LatencyUs map[string]float64 `json:"latency_us"` + LatencyMs map[string]float64 `json:"latency_ms"` + LatencyDepth int `json:"latency_depth"` + LatencyTarget int `json:"latency_target"` + LatencyPercentile float64 `json:"latency_percentile"` + LatencyWindow int `json:"latency_window"` +} + +// IOStats holds the detailed I/O statistics for read, write, or trim operations. +type fioIOStats struct { + IoBytes int64 `json:"io_bytes"` + IoKbytes int64 `json:"io_kbytes"` + BwBytes int64 `json:"bw_bytes"` + Bw int64 `json:"bw"` + Iops float64 `json:"iops"` + Runtime int `json:"runtime"` + TotalIos int `json:"total_ios"` + ShortIos int `json:"short_ios"` + DropIos int `json:"drop_ios"` + SlatNs fioLatencyStats `json:"slat_ns"` + ClatNs fioLatencyStatsPercentiles `json:"clat_ns"` + LatNs fioLatencyStats `json:"lat_ns"` + BwMin int `json:"bw_min"` + BwMax int `json:"bw_max"` + BwAgg float64 `json:"bw_agg"` + BwMean float64 `json:"bw_mean"` + BwDev float64 `json:"bw_dev"` + BwSamples int `json:"bw_samples"` + IopsMin int `json:"iops_min"` + IopsMax int `json:"iops_max"` + IopsMean float64 `json:"iops_mean"` + IopsStddev float64 `json:"iops_stddev"` + IopsSamples int `json:"iops_samples"` +} + +// fioLatencyStats holds basic latency metrics. +type fioLatencyStats struct { + Min int64 `json:"min"` + Max int64 `json:"max"` + Mean float64 `json:"mean"` + Stddev float64 `json:"stddev"` + N int `json:"N"` +} + +// LatencyStatsPercentiles holds latency metrics including percentiles. +type fioLatencyStatsPercentiles struct { + Min int64 `json:"min"` + Max int64 `json:"max"` + Mean float64 `json:"mean"` + Stddev float64 `json:"stddev"` + N int `json:"N"` + Percentile map[string]int64 `json:"percentile"` +} + +func cpuSpeedFromOutput(outputs map[string]script.ScriptOutput) string { + var vals []float64 + for line := range strings.SplitSeq(strings.TrimSpace(outputs[script.SpeedBenchmarkScriptName].Stdout), "\n") { + tokens := strings.Split(line, " ") + if len(tokens) != 2 { + slog.Error("unexpected stress-ng output format", slog.String("line", line)) + return "" + } + fv, err := strconv.ParseFloat(tokens[1], 64) + if err != nil { + slog.Error("unexpected value in 2nd token (%s), expected float in line: %s", slog.String("token", tokens[1]), slog.String("line", line)) + return "" + } + vals = append(vals, fv) + } + if len(vals) == 0 { + slog.Warn("no values detected in stress-ng output") + return "" + } + return fmt.Sprintf("%.0f", util.GeoMean(vals)) +} + +func storagePerfFromOutput(outputs map[string]script.ScriptOutput) (fioOutput, error) { + output := outputs[script.StorageBenchmarkScriptName].Stdout + if output == "" { + return fioOutput{}, fmt.Errorf("no output from storage benchmark") + } + if strings.Contains(output, "ERROR:") { + return fioOutput{}, fmt.Errorf("failed to run storage benchmark: %s", output) + } + i := strings.Index(output, "{\n \"fio version\"") + if i >= 0 { + output = output[i:] + } else { + outputLen := min(len(output), 100) + slog.Info("fio output snip", "output", output[:outputLen], "stderr", outputs[script.StorageBenchmarkScriptName].Stderr) + return fioOutput{}, fmt.Errorf("unable to find fio output") + } + var fioData fioOutput + if err := json.Unmarshal([]byte(output), &fioData); err != nil { + return fioOutput{}, fmt.Errorf("error unmarshalling JSON: %w", err) + } + if len(fioData.Jobs) == 0 { + return fioOutput{}, fmt.Errorf("no jobs found in storage benchmark output") + } + return fioData, nil +} + +// avxTurboFrequenciesFromOutput parses the output of avx-turbo and returns the turbo frequencies as a map of instruction type to frequencies +// Sample avx-turbo output +// ... +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 1 | scalar_iadd | Scalar integer adds | 1.000 | 3901 | 1.95 | 3900 | 1.00 +// 1 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 +// 1 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 2 | scalar_iadd | Scalar integer adds | 1.000 | 3901, 3901 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// 2 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// 2 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 3 | scalar_iadd | Scalar integer adds | 1.000 | 3900, 3901, 3901 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// 3 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 975, 975 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// 3 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 975, 974 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// ... +func avxTurboFrequenciesFromOutput(output string) (instructionFreqs map[string][]float64, err error) { + instructionFreqs = make(map[string][]float64) + started := false + for line := range strings.SplitSeq(output, "\n") { + if strings.HasPrefix(line, "Cores | ID") { + started = true + continue + } + if !started { + continue + } + if line == "" { + started = false + continue + } + fields := strings.Split(line, "|") + if len(fields) < 7 { + err = fmt.Errorf("avx-turbo unable to measure frequencies") + return + } + freqs := strings.Split(fields[6], ",") + var sumFreqs float64 + for _, freq := range freqs { + var f float64 + f, err = strconv.ParseFloat(strings.TrimSpace(freq), 64) + if err != nil { + return + } + sumFreqs += f + } + avgFreq := sumFreqs / float64(len(freqs)) + instructionType := strings.TrimSpace(fields[1]) + if _, ok := instructionFreqs[instructionType]; !ok { + instructionFreqs[instructionType] = []float64{} + } + instructionFreqs[instructionType] = append(instructionFreqs[instructionType], avgFreq/1000.0) + } + return +} 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 fc3dceaa..cfa64ae8 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 @@ -464,77 +455,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}, } // @@ -1771,237 +1691,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" @@ -2121,99 +1810,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) From 662db75815eb73c58329d42b0340945e2d898491 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:34:39 -0800 Subject: [PATCH 08/24] Add system summary table to benchmark command - Add --no-summary flag to optionally exclude system summary - Include Brief System Summary table by default (like telemetry command) - System summary provides quick overview of target system configuration - Follows same pattern as telemetry command for consistency The system summary table shows key system information including: - Host name, time, CPU model, microarchitecture, TDP - Sockets, cores, hyperthreading, CPUs, NUMA nodes - Scaling driver/governor, C-states, frequencies - Energy settings, memory, NIC, disk, OS, kernel --- cmd/benchmark/benchmark.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go index 5da5bcfb..45a4fd43 100644 --- a/cmd/benchmark/benchmark.go +++ b/cmd/benchmark/benchmark.go @@ -56,6 +56,8 @@ var ( flagNuma bool flagStorage bool + flagNoSystemSummary bool + flagStorageDir string ) @@ -71,6 +73,8 @@ const ( flagNumaName = "numa" flagStorageName = "storage" + flagNoSystemSummaryName = "no-summary" + flagStorageDirName = "storage-dir" ) @@ -95,6 +99,7 @@ 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().BoolVar(&flagNoSystemSummary, flagNoSystemSummaryName, false, "") Cmd.Flags().StringVar(&flagStorageDir, flagStorageDirName, "/tmp", "") common.AddTargetFlags(Cmd) @@ -146,6 +151,10 @@ func getFlagGroups() []common.FlagGroup { 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", @@ -210,7 +219,11 @@ func validateFlags(cmd *cobra.Command, args []string) error { } func runCmd(cmd *cobra.Command, args []string) error { - tables := []table.TableDefinition{} + 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 benchmarks { From 4d8c4696f366749bc89e456949f0470ae91cb14e Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:42:58 -0800 Subject: [PATCH 09/24] remove duplicate Signed-off-by: Harper, Jason M --- cmd/report/benchmarking.go | 203 ------------------------------------- 1 file changed, 203 deletions(-) delete mode 100644 cmd/report/benchmarking.go diff --git a/cmd/report/benchmarking.go b/cmd/report/benchmarking.go deleted file mode 100644 index 10a8327e..00000000 --- a/cmd/report/benchmarking.go +++ /dev/null @@ -1,203 +0,0 @@ -package report - -// Copyright (C) 2021-2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause - -import ( - "encoding/json" - "fmt" - "log/slog" - "perfspect/internal/script" - "perfspect/internal/util" - "strconv" - "strings" -) - -// fioOutput is the top-level struct for the FIO JSON report. -// ref: https://fio.readthedocs.io/en/latest/fio_doc.html#json-output -type fioOutput struct { - FioVersion string `json:"fio version"` - Timestamp int64 `json:"timestamp"` - TimestampMs int64 `json:"timestamp_ms"` - Time string `json:"time"` - Jobs []fioJob `json:"jobs"` -} - -// Job represents a single job's results within the FIO report. -type fioJob struct { - Jobname string `json:"jobname"` - Groupid int `json:"groupid"` - JobStart int64 `json:"job_start"` - Error int `json:"error"` - Eta int `json:"eta"` - Elapsed int `json:"elapsed"` - Read fioIOStats `json:"read"` - Write fioIOStats `json:"write"` - Trim fioIOStats `json:"trim"` - JobRuntime int `json:"job_runtime"` - UsrCPU float64 `json:"usr_cpu"` - SysCPU float64 `json:"sys_cpu"` - Ctx int `json:"ctx"` - Majf int `json:"majf"` - Minf int `json:"minf"` - IodepthLevel map[string]float64 `json:"iodepth_level"` - IodepthSubmit map[string]float64 `json:"iodepth_submit"` - IodepthComplete map[string]float64 `json:"iodepth_complete"` - LatencyNs map[string]float64 `json:"latency_ns"` - LatencyUs map[string]float64 `json:"latency_us"` - LatencyMs map[string]float64 `json:"latency_ms"` - LatencyDepth int `json:"latency_depth"` - LatencyTarget int `json:"latency_target"` - LatencyPercentile float64 `json:"latency_percentile"` - LatencyWindow int `json:"latency_window"` -} - -// IOStats holds the detailed I/O statistics for read, write, or trim operations. -type fioIOStats struct { - IoBytes int64 `json:"io_bytes"` - IoKbytes int64 `json:"io_kbytes"` - BwBytes int64 `json:"bw_bytes"` - Bw int64 `json:"bw"` - Iops float64 `json:"iops"` - Runtime int `json:"runtime"` - TotalIos int `json:"total_ios"` - ShortIos int `json:"short_ios"` - DropIos int `json:"drop_ios"` - SlatNs fioLatencyStats `json:"slat_ns"` - ClatNs fioLatencyStatsPercentiles `json:"clat_ns"` - LatNs fioLatencyStats `json:"lat_ns"` - BwMin int `json:"bw_min"` - BwMax int `json:"bw_max"` - BwAgg float64 `json:"bw_agg"` - BwMean float64 `json:"bw_mean"` - BwDev float64 `json:"bw_dev"` - BwSamples int `json:"bw_samples"` - IopsMin int `json:"iops_min"` - IopsMax int `json:"iops_max"` - IopsMean float64 `json:"iops_mean"` - IopsStddev float64 `json:"iops_stddev"` - IopsSamples int `json:"iops_samples"` -} - -// fioLatencyStats holds basic latency metrics. -type fioLatencyStats struct { - Min int64 `json:"min"` - Max int64 `json:"max"` - Mean float64 `json:"mean"` - Stddev float64 `json:"stddev"` - N int `json:"N"` -} - -// LatencyStatsPercentiles holds latency metrics including percentiles. -type fioLatencyStatsPercentiles struct { - Min int64 `json:"min"` - Max int64 `json:"max"` - Mean float64 `json:"mean"` - Stddev float64 `json:"stddev"` - N int `json:"N"` - Percentile map[string]int64 `json:"percentile"` -} - -func cpuSpeedFromOutput(outputs map[string]script.ScriptOutput) string { - var vals []float64 - for line := range strings.SplitSeq(strings.TrimSpace(outputs[script.SpeedBenchmarkScriptName].Stdout), "\n") { - tokens := strings.Split(line, " ") - if len(tokens) != 2 { - slog.Error("unexpected stress-ng output format", slog.String("line", line)) - return "" - } - fv, err := strconv.ParseFloat(tokens[1], 64) - if err != nil { - slog.Error("unexpected value in 2nd token (%s), expected float in line: %s", slog.String("token", tokens[1]), slog.String("line", line)) - return "" - } - vals = append(vals, fv) - } - if len(vals) == 0 { - slog.Warn("no values detected in stress-ng output") - return "" - } - return fmt.Sprintf("%.0f", util.GeoMean(vals)) -} - -func storagePerfFromOutput(outputs map[string]script.ScriptOutput) (fioOutput, error) { - output := outputs[script.StorageBenchmarkScriptName].Stdout - if output == "" { - return fioOutput{}, fmt.Errorf("no output from storage benchmark") - } - if strings.Contains(output, "ERROR:") { - return fioOutput{}, fmt.Errorf("failed to run storage benchmark: %s", output) - } - i := strings.Index(output, "{\n \"fio version\"") - if i >= 0 { - output = output[i:] - } else { - outputLen := min(len(output), 100) - slog.Info("fio output snip", "output", output[:outputLen], "stderr", outputs[script.StorageBenchmarkScriptName].Stderr) - return fioOutput{}, fmt.Errorf("unable to find fio output") - } - var fioData fioOutput - if err := json.Unmarshal([]byte(output), &fioData); err != nil { - return fioOutput{}, fmt.Errorf("error unmarshalling JSON: %w", err) - } - if len(fioData.Jobs) == 0 { - return fioOutput{}, fmt.Errorf("no jobs found in storage benchmark output") - } - return fioData, nil -} - -// avxTurboFrequenciesFromOutput parses the output of avx-turbo and returns the turbo frequencies as a map of instruction type to frequencies -// Sample avx-turbo output -// ... -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 1 | scalar_iadd | Scalar integer adds | 1.000 | 3901 | 1.95 | 3900 | 1.00 -// 1 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 -// 1 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 2 | scalar_iadd | Scalar integer adds | 1.000 | 3901, 3901 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// 2 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// 2 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 3 | scalar_iadd | Scalar integer adds | 1.000 | 3900, 3901, 3901 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// 3 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 975, 975 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// 3 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 975, 974 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// ... -func avxTurboFrequenciesFromOutput(output string) (instructionFreqs map[string][]float64, err error) { - instructionFreqs = make(map[string][]float64) - started := false - for line := range strings.SplitSeq(output, "\n") { - if strings.HasPrefix(line, "Cores | ID") { - started = true - continue - } - if !started { - continue - } - if line == "" { - started = false - continue - } - fields := strings.Split(line, "|") - if len(fields) < 7 { - err = fmt.Errorf("avx-turbo unable to measure frequencies") - return - } - freqs := strings.Split(fields[6], ",") - var sumFreqs float64 - for _, freq := range freqs { - var f float64 - f, err = strconv.ParseFloat(strings.TrimSpace(freq), 64) - if err != nil { - return - } - sumFreqs += f - } - avgFreq := sumFreqs / float64(len(freqs)) - instructionType := strings.TrimSpace(fields[1]) - if _, ok := instructionFreqs[instructionType]; !ok { - instructionFreqs[instructionType] = []float64{} - } - instructionFreqs[instructionType] = append(instructionFreqs[instructionType], avgFreq/1000.0) - } - return -} From 0fa0436da162b5df20d97934e0fbd90473f91d11 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:43:06 -0800 Subject: [PATCH 10/24] formatting Signed-off-by: Harper, Jason M --- cmd/report/report_tables.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/report/report_tables.go b/cmd/report/report_tables.go index cfa64ae8..79a51c08 100644 --- a/cmd/report/report_tables.go +++ b/cmd/report/report_tables.go @@ -1809,4 +1809,3 @@ func dimmTableHTMLRenderer(tableValues table.TableValues, targetName string) str } return report.RenderHTMLTable(socketTableHeaders, socketTableValues, "pure-table pure-table-bordered", [][]string{}) } - From 43b9891f17c6159007e9d9d796bc68092c7241a1 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:49:46 -0800 Subject: [PATCH 11/24] Update documentation for new benchmark command - Add benchmark command to commands table in README.md - Create new Benchmark Command section with detailed descriptions - Remove benchmark subsection from Report Command documentation - Update copilot-instructions.md to include benchmark command - Document --no-summary flag for excluding system summary The benchmark command is now properly documented as a standalone command rather than a flag within the report command. --- .github/copilot-instructions.md | 3 ++- README.md | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) 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..7f9bb30b 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,15 +88,24 @@ 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. | From e79e95b3dc23e987862ed9dffade07a6f4835609 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:56:23 -0800 Subject: [PATCH 12/24] Add alias 'bench' to benchmark command Signed-off-by: Harper, Jason M --- cmd/benchmark/benchmark.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go index 45a4fd43..c23050e0 100644 --- a/cmd/benchmark/benchmark.go +++ b/cmd/benchmark/benchmark.go @@ -35,6 +35,7 @@ var examples = []string{ var Cmd = &cobra.Command{ Use: cmdName, + Aliases: []string{"bench"}, Short: "Run performance benchmarks on target(s)", Example: strings.Join(examples, "\n"), RunE: runCmd, From 5cae9a2d3a47bbeaaaf970b1761fd46e32256280 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 05:19:57 -0800 Subject: [PATCH 13/24] Update storage command description to reflect new I/O patterns and disk space requirements Signed-off-by: Harper, Jason M --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f9bb30b..06284bb1 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ See `perfspect benchmark -h` for all options. | 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`). From 4f5d112c8c5f6d92c50c88f8bc1c5bf6d2504d29 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 12:34:26 -0800 Subject: [PATCH 14/24] ref data Signed-off-by: Harper, Jason M --- cmd/benchmark/benchmark.go | 17 ++++++++--------- cmd/benchmark/benchmark_tables.go | 18 ------------------ 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go index c23050e0..b299ec2e 100644 --- a/cmd/benchmark/benchmark.go +++ b/cmd/benchmark/benchmark.go @@ -81,7 +81,7 @@ const ( var benchmarkSummaryTableName = "Benchmark Summary" -var benchmarks = []common.Category{ +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]}}, @@ -93,7 +93,7 @@ var benchmarks = []common.Category{ func init() { // set up benchmark flags - for _, benchmark := range benchmarks { + for _, benchmark := range categories { Cmd.Flags().BoolVar(benchmark.FlagVar, benchmark.FlagName, benchmark.DefaultValue, benchmark.Help) } // set up other flags @@ -141,7 +141,7 @@ func getFlagGroups() []common.FlagGroup { Help: "run all benchmarks", }, } - for _, benchmark := range benchmarks { + for _, benchmark := range categories { flags = append(flags, common.Flag{ Name: benchmark.FlagName, Help: benchmark.Help, @@ -186,7 +186,7 @@ func getFlagGroups() []common.FlagGroup { func validateFlags(cmd *cobra.Command, args []string) error { // clear flagAll if any benchmarks are selected if flagAll { - for _, benchmark := range benchmarks { + for _, benchmark := range categories { if benchmark.FlagVar != nil && *benchmark.FlagVar { flagAll = false break @@ -227,7 +227,7 @@ func runCmd(cmd *cobra.Command, args []string) error { } // add benchmark tables selectedBenchmarkCount := 0 - for _, benchmark := range benchmarks { + for _, benchmark := range categories { if *benchmark.FlagVar || flagAll { tables = append(tables, benchmark.Tables...) selectedBenchmarkCount++ @@ -235,7 +235,7 @@ func runCmd(cmd *cobra.Command, args []string) error { } // include benchmark summary table if all benchmarks are selected var summaryFunc common.SummaryFunc - if selectedBenchmarkCount == len(benchmarks) { + if selectedBenchmarkCount == len(categories) { summaryFunc = benchmarkSummaryFromTableValues } @@ -247,7 +247,6 @@ func runCmd(cmd *cobra.Command, args []string) error { SummaryTableName: benchmarkSummaryTableName, SummaryBeforeTableName: SpeedBenchmarkTableName, InsightsFunc: nil, - SystemSummaryTableName: SystemSummaryTableName, } report.RegisterHTMLRenderer(FrequencyBenchmarkTableName, frequencyBenchmarkTableHtmlRenderer) @@ -313,8 +312,8 @@ func benchmarkSummaryFromTableValues(allTableValues []table.TableValues, outputs {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)}}, + {Name: "Microarchitecture", Values: []string{common.UarchFromOutput(outputs)}}, + {Name: "Sockets", Values: []string{common.ValFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Socket\(s\):\s*(.+)$`)}}, }, } } diff --git a/cmd/benchmark/benchmark_tables.go b/cmd/benchmark/benchmark_tables.go index eb29dd6b..51639d6a 100644 --- a/cmd/benchmark/benchmark_tables.go +++ b/cmd/benchmark/benchmark_tables.go @@ -25,7 +25,6 @@ const ( MemoryBenchmarkTableName = "Memory Benchmark" NUMABenchmarkTableName = "NUMA Benchmark" StorageBenchmarkTableName = "Storage Benchmark" - SystemSummaryTableName = "System Summary" ) var tableDefinitions = map[string]table.TableDefinition{ @@ -97,15 +96,6 @@ var tableDefinitions = map[string]table.TableDefinition{ script.StorageBenchmarkScriptName, }, FieldsFunc: storageBenchmarkTableValues}, - SystemSummaryTableName: { - Name: SystemSummaryTableName, - MenuLabel: SystemSummaryTableName, - HasRows: false, - ScriptNames: []string{ - script.LscpuScriptName, - script.UnameScriptName, - }, - FieldsFunc: systemSummaryTableValues}, } func speedBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { @@ -335,11 +325,3 @@ func storageBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table } return fields } - -func systemSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Field { - lscpuOutput := outputs[script.LscpuScriptName].Stdout - return []table.Field{ - {Name: "Microarchitecture", Values: []string{common.UarchFromOutput(outputs)}}, - {Name: "Sockets", Values: []string{common.ValFromRegexSubmatch(lscpuOutput, `^Socket\(s\):\s*(.+)$`)}}, - } -} From 55804ad3641f43617c88bc83e060b0d47e1699a1 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 12:58:10 -0800 Subject: [PATCH 15/24] Filter out anomalous high power and temperature readings in turbostat output parsing Signed-off-by: Harper, Jason M --- internal/common/turbostat.go | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 } From 122ee4dc94bd5643492299ce9b1852a868c8965d Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:25:54 -0800 Subject: [PATCH 16/24] Extract benchmark functionality into standalone command - Create new 'benchmark' command under cmd/benchmark/ - Implement benchmark flags following telemetry command pattern: * --all (default) - run all benchmarks * --speed, --power, --temperature, --frequency, --memory, --numa, --storage - Move benchmark table definitions and processing functions to new command - Move benchmark HTML renderers to new command - Remove benchmark functionality from report command: * Remove --benchmark flag * Remove benchmark table definitions * Remove benchmark data processing functions * Clean up unused imports - Register benchmark command in root command The benchmark command now provides a cleaner separation of concerns, following the same flag pattern as the telemetry command. Usage: perfspect benchmark # Runs all benchmarks perfspect benchmark --speed --power # Runs specific benchmarks perfspect benchmark --target # Benchmark remote target --- cmd/benchmark/benchmark.go | 426 +++++++++++++++++++++++++++ cmd/benchmark/benchmark_renderers.go | 111 +++++++ cmd/benchmark/benchmark_tables.go | 345 ++++++++++++++++++++++ cmd/benchmark/benchmarking.go | 203 +++++++++++++ cmd/report/report.go | 272 ----------------- cmd/report/report_tables.go | 407 ------------------------- cmd/root.go | 2 + 7 files changed, 1087 insertions(+), 679 deletions(-) create mode 100644 cmd/benchmark/benchmark.go create mode 100644 cmd/benchmark/benchmark_renderers.go create mode 100644 cmd/benchmark/benchmark_tables.go create mode 100644 cmd/benchmark/benchmarking.go diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go new file mode 100644 index 00000000..5da5bcfb --- /dev/null +++ b/cmd/benchmark/benchmark.go @@ -0,0 +1,426 @@ +// 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, + 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 + + flagStorageDir string +) + +// flag names +const ( + flagAllName = "all" + + flagSpeedName = "speed" + flagPowerName = "power" + flagTemperatureName = "temperature" + flagFrequencyName = "frequency" + flagMemoryName = "memory" + flagNumaName = "numa" + flagStorageName = "storage" + + flagStorageDirName = "storage-dir" +) + +var benchmarkSummaryTableName = "Benchmark Summary" + +var benchmarks = []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 benchmarks { + 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().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 benchmarks { + 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: 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 benchmarks { + 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 { + tables := []table.TableDefinition{} + // add benchmark tables + selectedBenchmarkCount := 0 + for _, benchmark := range benchmarks { + 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(benchmarks) { + summaryFunc = benchmarkSummaryFromTableValues + } + + reportingCommand := common.ReportingCommand{ + Cmd: cmd, + ScriptParams: map[string]string{"StorageDir": flagStorageDir}, + Tables: tables, + SummaryFunc: summaryFunc, + SummaryTableName: benchmarkSummaryTableName, + SummaryBeforeTableName: SpeedBenchmarkTableName, + InsightsFunc: nil, + SystemSummaryTableName: SystemSummaryTableName, + } + + 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/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..eb29dd6b --- /dev/null +++ b/cmd/benchmark/benchmark_tables.go @@ -0,0 +1,345 @@ +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" + SystemSummaryTableName = "System Summary" +) + +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}, + SystemSummaryTableName: { + Name: SystemSummaryTableName, + MenuLabel: SystemSummaryTableName, + HasRows: false, + ScriptNames: []string{ + script.LscpuScriptName, + script.UnameScriptName, + }, + FieldsFunc: systemSummaryTableValues}, +} + +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 systemSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Field { + lscpuOutput := outputs[script.LscpuScriptName].Stdout + return []table.Field{ + {Name: "Microarchitecture", Values: []string{common.UarchFromOutput(outputs)}}, + {Name: "Sockets", Values: []string{common.ValFromRegexSubmatch(lscpuOutput, `^Socket\(s\):\s*(.+)$`)}}, + } +} diff --git a/cmd/benchmark/benchmarking.go b/cmd/benchmark/benchmarking.go new file mode 100644 index 00000000..483d840c --- /dev/null +++ b/cmd/benchmark/benchmarking.go @@ -0,0 +1,203 @@ +package benchmark + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "encoding/json" + "fmt" + "log/slog" + "perfspect/internal/script" + "perfspect/internal/util" + "strconv" + "strings" +) + +// fioOutput is the top-level struct for the FIO JSON report. +// ref: https://fio.readthedocs.io/en/latest/fio_doc.html#json-output +type fioOutput struct { + FioVersion string `json:"fio version"` + Timestamp int64 `json:"timestamp"` + TimestampMs int64 `json:"timestamp_ms"` + Time string `json:"time"` + Jobs []fioJob `json:"jobs"` +} + +// Job represents a single job's results within the FIO report. +type fioJob struct { + Jobname string `json:"jobname"` + Groupid int `json:"groupid"` + JobStart int64 `json:"job_start"` + Error int `json:"error"` + Eta int `json:"eta"` + Elapsed int `json:"elapsed"` + Read fioIOStats `json:"read"` + Write fioIOStats `json:"write"` + Trim fioIOStats `json:"trim"` + JobRuntime int `json:"job_runtime"` + UsrCPU float64 `json:"usr_cpu"` + SysCPU float64 `json:"sys_cpu"` + Ctx int `json:"ctx"` + Majf int `json:"majf"` + Minf int `json:"minf"` + IodepthLevel map[string]float64 `json:"iodepth_level"` + IodepthSubmit map[string]float64 `json:"iodepth_submit"` + IodepthComplete map[string]float64 `json:"iodepth_complete"` + LatencyNs map[string]float64 `json:"latency_ns"` + LatencyUs map[string]float64 `json:"latency_us"` + LatencyMs map[string]float64 `json:"latency_ms"` + LatencyDepth int `json:"latency_depth"` + LatencyTarget int `json:"latency_target"` + LatencyPercentile float64 `json:"latency_percentile"` + LatencyWindow int `json:"latency_window"` +} + +// IOStats holds the detailed I/O statistics for read, write, or trim operations. +type fioIOStats struct { + IoBytes int64 `json:"io_bytes"` + IoKbytes int64 `json:"io_kbytes"` + BwBytes int64 `json:"bw_bytes"` + Bw int64 `json:"bw"` + Iops float64 `json:"iops"` + Runtime int `json:"runtime"` + TotalIos int `json:"total_ios"` + ShortIos int `json:"short_ios"` + DropIos int `json:"drop_ios"` + SlatNs fioLatencyStats `json:"slat_ns"` + ClatNs fioLatencyStatsPercentiles `json:"clat_ns"` + LatNs fioLatencyStats `json:"lat_ns"` + BwMin int `json:"bw_min"` + BwMax int `json:"bw_max"` + BwAgg float64 `json:"bw_agg"` + BwMean float64 `json:"bw_mean"` + BwDev float64 `json:"bw_dev"` + BwSamples int `json:"bw_samples"` + IopsMin int `json:"iops_min"` + IopsMax int `json:"iops_max"` + IopsMean float64 `json:"iops_mean"` + IopsStddev float64 `json:"iops_stddev"` + IopsSamples int `json:"iops_samples"` +} + +// fioLatencyStats holds basic latency metrics. +type fioLatencyStats struct { + Min int64 `json:"min"` + Max int64 `json:"max"` + Mean float64 `json:"mean"` + Stddev float64 `json:"stddev"` + N int `json:"N"` +} + +// LatencyStatsPercentiles holds latency metrics including percentiles. +type fioLatencyStatsPercentiles struct { + Min int64 `json:"min"` + Max int64 `json:"max"` + Mean float64 `json:"mean"` + Stddev float64 `json:"stddev"` + N int `json:"N"` + Percentile map[string]int64 `json:"percentile"` +} + +func cpuSpeedFromOutput(outputs map[string]script.ScriptOutput) string { + var vals []float64 + for line := range strings.SplitSeq(strings.TrimSpace(outputs[script.SpeedBenchmarkScriptName].Stdout), "\n") { + tokens := strings.Split(line, " ") + if len(tokens) != 2 { + slog.Error("unexpected stress-ng output format", slog.String("line", line)) + return "" + } + fv, err := strconv.ParseFloat(tokens[1], 64) + if err != nil { + slog.Error("unexpected value in 2nd token (%s), expected float in line: %s", slog.String("token", tokens[1]), slog.String("line", line)) + return "" + } + vals = append(vals, fv) + } + if len(vals) == 0 { + slog.Warn("no values detected in stress-ng output") + return "" + } + return fmt.Sprintf("%.0f", util.GeoMean(vals)) +} + +func storagePerfFromOutput(outputs map[string]script.ScriptOutput) (fioOutput, error) { + output := outputs[script.StorageBenchmarkScriptName].Stdout + if output == "" { + return fioOutput{}, fmt.Errorf("no output from storage benchmark") + } + if strings.Contains(output, "ERROR:") { + return fioOutput{}, fmt.Errorf("failed to run storage benchmark: %s", output) + } + i := strings.Index(output, "{\n \"fio version\"") + if i >= 0 { + output = output[i:] + } else { + outputLen := min(len(output), 100) + slog.Info("fio output snip", "output", output[:outputLen], "stderr", outputs[script.StorageBenchmarkScriptName].Stderr) + return fioOutput{}, fmt.Errorf("unable to find fio output") + } + var fioData fioOutput + if err := json.Unmarshal([]byte(output), &fioData); err != nil { + return fioOutput{}, fmt.Errorf("error unmarshalling JSON: %w", err) + } + if len(fioData.Jobs) == 0 { + return fioOutput{}, fmt.Errorf("no jobs found in storage benchmark output") + } + return fioData, nil +} + +// avxTurboFrequenciesFromOutput parses the output of avx-turbo and returns the turbo frequencies as a map of instruction type to frequencies +// Sample avx-turbo output +// ... +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 1 | scalar_iadd | Scalar integer adds | 1.000 | 3901 | 1.95 | 3900 | 1.00 +// 1 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 +// 1 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 2 | scalar_iadd | Scalar integer adds | 1.000 | 3901, 3901 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// 2 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// 2 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 +// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio +// 3 | scalar_iadd | Scalar integer adds | 1.000 | 3900, 3901, 3901 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// 3 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 975, 975 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// 3 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 975, 974 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 +// ... +func avxTurboFrequenciesFromOutput(output string) (instructionFreqs map[string][]float64, err error) { + instructionFreqs = make(map[string][]float64) + started := false + for line := range strings.SplitSeq(output, "\n") { + if strings.HasPrefix(line, "Cores | ID") { + started = true + continue + } + if !started { + continue + } + if line == "" { + started = false + continue + } + fields := strings.Split(line, "|") + if len(fields) < 7 { + err = fmt.Errorf("avx-turbo unable to measure frequencies") + return + } + freqs := strings.Split(fields[6], ",") + var sumFreqs float64 + for _, freq := range freqs { + var f float64 + f, err = strconv.ParseFloat(strings.TrimSpace(freq), 64) + if err != nil { + return + } + sumFreqs += f + } + avgFreq := sumFreqs / float64(len(freqs)) + instructionType := strings.TrimSpace(fields[1]) + if _, ok := instructionFreqs[instructionType]; !ok { + instructionFreqs[instructionType] = []float64{} + } + instructionFreqs[instructionType] = append(instructionFreqs[instructionType], avgFreq/1000.0) + } + return +} 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..2c0a7602 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" @@ -2120,99 +1809,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) From 375df8037334b523afdaf5bb0e08a0feb5b2ecd0 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:34:39 -0800 Subject: [PATCH 17/24] Add system summary table to benchmark command - Add --no-summary flag to optionally exclude system summary - Include Brief System Summary table by default (like telemetry command) - System summary provides quick overview of target system configuration - Follows same pattern as telemetry command for consistency The system summary table shows key system information including: - Host name, time, CPU model, microarchitecture, TDP - Sockets, cores, hyperthreading, CPUs, NUMA nodes - Scaling driver/governor, C-states, frequencies - Energy settings, memory, NIC, disk, OS, kernel --- cmd/benchmark/benchmark.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go index 5da5bcfb..45a4fd43 100644 --- a/cmd/benchmark/benchmark.go +++ b/cmd/benchmark/benchmark.go @@ -56,6 +56,8 @@ var ( flagNuma bool flagStorage bool + flagNoSystemSummary bool + flagStorageDir string ) @@ -71,6 +73,8 @@ const ( flagNumaName = "numa" flagStorageName = "storage" + flagNoSystemSummaryName = "no-summary" + flagStorageDirName = "storage-dir" ) @@ -95,6 +99,7 @@ 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().BoolVar(&flagNoSystemSummary, flagNoSystemSummaryName, false, "") Cmd.Flags().StringVar(&flagStorageDir, flagStorageDirName, "/tmp", "") common.AddTargetFlags(Cmd) @@ -146,6 +151,10 @@ func getFlagGroups() []common.FlagGroup { 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", @@ -210,7 +219,11 @@ func validateFlags(cmd *cobra.Command, args []string) error { } func runCmd(cmd *cobra.Command, args []string) error { - tables := []table.TableDefinition{} + 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 benchmarks { From 5f1b0cf64fea69fb5d8b7dc8f8336ea8b98e53a9 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:42:58 -0800 Subject: [PATCH 18/24] remove duplicate Signed-off-by: Harper, Jason M --- cmd/report/benchmarking.go | 203 ------------------------------------- 1 file changed, 203 deletions(-) delete mode 100644 cmd/report/benchmarking.go diff --git a/cmd/report/benchmarking.go b/cmd/report/benchmarking.go deleted file mode 100644 index 10a8327e..00000000 --- a/cmd/report/benchmarking.go +++ /dev/null @@ -1,203 +0,0 @@ -package report - -// Copyright (C) 2021-2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause - -import ( - "encoding/json" - "fmt" - "log/slog" - "perfspect/internal/script" - "perfspect/internal/util" - "strconv" - "strings" -) - -// fioOutput is the top-level struct for the FIO JSON report. -// ref: https://fio.readthedocs.io/en/latest/fio_doc.html#json-output -type fioOutput struct { - FioVersion string `json:"fio version"` - Timestamp int64 `json:"timestamp"` - TimestampMs int64 `json:"timestamp_ms"` - Time string `json:"time"` - Jobs []fioJob `json:"jobs"` -} - -// Job represents a single job's results within the FIO report. -type fioJob struct { - Jobname string `json:"jobname"` - Groupid int `json:"groupid"` - JobStart int64 `json:"job_start"` - Error int `json:"error"` - Eta int `json:"eta"` - Elapsed int `json:"elapsed"` - Read fioIOStats `json:"read"` - Write fioIOStats `json:"write"` - Trim fioIOStats `json:"trim"` - JobRuntime int `json:"job_runtime"` - UsrCPU float64 `json:"usr_cpu"` - SysCPU float64 `json:"sys_cpu"` - Ctx int `json:"ctx"` - Majf int `json:"majf"` - Minf int `json:"minf"` - IodepthLevel map[string]float64 `json:"iodepth_level"` - IodepthSubmit map[string]float64 `json:"iodepth_submit"` - IodepthComplete map[string]float64 `json:"iodepth_complete"` - LatencyNs map[string]float64 `json:"latency_ns"` - LatencyUs map[string]float64 `json:"latency_us"` - LatencyMs map[string]float64 `json:"latency_ms"` - LatencyDepth int `json:"latency_depth"` - LatencyTarget int `json:"latency_target"` - LatencyPercentile float64 `json:"latency_percentile"` - LatencyWindow int `json:"latency_window"` -} - -// IOStats holds the detailed I/O statistics for read, write, or trim operations. -type fioIOStats struct { - IoBytes int64 `json:"io_bytes"` - IoKbytes int64 `json:"io_kbytes"` - BwBytes int64 `json:"bw_bytes"` - Bw int64 `json:"bw"` - Iops float64 `json:"iops"` - Runtime int `json:"runtime"` - TotalIos int `json:"total_ios"` - ShortIos int `json:"short_ios"` - DropIos int `json:"drop_ios"` - SlatNs fioLatencyStats `json:"slat_ns"` - ClatNs fioLatencyStatsPercentiles `json:"clat_ns"` - LatNs fioLatencyStats `json:"lat_ns"` - BwMin int `json:"bw_min"` - BwMax int `json:"bw_max"` - BwAgg float64 `json:"bw_agg"` - BwMean float64 `json:"bw_mean"` - BwDev float64 `json:"bw_dev"` - BwSamples int `json:"bw_samples"` - IopsMin int `json:"iops_min"` - IopsMax int `json:"iops_max"` - IopsMean float64 `json:"iops_mean"` - IopsStddev float64 `json:"iops_stddev"` - IopsSamples int `json:"iops_samples"` -} - -// fioLatencyStats holds basic latency metrics. -type fioLatencyStats struct { - Min int64 `json:"min"` - Max int64 `json:"max"` - Mean float64 `json:"mean"` - Stddev float64 `json:"stddev"` - N int `json:"N"` -} - -// LatencyStatsPercentiles holds latency metrics including percentiles. -type fioLatencyStatsPercentiles struct { - Min int64 `json:"min"` - Max int64 `json:"max"` - Mean float64 `json:"mean"` - Stddev float64 `json:"stddev"` - N int `json:"N"` - Percentile map[string]int64 `json:"percentile"` -} - -func cpuSpeedFromOutput(outputs map[string]script.ScriptOutput) string { - var vals []float64 - for line := range strings.SplitSeq(strings.TrimSpace(outputs[script.SpeedBenchmarkScriptName].Stdout), "\n") { - tokens := strings.Split(line, " ") - if len(tokens) != 2 { - slog.Error("unexpected stress-ng output format", slog.String("line", line)) - return "" - } - fv, err := strconv.ParseFloat(tokens[1], 64) - if err != nil { - slog.Error("unexpected value in 2nd token (%s), expected float in line: %s", slog.String("token", tokens[1]), slog.String("line", line)) - return "" - } - vals = append(vals, fv) - } - if len(vals) == 0 { - slog.Warn("no values detected in stress-ng output") - return "" - } - return fmt.Sprintf("%.0f", util.GeoMean(vals)) -} - -func storagePerfFromOutput(outputs map[string]script.ScriptOutput) (fioOutput, error) { - output := outputs[script.StorageBenchmarkScriptName].Stdout - if output == "" { - return fioOutput{}, fmt.Errorf("no output from storage benchmark") - } - if strings.Contains(output, "ERROR:") { - return fioOutput{}, fmt.Errorf("failed to run storage benchmark: %s", output) - } - i := strings.Index(output, "{\n \"fio version\"") - if i >= 0 { - output = output[i:] - } else { - outputLen := min(len(output), 100) - slog.Info("fio output snip", "output", output[:outputLen], "stderr", outputs[script.StorageBenchmarkScriptName].Stderr) - return fioOutput{}, fmt.Errorf("unable to find fio output") - } - var fioData fioOutput - if err := json.Unmarshal([]byte(output), &fioData); err != nil { - return fioOutput{}, fmt.Errorf("error unmarshalling JSON: %w", err) - } - if len(fioData.Jobs) == 0 { - return fioOutput{}, fmt.Errorf("no jobs found in storage benchmark output") - } - return fioData, nil -} - -// avxTurboFrequenciesFromOutput parses the output of avx-turbo and returns the turbo frequencies as a map of instruction type to frequencies -// Sample avx-turbo output -// ... -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 1 | scalar_iadd | Scalar integer adds | 1.000 | 3901 | 1.95 | 3900 | 1.00 -// 1 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 -// 1 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974 | 1.95 | 3900 | 1.00 -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 2 | scalar_iadd | Scalar integer adds | 1.000 | 3901, 3901 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// 2 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// 2 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 974 | 1.95, 1.95 | 3900, 3900 | 1.00, 1.00 -// Cores | ID | Description | OVRLP3 | Mops | A/M-ratio | A/M-MHz | M/tsc-ratio -// 3 | scalar_iadd | Scalar integer adds | 1.000 | 3900, 3901, 3901 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// 3 | avx256_fma | 256-bit serial DP FMAs | 1.000 | 974, 975, 975 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// 3 | avx512_fma | 512-bit serial DP FMAs | 1.000 | 974, 975, 974 | 1.95, 1.95, 1.95 | 3900, 3900, 3900 | 1.00, 1.00, 1.00 -// ... -func avxTurboFrequenciesFromOutput(output string) (instructionFreqs map[string][]float64, err error) { - instructionFreqs = make(map[string][]float64) - started := false - for line := range strings.SplitSeq(output, "\n") { - if strings.HasPrefix(line, "Cores | ID") { - started = true - continue - } - if !started { - continue - } - if line == "" { - started = false - continue - } - fields := strings.Split(line, "|") - if len(fields) < 7 { - err = fmt.Errorf("avx-turbo unable to measure frequencies") - return - } - freqs := strings.Split(fields[6], ",") - var sumFreqs float64 - for _, freq := range freqs { - var f float64 - f, err = strconv.ParseFloat(strings.TrimSpace(freq), 64) - if err != nil { - return - } - sumFreqs += f - } - avgFreq := sumFreqs / float64(len(freqs)) - instructionType := strings.TrimSpace(fields[1]) - if _, ok := instructionFreqs[instructionType]; !ok { - instructionFreqs[instructionType] = []float64{} - } - instructionFreqs[instructionType] = append(instructionFreqs[instructionType], avgFreq/1000.0) - } - return -} From ade5c636dafed76aafe24a8c58425b1576954295 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:43:06 -0800 Subject: [PATCH 19/24] formatting Signed-off-by: Harper, Jason M --- cmd/report/report_tables.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/report/report_tables.go b/cmd/report/report_tables.go index 2c0a7602..8db5fdcd 100644 --- a/cmd/report/report_tables.go +++ b/cmd/report/report_tables.go @@ -1808,4 +1808,3 @@ func dimmTableHTMLRenderer(tableValues table.TableValues, targetName string) str } return report.RenderHTMLTable(socketTableHeaders, socketTableValues, "pure-table pure-table-bordered", [][]string{}) } - From 43126ccdd7e10d662cc937be3fb69123383eb9ad Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:49:46 -0800 Subject: [PATCH 20/24] Update documentation for new benchmark command - Add benchmark command to commands table in README.md - Create new Benchmark Command section with detailed descriptions - Remove benchmark subsection from Report Command documentation - Update copilot-instructions.md to include benchmark command - Document --no-summary flag for excluding system summary The benchmark command is now properly documented as a standalone command rather than a flag within the report command. --- .github/copilot-instructions.md | 3 ++- README.md | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) 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..7f9bb30b 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,15 +88,24 @@ 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. | From 6c96a736a10c89b85621b2bd966701185616ec35 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 03:56:23 -0800 Subject: [PATCH 21/24] Add alias 'bench' to benchmark command Signed-off-by: Harper, Jason M --- cmd/benchmark/benchmark.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go index 45a4fd43..c23050e0 100644 --- a/cmd/benchmark/benchmark.go +++ b/cmd/benchmark/benchmark.go @@ -35,6 +35,7 @@ var examples = []string{ var Cmd = &cobra.Command{ Use: cmdName, + Aliases: []string{"bench"}, Short: "Run performance benchmarks on target(s)", Example: strings.Join(examples, "\n"), RunE: runCmd, From 6fa2ea5e92ba63ac97a19f6db0b9c8a1380cdeac Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 05:19:57 -0800 Subject: [PATCH 22/24] Update storage command description to reflect new I/O patterns and disk space requirements Signed-off-by: Harper, Jason M --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f9bb30b..06284bb1 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ See `perfspect benchmark -h` for all options. | 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`). From a2a4da3fa8f83b662241d20e886ccdc4b1d06468 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 12:34:26 -0800 Subject: [PATCH 23/24] ref data Signed-off-by: Harper, Jason M --- cmd/benchmark/benchmark.go | 17 ++++++++--------- cmd/benchmark/benchmark_tables.go | 18 ------------------ 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/cmd/benchmark/benchmark.go b/cmd/benchmark/benchmark.go index c23050e0..b299ec2e 100644 --- a/cmd/benchmark/benchmark.go +++ b/cmd/benchmark/benchmark.go @@ -81,7 +81,7 @@ const ( var benchmarkSummaryTableName = "Benchmark Summary" -var benchmarks = []common.Category{ +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]}}, @@ -93,7 +93,7 @@ var benchmarks = []common.Category{ func init() { // set up benchmark flags - for _, benchmark := range benchmarks { + for _, benchmark := range categories { Cmd.Flags().BoolVar(benchmark.FlagVar, benchmark.FlagName, benchmark.DefaultValue, benchmark.Help) } // set up other flags @@ -141,7 +141,7 @@ func getFlagGroups() []common.FlagGroup { Help: "run all benchmarks", }, } - for _, benchmark := range benchmarks { + for _, benchmark := range categories { flags = append(flags, common.Flag{ Name: benchmark.FlagName, Help: benchmark.Help, @@ -186,7 +186,7 @@ func getFlagGroups() []common.FlagGroup { func validateFlags(cmd *cobra.Command, args []string) error { // clear flagAll if any benchmarks are selected if flagAll { - for _, benchmark := range benchmarks { + for _, benchmark := range categories { if benchmark.FlagVar != nil && *benchmark.FlagVar { flagAll = false break @@ -227,7 +227,7 @@ func runCmd(cmd *cobra.Command, args []string) error { } // add benchmark tables selectedBenchmarkCount := 0 - for _, benchmark := range benchmarks { + for _, benchmark := range categories { if *benchmark.FlagVar || flagAll { tables = append(tables, benchmark.Tables...) selectedBenchmarkCount++ @@ -235,7 +235,7 @@ func runCmd(cmd *cobra.Command, args []string) error { } // include benchmark summary table if all benchmarks are selected var summaryFunc common.SummaryFunc - if selectedBenchmarkCount == len(benchmarks) { + if selectedBenchmarkCount == len(categories) { summaryFunc = benchmarkSummaryFromTableValues } @@ -247,7 +247,6 @@ func runCmd(cmd *cobra.Command, args []string) error { SummaryTableName: benchmarkSummaryTableName, SummaryBeforeTableName: SpeedBenchmarkTableName, InsightsFunc: nil, - SystemSummaryTableName: SystemSummaryTableName, } report.RegisterHTMLRenderer(FrequencyBenchmarkTableName, frequencyBenchmarkTableHtmlRenderer) @@ -313,8 +312,8 @@ func benchmarkSummaryFromTableValues(allTableValues []table.TableValues, outputs {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)}}, + {Name: "Microarchitecture", Values: []string{common.UarchFromOutput(outputs)}}, + {Name: "Sockets", Values: []string{common.ValFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Socket\(s\):\s*(.+)$`)}}, }, } } diff --git a/cmd/benchmark/benchmark_tables.go b/cmd/benchmark/benchmark_tables.go index eb29dd6b..51639d6a 100644 --- a/cmd/benchmark/benchmark_tables.go +++ b/cmd/benchmark/benchmark_tables.go @@ -25,7 +25,6 @@ const ( MemoryBenchmarkTableName = "Memory Benchmark" NUMABenchmarkTableName = "NUMA Benchmark" StorageBenchmarkTableName = "Storage Benchmark" - SystemSummaryTableName = "System Summary" ) var tableDefinitions = map[string]table.TableDefinition{ @@ -97,15 +96,6 @@ var tableDefinitions = map[string]table.TableDefinition{ script.StorageBenchmarkScriptName, }, FieldsFunc: storageBenchmarkTableValues}, - SystemSummaryTableName: { - Name: SystemSummaryTableName, - MenuLabel: SystemSummaryTableName, - HasRows: false, - ScriptNames: []string{ - script.LscpuScriptName, - script.UnameScriptName, - }, - FieldsFunc: systemSummaryTableValues}, } func speedBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table.Field { @@ -335,11 +325,3 @@ func storageBenchmarkTableValues(outputs map[string]script.ScriptOutput) []table } return fields } - -func systemSummaryTableValues(outputs map[string]script.ScriptOutput) []table.Field { - lscpuOutput := outputs[script.LscpuScriptName].Stdout - return []table.Field{ - {Name: "Microarchitecture", Values: []string{common.UarchFromOutput(outputs)}}, - {Name: "Sockets", Values: []string{common.ValFromRegexSubmatch(lscpuOutput, `^Socket\(s\):\s*(.+)$`)}}, - } -} From cc28614ac1fa3007115e9ab6222c5dded80ab57b Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 15 Dec 2025 12:58:10 -0800 Subject: [PATCH 24/24] Filter out anomalous high power and temperature readings in turbostat output parsing Signed-off-by: Harper, Jason M --- internal/common/turbostat.go | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 }