From 22bb007a591425f3c37efafea16e7b1cc919b0bd Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 12 Nov 2025 13:28:59 -0800 Subject: [PATCH 1/5] fix: improve error handling and input validation Signed-off-by: Harper, Jason M --- internal/report/table_helpers.go | 74 +++++++++++++++++++------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index 63c84e0d..b9e94f21 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -303,18 +303,10 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string for _, isaHex := range values[1:] { var isaFreqs []string var freqs []int - if isaHex != "0" { - var err error - freqs, err = getFrequenciesFromHex(isaHex) - if err != nil { - return nil, fmt.Errorf("failed to get frequencies from Hex string: %w", err) - } - } else { - // if the ISA is not supported, set the frequency to zero for all buckets - freqs = make([]int, len(bucketCoreCounts)) - for i := range freqs { - freqs[i] = 0 - } + var err error + freqs, err = getFrequenciesFromHex(isaHex) + if err != nil { + return nil, fmt.Errorf("failed to get frequencies from Hex string: %w", err) } if len(freqs) != len(bucketCoreCounts) { freqs, err = padFrequencies(freqs, len(bucketCoreCounts)) @@ -339,6 +331,9 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string } // add fieldNames for ISAs that have frequencies for i := range allIsaFreqs { + if len(allIsaFreqs[i]) < 1 { + return nil, fmt.Errorf("no frequencies found for isa") + } if allIsaFreqs[i][0] == "0.0" { continue } @@ -348,20 +343,25 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string row := make([]string, 0, len(allIsaFreqs)+2) // add the total core buckets for multi-die architectures if archMultiplier > 1 { + if i >= len(totalCoreBuckets) { + return nil, fmt.Errorf("index out of range for total core buckets") + } row = append(row, totalCoreBuckets[i]) } // add the die core buckets row = append(row, bucket) // add the frequencies for each ISA for _, isaFreqs := range allIsaFreqs { + if len(isaFreqs) < 1 { + return nil, fmt.Errorf("no frequencies found for isa") + } if isaFreqs[0] == "0.0" { continue - } else { - if i >= len(isaFreqs) { - return nil, fmt.Errorf("index out of range for isa frequencies") - } - row = append(row, isaFreqs[i]) } + if i >= len(isaFreqs) { + return nil, fmt.Errorf("index out of range for isa frequencies") + } + row = append(row, isaFreqs[i]) } specCoreFreqs = append(specCoreFreqs, row) } @@ -895,7 +895,11 @@ func elcFieldValuesFromOutput(outputs map[string]script.ScriptOutput) (fieldValu values := []string{} // value rows for _, row := range rows[1:] { - values = append(values, row[fieldNamesIndex]) + if fieldNamesIndex < len(row) { + values = append(values, row[fieldNamesIndex]) + } else { + values = append(values, "") + } } fieldValues = append(fieldValues, Field{Name: fieldName, Values: values}) } @@ -1010,12 +1014,16 @@ func eppFromOutput(outputs map[string]script.ScriptOutput) string { } // check if the epp valid bit is set and consistent across all cores var eppValid string - for i, line := range strings.Split(outputs[script.EppValidScriptName].Stdout, "\n") { // MSR 0x774, bit 60 + for line := range strings.SplitSeq(outputs[script.EppValidScriptName].Stdout, "\n") { // MSR 0x774, bit 60 if line == "" { continue } - currentEpbValid := strings.TrimSpace(strings.Split(line, ":")[1]) - if i == 0 { + parts := strings.Split(line, ":") + if len(parts) < 2 { + continue + } + currentEpbValid := strings.TrimSpace(parts[1]) + if eppValid == "" { eppValid = currentEpbValid continue } @@ -1026,12 +1034,16 @@ func eppFromOutput(outputs map[string]script.ScriptOutput) string { } // check if epp package control bit is set and consistent across all cores var eppPkgCtrl string - for i, line := range strings.Split(outputs[script.EppPackageControlScriptName].Stdout, "\n") { // MSR 0x774, bit 42 + for line := range strings.SplitSeq(outputs[script.EppPackageControlScriptName].Stdout, "\n") { // MSR 0x774, bit 42 if line == "" { continue } - currentEppPkgCtrl := strings.TrimSpace(strings.Split(line, ":")[1]) - if i == 0 { + parts := strings.Split(line, ":") + if len(parts) < 2 { + continue + } + currentEppPkgCtrl := strings.TrimSpace(parts[1]) + if eppPkgCtrl == "" { eppPkgCtrl = currentEppPkgCtrl continue } @@ -1050,12 +1062,16 @@ func eppFromOutput(outputs map[string]script.ScriptOutput) string { return eppValToLabel(int(msr)) } else { var epp string - for i, line := range strings.Split(outputs[script.EppScriptName].Stdout, "\n") { // MSR 0x774, bits 24-31 (per-core) + for line := range strings.SplitSeq(outputs[script.EppScriptName].Stdout, "\n") { // MSR 0x774, bits 24-31 (per-core) if line == "" { continue } - currentEpp := strings.TrimSpace(strings.Split(line, ":")[1]) - if i == 0 { + parts := strings.Split(line, ":") + if len(parts) < 2 { + continue + } + currentEpp := strings.TrimSpace(parts[1]) + if epp == "" { epp = currentEpp continue } @@ -1461,7 +1477,7 @@ func filesystemFieldValuesFromOutput(outputs map[string]script.ScriptOutput) []F } fields := strings.Fields(line) // "Mounted On" gets split into two fields, rejoin - if i == 0 && fields[len(fields)-2] == "Mounted" && fields[len(fields)-1] == "on" { + if i == 0 && len(fields) >= 2 && fields[len(fields)-2] == "Mounted" && fields[len(fields)-1] == "on" { fields[len(fields)-2] = "Mounted on" fields = fields[:len(fields)-1] for _, field := range fields { @@ -1720,7 +1736,7 @@ func getPCIDevices(class string, outputs map[string]script.ScriptOutput) (device continue } match := re.FindStringSubmatch(line) - if len(match) > 0 { + if len(match) >= 3 { key := match[1] value := match[2] device[key] = value From 431b7f1a7160b38d9de362d2869a6403d9846133 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Thu, 13 Nov 2025 17:03:36 -0800 Subject: [PATCH 2/5] refactor and move frequency functions into new source file, plus more Signed-off-by: Harper, Jason M --- internal/report/table_helpers.go | 568 +----------------- internal/report/table_helpers_accelerator.go | 93 +++ internal/report/table_helpers_frequency.go | 502 ++++++++++++++++ .../report/table_helpers_frequency_test.go | 207 +++++++ .../table_helpers_nic_integration_test.go | 204 ------- internal/report/table_helpers_nic_test.go | 195 ++++++ internal/report/table_helpers_stacks.go | 79 +++ internal/report/table_helpers_test.go | 197 ------ 8 files changed, 1077 insertions(+), 968 deletions(-) create mode 100644 internal/report/table_helpers_accelerator.go create mode 100644 internal/report/table_helpers_frequency.go create mode 100644 internal/report/table_helpers_frequency_test.go delete mode 100644 internal/report/table_helpers_nic_integration_test.go diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index b9e94f21..23c74f29 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -152,326 +152,7 @@ func UarchFromOutput(outputs map[string]script.ScriptOutput) string { return "" } -// baseFrequencyFromOutput gets base core frequency -// -// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/base_frequency -// 2nd option) from dmidecode "Current Speed" -// 3nd option) parse it from the model name -func baseFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - cmdout := strings.TrimSpace(outputs[script.BaseFrequencyScriptName].Stdout) - if cmdout != "" { - freqf, err := strconv.ParseFloat(cmdout, 64) - if err == nil { - freqf = freqf / 1000000 - return fmt.Sprintf("%.1fGHz", freqf) - } - } - currentSpeedVal := valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Current Speed:\s(.*)$`) - tokens := strings.Split(currentSpeedVal, " ") - if len(tokens) == 2 { - num, err := strconv.ParseFloat(tokens[0], 64) - if err == nil { - unit := tokens[1] - if unit == "MHz" { - num = num / 1000 - unit = "GHz" - } - return fmt.Sprintf("%.1f%s", num, unit) - } - } - // the frequency (if included) is at the end of the model name in lscpu's output - modelName := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^[Mm]odel name.*:\s*(.+?)$`) - tokens = strings.Split(modelName, " ") - if len(tokens) > 0 { - lastToken := tokens[len(tokens)-1] - if len(lastToken) > 0 && lastToken[len(lastToken)-1] == 'z' { - return lastToken - } - } - return "" -} - -// getFrequenciesFromHex -func getFrequenciesFromHex(hex string) ([]int, error) { - freqs, err := util.HexToIntList(hex) - if err != nil { - return nil, err - } - // reverse the order of the frequencies - slices.Reverse(freqs) - return freqs, nil -} - -// getBucketSizesFromHex -func getBucketSizesFromHex(hex string) ([]int, error) { - bucketSizes, err := util.HexToIntList(hex) - if err != nil { - return nil, err - } - if len(bucketSizes) != 8 { - err = fmt.Errorf("expected 8 bucket sizes, got %d", len(bucketSizes)) - return nil, err - } - // reverse the order of the core counts - slices.Reverse(bucketSizes) - return bucketSizes, nil -} - -// padFrequencies adds items to the frequencies slice until it reaches the desired length. -// The value of the added items is the same as the last item in the original slice. -func padFrequencies(freqs []int, desiredLength int) ([]int, error) { - if len(freqs) == 0 { - return nil, fmt.Errorf("cannot pad empty frequencies slice") - } - for len(freqs) < desiredLength { - freqs = append(freqs, freqs[len(freqs)-1]) - } - return freqs, nil -} - -// getSpecFrequencyBuckets -// returns slice of rows -// first row is header -// each row is a slice of strings -// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx" -// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1" -// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1" -// "64-85", "32-43", "3.5", "3.5", "3.3", "3.2", "3.1" -// ... -// the "cores per die" column is only present for some architectures -func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string, error) { - arch := UarchFromOutput(outputs) - if arch == "" { - return nil, fmt.Errorf("uarch is required") - } - out := outputs[script.SpecCoreFrequenciesScriptName].Stdout - // expected script output format, the number of fields may vary: - // "cores sse avx2 avx512 avx512h amx" - // "hex hex hex hex hex hex" - if out == "" { - return nil, fmt.Errorf("no core frequencies found") - } - lines := strings.Split(out, "\n") - if len(lines) < 2 { - return nil, fmt.Errorf("unexpected output format") - } - fieldNames := strings.Fields(lines[0]) - if len(fieldNames) < 2 { - return nil, fmt.Errorf("unexpected output format") - } - values := strings.Fields(lines[1]) - if len(values) != len(fieldNames) { - return nil, fmt.Errorf("unexpected output format") - } - // get list of buckets sizes - bucketCoreCounts, err := getBucketSizesFromHex(values[0]) - if err != nil { - return nil, fmt.Errorf("failed to get bucket sizes from Hex string: %w", err) - } - // create buckets - var totalCoreBuckets []string // only for multi-die architectures - var dieCoreBuckets []string - totalCoreStartRange := 1 - startRange := 1 - var archMultiplier int - if strings.Contains(arch, "SRF") || strings.Contains(arch, "CWF") { - archMultiplier = 4 - } else if strings.Contains(arch, "GNR_X3") { - archMultiplier = 3 - } else if strings.Contains(arch, "GNR_X2") { - archMultiplier = 2 - } else { - archMultiplier = 1 - } - for _, count := range bucketCoreCounts { - if startRange > count { - break - } - if archMultiplier > 1 { - totalCoreCount := count * archMultiplier - if totalCoreStartRange > int(totalCoreCount) { - break - } - totalCoreBuckets = append(totalCoreBuckets, fmt.Sprintf("%d-%d", totalCoreStartRange, totalCoreCount)) - totalCoreStartRange = int(totalCoreCount) + 1 - } - dieCoreBuckets = append(dieCoreBuckets, fmt.Sprintf("%d-%d", startRange, count)) - startRange = int(count) + 1 - } - // get the frequencies for each isa - var allIsaFreqs [][]string - for _, isaHex := range values[1:] { - var isaFreqs []string - var freqs []int - var err error - freqs, err = getFrequenciesFromHex(isaHex) - if err != nil { - return nil, fmt.Errorf("failed to get frequencies from Hex string: %w", err) - } - if len(freqs) != len(bucketCoreCounts) { - freqs, err = padFrequencies(freqs, len(bucketCoreCounts)) - if err != nil { - return nil, fmt.Errorf("failed to pad frequencies: %w", err) - } - } - for _, freq := range freqs { - // convert freq to GHz - freqf := float64(freq) / 10.0 - isaFreqs = append(isaFreqs, fmt.Sprintf("%.1f", freqf)) - } - allIsaFreqs = append(allIsaFreqs, isaFreqs) - } - // format the output - var specCoreFreqs [][]string - specCoreFreqs = make([][]string, 1, len(dieCoreBuckets)+1) - // add bucket field name(s) - specCoreFreqs[0] = append(specCoreFreqs[0], "Cores") - if archMultiplier > 1 { - specCoreFreqs[0] = append(specCoreFreqs[0], "Cores per Die") - } - // add fieldNames for ISAs that have frequencies - for i := range allIsaFreqs { - if len(allIsaFreqs[i]) < 1 { - return nil, fmt.Errorf("no frequencies found for isa") - } - if allIsaFreqs[i][0] == "0.0" { - continue - } - specCoreFreqs[0] = append(specCoreFreqs[0], strings.ToUpper(fieldNames[i+1])) - } - for i, bucket := range dieCoreBuckets { - row := make([]string, 0, len(allIsaFreqs)+2) - // add the total core buckets for multi-die architectures - if archMultiplier > 1 { - if i >= len(totalCoreBuckets) { - return nil, fmt.Errorf("index out of range for total core buckets") - } - row = append(row, totalCoreBuckets[i]) - } - // add the die core buckets - row = append(row, bucket) - // add the frequencies for each ISA - for _, isaFreqs := range allIsaFreqs { - if len(isaFreqs) < 1 { - return nil, fmt.Errorf("no frequencies found for isa") - } - if isaFreqs[0] == "0.0" { - continue - } - if i >= len(isaFreqs) { - return nil, fmt.Errorf("index out of range for isa frequencies") - } - row = append(row, isaFreqs[i]) - } - specCoreFreqs = append(specCoreFreqs, row) - } - return specCoreFreqs, nil -} - -// expandTurboFrequencies expands the turbo frequencies to a list of frequencies -// input is the output of getSpecFrequencyBuckets, e.g.: -// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx" -// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1" -// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1" -// ... -// output is the expanded list of the frequencies for the requested ISA -func expandTurboFrequencies(specFrequencyBuckets [][]string, isa string) ([]string, error) { - if len(specFrequencyBuckets) < 2 || len(specFrequencyBuckets[0]) < 2 { - return nil, fmt.Errorf("unable to parse core frequency buckets") - } - rangeIdx := 0 // the first column is the bucket, e.g., 1-44 - // find the index of the ISA column - var isaIdx int - for i := 1; i < len(specFrequencyBuckets[0]); i++ { - if strings.EqualFold(specFrequencyBuckets[0][i], isa) { - isaIdx = i - break - } - } - if isaIdx == 0 { - return nil, fmt.Errorf("unable to find %s frequency column", isa) - } - var freqs []string - for i := 1; i < len(specFrequencyBuckets); i++ { - bucketCores, err := util.IntRangeToIntList(strings.TrimSpace(specFrequencyBuckets[i][rangeIdx])) - if err != nil { - return nil, fmt.Errorf("unable to parse bucket range %s", specFrequencyBuckets[i][rangeIdx]) - } - bucketFreq := strings.TrimSpace(specFrequencyBuckets[i][isaIdx]) - if bucketFreq == "" { - return nil, fmt.Errorf("unable to parse bucket frequency %s", specFrequencyBuckets[i][isaIdx]) - } - for range bucketCores { - freqs = append(freqs, bucketFreq) - } - } - return freqs, nil -} - -// maxFrequencyFromOutputs gets max core frequency -// -// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq -// 2nd option) from MSR/tpmi -// 3rd option) from dmidecode "Max Speed" -func maxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - cmdout := strings.TrimSpace(outputs[script.MaximumFrequencyScriptName].Stdout) - if cmdout != "" { - freqf, err := strconv.ParseFloat(cmdout, 64) - if err == nil { - freqf = freqf / 1000000 - return fmt.Sprintf("%.1fGHz", freqf) - } - } - // get the max frequency from the MSR/tpmi - specCoreFrequencies, err := getSpecFrequencyBuckets(outputs) - if err == nil { - sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies) - if len(sseFreqs) > 0 { - // max (single-core) frequency is the first SSE frequency - return sseFreqs[0] + "GHz" - } - } - return valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Max Speed:\s(.*)`) -} - -func getSSEFreqsFromBuckets(buckets [][]string) []string { - if len(buckets) < 2 { - return nil - } - // find the SSE column - sseColumn := -1 - for i, col := range buckets[0] { - if strings.ToUpper(col) == "SSE" { - sseColumn = i - break - } - } - if sseColumn == -1 { - return nil - } - // get the SSE values from the buckets - sse := make([]string, 0, len(buckets)-1) - for i := 1; i < len(buckets); i++ { - if len(buckets[i]) > sseColumn { - sse = append(sse, buckets[i][sseColumn]) - } - } - return sse -} - -func allCoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - specCoreFrequencies, err := getSpecFrequencyBuckets(outputs) - if err != nil { - return "" - } - sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies) - if len(sseFreqs) < 1 { - return "" - } - // all core max frequency is the last SSE frequency - return sseFreqs[len(sseFreqs)-1] + "GHz" -} - +// hyperthreadingFromOutput determines if hyperthreading is enabled based on lscpu output func hyperthreadingFromOutput(outputs map[string]script.ScriptOutput) string { family := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^CPU family:\s*(.+)$`) model := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Model:\s*(.+)$`) @@ -696,73 +377,6 @@ func prefetchersSummaryFromOutput(outputs map[string]script.ScriptOutput) string return "None" } -func acceleratorNames() []string { - var names []string - for _, accel := range acceleratorDefinitions { - names = append(names, accel.Name) - } - return names -} - -func acceleratorCountsFromOutput(outputs map[string]script.ScriptOutput) []string { - var counts []string - lshw := outputs[script.LshwScriptName].Stdout - for _, accel := range acceleratorDefinitions { - regex := fmt.Sprintf("%s:%s", accel.MfgID, accel.DevID) - re := regexp.MustCompile(regex) - count := len(re.FindAllString(lshw, -1)) - counts = append(counts, fmt.Sprintf("%d", count)) - } - return counts -} - -func acceleratorWorkQueuesFromOutput(outputs map[string]script.ScriptOutput) []string { - var queues []string - for _, accel := range acceleratorDefinitions { - if accel.Name == "IAA" || accel.Name == "DSA" { - var scriptName string - if accel.Name == "IAA" { - scriptName = script.IaaDevicesScriptName - } else { - scriptName = script.DsaDevicesScriptName - } - devices := outputs[scriptName].Stdout - lines := strings.Split(devices, "\n") - // get non-empty lines - var nonEmptyLines []string - for _, line := range lines { - if strings.TrimSpace(line) != "" { - nonEmptyLines = append(nonEmptyLines, line) - } - } - if len(nonEmptyLines) == 0 { - queues = append(queues, "None") - } else { - queues = append(queues, strings.Join(nonEmptyLines, ", ")) - } - } else { - queues = append(queues, "N/A") - } - } - return queues -} - -func acceleratorFullNamesFromYaml() []string { - var fullNames []string - for _, accel := range acceleratorDefinitions { - fullNames = append(fullNames, accel.FullName) - } - return fullNames -} - -func acceleratorDescriptionsFromYaml() []string { - var descriptions []string - for _, accel := range acceleratorDefinitions { - descriptions = append(descriptions, accel.Description) - } - return descriptions -} - func tdpFromOutput(outputs map[string]script.ScriptOutput) string { msrHex := strings.TrimSpace(outputs[script.PackagePowerLimitName].Stdout) msr, err := strconv.ParseInt(msrHex, 16, 0) @@ -772,94 +386,6 @@ func tdpFromOutput(outputs map[string]script.ScriptOutput) string { return fmt.Sprint(msr/8) + "W" } -func uncoreMinMaxDieFrequencyFromOutput(maxFreq bool, computeDie bool, outputs map[string]script.ScriptOutput) string { - // find the first die that matches requrested die type (compute or I/O) - re := regexp.MustCompile(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry (\d+) in instance (\d+)`) - var instance, entry string - found := false - for line := range strings.SplitSeq(outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout, "\n") { - match := re.FindStringSubmatch(line) - if match == nil { - continue - } - if computeDie && match[1] == "0" { - found = true - entry = match[2] - instance = match[3] - break - } - if !computeDie && match[1] == "1" { - found = true - entry = match[2] - instance = match[3] - break - } - } - if !found { - slog.Error("failed to find uncore die type in TPMI output", slog.String("output", outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout)) - return "" - } - // get the frequency for the found die - re = regexp.MustCompile(fmt.Sprintf(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry %s in instance %s`, entry, instance)) - found = false - var parsed int64 - var err error - var scriptName string - if maxFreq { - scriptName = script.UncoreMaxFromTPMIScriptName - } else { - scriptName = script.UncoreMinFromTPMIScriptName - } - for line := range strings.SplitSeq(outputs[scriptName].Stdout, "\n") { - match := re.FindStringSubmatch(line) - if len(match) > 0 { - found = true - parsed, err = strconv.ParseInt(match[1], 10, 64) - if err != nil { - slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("line", line)) - return "" - } - break - } - } - if !found { - slog.Error("failed to find uncore frequency in TPMI output", slog.String("output", outputs[scriptName].Stdout)) - return "" - } - return fmt.Sprintf("%.1fGHz", float64(parsed)/10) -} - -func uncoreMinMaxFrequencyFromOutput(maxFreq bool, outputs map[string]script.ScriptOutput) string { - var parsed int64 - var err error - var scriptName string - if maxFreq { - scriptName = script.UncoreMaxFromMSRScriptName - } else { - scriptName = script.UncoreMinFromMSRScriptName - } - hex := strings.TrimSpace(outputs[scriptName].Stdout) - if hex != "" && hex != "0" { - parsed, err = strconv.ParseInt(hex, 16, 64) - if err != nil { - slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("hex", hex)) - return "" - } - } else { - slog.Warn("failed to get uncore frequency from MSR", slog.String("hex", hex)) - return "" - } - return fmt.Sprintf("%.1fGHz", float64(parsed)/10) -} - -func uncoreMinFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - return uncoreMinMaxFrequencyFromOutput(false, outputs) -} - -func uncoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - return uncoreMinMaxFrequencyFromOutput(true, outputs) -} - func chaCountFromOutput(outputs map[string]script.ScriptOutput) string { // output is the result of three rdmsr calls // - client cha count @@ -1823,21 +1349,6 @@ func diskSummaryFromOutput(outputs map[string]script.ScriptOutput) string { return strings.Join(summary, ", ") } -func acceleratorSummaryFromOutput(outputs map[string]script.ScriptOutput) string { - var summary []string - accelerators := acceleratorNames() - counts := acceleratorCountsFromOutput(outputs) - for i, name := range accelerators { - if strings.Contains(name, "chipset") { // skip "QAT (on chipset)" in this table - continue - } else if strings.Contains(name, "CPU") { // rename "QAT (on CPU) to simply "QAT" - name = "QAT" - } - summary = append(summary, fmt.Sprintf("%s %s [0]", name, counts[i])) - } - return strings.Join(summary, ", ") -} - func cveSummaryFromOutput(outputs map[string]script.ScriptOutput) string { cves := cveInfoFromOutput(outputs) if len(cves) == 0 { @@ -1987,80 +1498,3 @@ func sectionValueFromOutput(output string, sectionName string) string { } return sections[sectionName] } - -func javaFoldedFromOutput(outputs map[string]script.ScriptOutput) string { - sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) - if len(sections) == 0 { - slog.Warn("no sections in collapsed call stack output") - return "" - } - javaFolded := make(map[string]string) - re := regexp.MustCompile(`^async-profiler (\d+) (.*)$`) - for header, stacks := range sections { - match := re.FindStringSubmatch(header) - if match == nil { - continue - } - pid := match[1] - processName := match[2] - if stacks == "" { - slog.Warn("no stacks for java process", slog.String("header", header)) - continue - } - if strings.HasPrefix(stacks, "Failed to inject profiler") { - slog.Error("profiling data error", slog.String("header", header)) - continue - } - _, ok := javaFolded[processName] - if processName == "" { - processName = "java (" + pid + ")" - } else if ok { - processName = processName + " (" + pid + ")" - } - javaFolded[processName] = stacks - } - folded, err := mergeJavaFolded(javaFolded) - if err != nil { - slog.Error("failed to merge java stacks", slog.String("error", err.Error())) - } - return folded -} - -func nativeFoldedFromOutput(outputs map[string]script.ScriptOutput) string { - sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) - if len(sections) == 0 { - slog.Warn("no sections in collapsed call stack output") - return "" - } - var dwarfFolded, fpFolded string - for header, content := range sections { - switch header { - case "perf_dwarf": - dwarfFolded = content - case "perf_fp": - fpFolded = content - } - } - if dwarfFolded == "" && fpFolded == "" { - return "" - } - folded, err := mergeSystemFolded(fpFolded, dwarfFolded) - if err != nil { - slog.Error("failed to merge native stacks", slog.String("error", err.Error())) - } - return folded -} - -func maxRenderDepthFromOutput(outputs map[string]script.ScriptOutput) string { - sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) - if len(sections) == 0 { - slog.Warn("no sections in collapsed call stack output") - return "" - } - for header, content := range sections { - if header == "maximum depth" { - return content - } - } - return "" -} diff --git a/internal/report/table_helpers_accelerator.go b/internal/report/table_helpers_accelerator.go new file mode 100644 index 00000000..23e02800 --- /dev/null +++ b/internal/report/table_helpers_accelerator.go @@ -0,0 +1,93 @@ +package report + +import ( + "fmt" + "perfspect/internal/script" + "regexp" + "strings" +) + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +func acceleratorNames() []string { + var names []string + for _, accel := range acceleratorDefinitions { + names = append(names, accel.Name) + } + return names +} + +func acceleratorCountsFromOutput(outputs map[string]script.ScriptOutput) []string { + var counts []string + lshw := outputs[script.LshwScriptName].Stdout + for _, accel := range acceleratorDefinitions { + regex := fmt.Sprintf("%s:%s", accel.MfgID, accel.DevID) + re := regexp.MustCompile(regex) + count := len(re.FindAllString(lshw, -1)) + counts = append(counts, fmt.Sprintf("%d", count)) + } + return counts +} + +func acceleratorWorkQueuesFromOutput(outputs map[string]script.ScriptOutput) []string { + var queues []string + for _, accel := range acceleratorDefinitions { + if accel.Name == "IAA" || accel.Name == "DSA" { + var scriptName string + if accel.Name == "IAA" { + scriptName = script.IaaDevicesScriptName + } else { + scriptName = script.DsaDevicesScriptName + } + devices := outputs[scriptName].Stdout + lines := strings.Split(devices, "\n") + // get non-empty lines + var nonEmptyLines []string + for _, line := range lines { + if strings.TrimSpace(line) != "" { + nonEmptyLines = append(nonEmptyLines, line) + } + } + if len(nonEmptyLines) == 0 { + queues = append(queues, "None") + } else { + queues = append(queues, strings.Join(nonEmptyLines, ", ")) + } + } else { + queues = append(queues, "N/A") + } + } + return queues +} + +func acceleratorFullNamesFromYaml() []string { + var fullNames []string + for _, accel := range acceleratorDefinitions { + fullNames = append(fullNames, accel.FullName) + } + return fullNames +} + +func acceleratorDescriptionsFromYaml() []string { + var descriptions []string + for _, accel := range acceleratorDefinitions { + descriptions = append(descriptions, accel.Description) + } + return descriptions +} + +func acceleratorSummaryFromOutput(outputs map[string]script.ScriptOutput) string { + var summary []string + accelerators := acceleratorNames() + counts := acceleratorCountsFromOutput(outputs) + for i, name := range accelerators { + if strings.Contains(name, "chipset") { // skip "QAT (on chipset)" in this table + continue + } else if strings.Contains(name, "CPU") { // rename "QAT (on CPU) to simply "QAT" + name = "QAT" + } + summary = append(summary, fmt.Sprintf("%s %s [0]", name, counts[i])) + } + return strings.Join(summary, ", ") +} diff --git a/internal/report/table_helpers_frequency.go b/internal/report/table_helpers_frequency.go new file mode 100644 index 00000000..f71a41ae --- /dev/null +++ b/internal/report/table_helpers_frequency.go @@ -0,0 +1,502 @@ +package report + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// table_helpers_frequency.go contains helper functions for parsing and processing CPU frequency data. + +import ( + "fmt" + "log/slog" + "regexp" + "strconv" + "strings" + + "perfspect/internal/script" + "perfspect/internal/util" + + "slices" +) + +// getFrequenciesFromHex converts a hex string to a list of frequency integers. +// The frequencies are reversed to match the expected order. +func getFrequenciesFromHex(hex string) ([]int, error) { + freqs, err := util.HexToIntList(hex) + if err != nil { + return nil, err + } + // reverse the order of the frequencies + slices.Reverse(freqs) + return freqs, nil +} + +// getBucketSizesFromHex extracts bucket sizes from a hex string. +// Expects exactly 8 bucket sizes and reverses their order. +func getBucketSizesFromHex(hex string) ([]int, error) { + bucketSizes, err := util.HexToIntList(hex) + if err != nil { + return nil, err + } + if len(bucketSizes) != 8 { + err = fmt.Errorf("expected 8 bucket sizes, got %d", len(bucketSizes)) + return nil, err + } + // reverse the order of the core counts + slices.Reverse(bucketSizes) + return bucketSizes, nil +} + +// padFrequencies adds items to the frequencies slice until it reaches the desired length. +// The value of the added items is the same as the last item in the original slice. +func padFrequencies(freqs []int, desiredLength int) ([]int, error) { + if len(freqs) == 0 { + return nil, fmt.Errorf("cannot pad empty frequencies slice") + } + for len(freqs) < desiredLength { + freqs = append(freqs, freqs[len(freqs)-1]) + } + return freqs, nil +} + +// getArchMultiplier returns the die multiplier for multi-die architectures. +// Returns 1 for single-die architectures. +func getArchMultiplier(arch string) int { + if strings.Contains(arch, "SRF") || strings.Contains(arch, "CWF") { + return 4 + } else if strings.Contains(arch, "GNR_X3") { + return 3 + } else if strings.Contains(arch, "GNR_X2") { + return 2 + } + return 1 +} + +// parseFrequencyScriptOutput validates and parses the raw script output. +// Returns field names and hex values. +func parseFrequencyScriptOutput(output string) (fieldNames []string, hexValues []string, err error) { + if output == "" { + return nil, nil, fmt.Errorf("no core frequencies found") + } + + lines := strings.Split(output, "\n") + if len(lines) < 2 { + return nil, nil, fmt.Errorf("unexpected output format: need at least 2 lines") + } + + fieldNames = strings.Fields(lines[0]) + if len(fieldNames) < 2 { + return nil, nil, fmt.Errorf("unexpected output format: need at least 2 fields") + } + + hexValues = strings.Fields(lines[1]) + if len(hexValues) != len(fieldNames) { + return nil, nil, fmt.Errorf("unexpected output format: field count mismatch") + } + + return fieldNames, hexValues, nil +} + +// buildCoreBuckets creates core range strings for both total and per-die cores. +// For single-die architectures, totalCoreBuckets will be empty. +func buildCoreBuckets(bucketCoreCounts []int, archMultiplier int) (totalCoreBuckets, dieCoreBuckets []string) { + totalCoreStart := 1 + dieStart := 1 + + for _, count := range bucketCoreCounts { + if dieStart > count { + break + } + + // Build per-die bucket + dieCoreBuckets = append(dieCoreBuckets, fmt.Sprintf("%d-%d", dieStart, count)) + dieStart = count + 1 + + // Build total bucket for multi-die architectures + if archMultiplier > 1 { + totalCoreCount := count * archMultiplier + if totalCoreStart > totalCoreCount { + break + } + totalCoreBuckets = append(totalCoreBuckets, fmt.Sprintf("%d-%d", totalCoreStart, totalCoreCount)) + totalCoreStart = totalCoreCount + 1 + } + } + + return totalCoreBuckets, dieCoreBuckets +} + +// parseISAFrequencies converts hex frequency values to GHz strings. +func parseISAFrequencies(isaHex string, bucketCount int) ([]string, error) { + freqs, err := getFrequenciesFromHex(isaHex) + if err != nil { + return nil, fmt.Errorf("failed to get frequencies from hex: %w", err) + } + + // Pad if necessary to match bucket count + if len(freqs) != bucketCount { + freqs, err = padFrequencies(freqs, bucketCount) + if err != nil { + return nil, fmt.Errorf("failed to pad frequencies: %w", err) + } + } + + // Convert to GHz strings + isaFreqs := make([]string, len(freqs)) + for i, freq := range freqs { + freqGHz := float64(freq) / 10.0 + isaFreqs[i] = fmt.Sprintf("%.1f", freqGHz) + } + + return isaFreqs, nil +} + +// isISASupported checks if an ISA has non-zero frequencies. +func isISASupported(isaFreqs []string) bool { + return len(isaFreqs) > 0 && isaFreqs[0] != "0.0" +} + +// buildFrequencyTableHeader creates the header row for the frequency table. +func buildFrequencyTableHeader(fieldNames []string, allIsaFreqs [][]string, archMultiplier int) []string { + header := []string{"Cores"} + + if archMultiplier > 1 { + header = append(header, "Cores per Die") + } + + // Add ISA names for supported ISAs only + for i, isaFreqs := range allIsaFreqs { + if isISASupported(isaFreqs) { + header = append(header, strings.ToUpper(fieldNames[i+1])) + } + } + + return header +} + +// buildFrequencyTableRow creates a single data row for the frequency table. +func buildFrequencyTableRow(bucketIdx int, totalCoreBuckets, dieCoreBuckets []string, + allIsaFreqs [][]string, archMultiplier int) ([]string, error) { + + row := make([]string, 0, len(allIsaFreqs)+2) + + // Add total core bucket for multi-die architectures + if archMultiplier > 1 { + if bucketIdx >= len(totalCoreBuckets) { + return nil, fmt.Errorf("bucket index %d out of range for total core buckets", bucketIdx) + } + row = append(row, totalCoreBuckets[bucketIdx]) + } + + // Add per-die core bucket + row = append(row, dieCoreBuckets[bucketIdx]) + + // Add frequency values for supported ISAs + for _, isaFreqs := range allIsaFreqs { + if !isISASupported(isaFreqs) { + continue + } + if bucketIdx >= len(isaFreqs) { + return nil, fmt.Errorf("bucket index %d out of range for ISA frequencies", bucketIdx) + } + row = append(row, isaFreqs[bucketIdx]) + } + + return row, nil +} + +// getSpecFrequencyBuckets parses turbo frequency data and returns a formatted table. +// The table structure is: +// - First row: header with column names (Cores, [Cores per Die], ISA1, ISA2, ...) +// - Subsequent rows: frequency data for each core count bucket +// +// Example output for multi-die architecture: +// +// ["Cores", "Cores per Die", "SSE", "AVX2", "AVX512"] +// ["0-41", "0-20", "3.5", "3.5", "3.3"] +// ["42-63", "21-31", "3.5", "3.5", "3.3"] +// +// The "Cores per Die" column is only present for multi-die architectures (GNR_X2, GNR_X3, SRF, CWF). +func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string, error) { + // Get architecture to determine die multiplier + arch := UarchFromOutput(outputs) + if arch == "" { + return nil, fmt.Errorf("uarch is required") + } + archMultiplier := getArchMultiplier(arch) + + // Parse script output + fieldNames, hexValues, err := parseFrequencyScriptOutput(outputs[script.SpecCoreFrequenciesScriptName].Stdout) + if err != nil { + return nil, err + } + + // Extract bucket sizes from first hex value + bucketCoreCounts, err := getBucketSizesFromHex(hexValues[0]) + if err != nil { + return nil, fmt.Errorf("failed to get bucket sizes: %w", err) + } + + // Build core range strings + totalCoreBuckets, dieCoreBuckets := buildCoreBuckets(bucketCoreCounts, archMultiplier) + + // Parse ISA frequencies from remaining hex values + allIsaFreqs := make([][]string, 0, len(hexValues)-1) + for _, isaHex := range hexValues[1:] { + isaFreqs, err := parseISAFrequencies(isaHex, len(bucketCoreCounts)) + if err != nil { + return nil, err + } + allIsaFreqs = append(allIsaFreqs, isaFreqs) + } + + // Build output table + table := make([][]string, 0, len(dieCoreBuckets)+1) + + // Add header row + header := buildFrequencyTableHeader(fieldNames, allIsaFreqs, archMultiplier) + table = append(table, header) + + // Add data rows + for i := range dieCoreBuckets { + row, err := buildFrequencyTableRow(i, totalCoreBuckets, dieCoreBuckets, allIsaFreqs, archMultiplier) + if err != nil { + return nil, err + } + table = append(table, row) + } + + return table, nil +} + +// expandTurboFrequencies expands the turbo frequencies to a list of frequencies +// input is the output of getSpecFrequencyBuckets, e.g.: +// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx" +// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1" +// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1" +// ... +// output is the expanded list of the frequencies for the requested ISA +func expandTurboFrequencies(specFrequencyBuckets [][]string, isa string) ([]string, error) { + if len(specFrequencyBuckets) < 2 || len(specFrequencyBuckets[0]) < 2 { + return nil, fmt.Errorf("unable to parse core frequency buckets") + } + rangeIdx := 0 // the first column is the bucket, e.g., 1-44 + // find the index of the ISA column + var isaIdx int + for i := 1; i < len(specFrequencyBuckets[0]); i++ { + if strings.EqualFold(specFrequencyBuckets[0][i], isa) { + isaIdx = i + break + } + } + if isaIdx == 0 { + return nil, fmt.Errorf("unable to find %s frequency column", isa) + } + var freqs []string + for i := 1; i < len(specFrequencyBuckets); i++ { + bucketCores, err := util.IntRangeToIntList(strings.TrimSpace(specFrequencyBuckets[i][rangeIdx])) + if err != nil { + return nil, fmt.Errorf("unable to parse bucket range %s", specFrequencyBuckets[i][rangeIdx]) + } + bucketFreq := strings.TrimSpace(specFrequencyBuckets[i][isaIdx]) + if bucketFreq == "" { + return nil, fmt.Errorf("unable to parse bucket frequency %s", specFrequencyBuckets[i][isaIdx]) + } + for range bucketCores { + freqs = append(freqs, bucketFreq) + } + } + return freqs, nil +} + +// maxFrequencyFromOutput gets max core frequency +// +// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq +// 2nd option) from MSR/tpmi +// 3rd option) from dmidecode "Max Speed" +func maxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + cmdout := strings.TrimSpace(outputs[script.MaximumFrequencyScriptName].Stdout) + if cmdout != "" { + freqf, err := strconv.ParseFloat(cmdout, 64) + if err == nil { + freqf = freqf / 1000000 + return fmt.Sprintf("%.1fGHz", freqf) + } + } + // get the max frequency from the MSR/tpmi + specCoreFrequencies, err := getSpecFrequencyBuckets(outputs) + if err == nil { + sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies) + if len(sseFreqs) > 0 { + // max (single-core) frequency is the first SSE frequency + return sseFreqs[0] + "GHz" + } + } + return valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Max Speed:\s(.*)`) +} + +// getSSEFreqsFromBuckets extracts SSE frequency values from frequency buckets. +func getSSEFreqsFromBuckets(buckets [][]string) []string { + if len(buckets) < 2 { + return nil + } + // find the SSE column + sseColumn := -1 + for i, col := range buckets[0] { + if strings.ToUpper(col) == "SSE" { + sseColumn = i + break + } + } + if sseColumn == -1 { + return nil + } + // get the SSE values from the buckets + sse := make([]string, 0, len(buckets)-1) + for i := 1; i < len(buckets); i++ { + if len(buckets[i]) > sseColumn { + sse = append(sse, buckets[i][sseColumn]) + } + } + return sse +} + +// allCoreMaxFrequencyFromOutput gets the all-core max frequency. +func allCoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + specCoreFrequencies, err := getSpecFrequencyBuckets(outputs) + if err != nil { + return "" + } + sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies) + if len(sseFreqs) < 1 { + return "" + } + // all core max frequency is the last SSE frequency + return sseFreqs[len(sseFreqs)-1] + "GHz" +} + +// baseFrequencyFromOutput gets base core frequency +// +// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/base_frequency +// 2nd option) from dmidecode "Current Speed" +// 3nd option) parse it from the model name +func baseFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + cmdout := strings.TrimSpace(outputs[script.BaseFrequencyScriptName].Stdout) + if cmdout != "" { + freqf, err := strconv.ParseFloat(cmdout, 64) + if err == nil { + freqf = freqf / 1000000 + return fmt.Sprintf("%.1fGHz", freqf) + } + } + currentSpeedVal := valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Current Speed:\s(.*)$`) + tokens := strings.Split(currentSpeedVal, " ") + if len(tokens) == 2 { + num, err := strconv.ParseFloat(tokens[0], 64) + if err == nil { + unit := tokens[1] + if unit == "MHz" { + num = num / 1000 + unit = "GHz" + } + return fmt.Sprintf("%.1f%s", num, unit) + } + } + // the frequency (if included) is at the end of the model name in lscpu's output + modelName := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^[Mm]odel name.*:\s*(.+?)$`) + tokens = strings.Split(modelName, " ") + if len(tokens) > 0 { + lastToken := tokens[len(tokens)-1] + if len(lastToken) > 0 && lastToken[len(lastToken)-1] == 'z' { + return lastToken + } + } + return "" +} + +func uncoreMinMaxDieFrequencyFromOutput(maxFreq bool, computeDie bool, outputs map[string]script.ScriptOutput) string { + // find the first die that matches requrested die type (compute or I/O) + re := regexp.MustCompile(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry (\d+) in instance (\d+)`) + var instance, entry string + found := false + for line := range strings.SplitSeq(outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout, "\n") { + match := re.FindStringSubmatch(line) + if match == nil { + continue + } + if computeDie && match[1] == "0" { + found = true + entry = match[2] + instance = match[3] + break + } + if !computeDie && match[1] == "1" { + found = true + entry = match[2] + instance = match[3] + break + } + } + if !found { + slog.Error("failed to find uncore die type in TPMI output", slog.String("output", outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout)) + return "" + } + // get the frequency for the found die + re = regexp.MustCompile(fmt.Sprintf(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry %s in instance %s`, entry, instance)) + found = false + var parsed int64 + var err error + var scriptName string + if maxFreq { + scriptName = script.UncoreMaxFromTPMIScriptName + } else { + scriptName = script.UncoreMinFromTPMIScriptName + } + for line := range strings.SplitSeq(outputs[scriptName].Stdout, "\n") { + match := re.FindStringSubmatch(line) + if len(match) > 0 { + found = true + parsed, err = strconv.ParseInt(match[1], 10, 64) + if err != nil { + slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("line", line)) + return "" + } + break + } + } + if !found { + slog.Error("failed to find uncore frequency in TPMI output", slog.String("output", outputs[scriptName].Stdout)) + return "" + } + return fmt.Sprintf("%.1fGHz", float64(parsed)/10) +} + +func uncoreMinMaxFrequencyFromOutput(maxFreq bool, outputs map[string]script.ScriptOutput) string { + var parsed int64 + var err error + var scriptName string + if maxFreq { + scriptName = script.UncoreMaxFromMSRScriptName + } else { + scriptName = script.UncoreMinFromMSRScriptName + } + hex := strings.TrimSpace(outputs[scriptName].Stdout) + if hex != "" && hex != "0" { + parsed, err = strconv.ParseInt(hex, 16, 64) + if err != nil { + slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("hex", hex)) + return "" + } + } else { + slog.Warn("failed to get uncore frequency from MSR", slog.String("hex", hex)) + return "" + } + return fmt.Sprintf("%.1fGHz", float64(parsed)/10) +} + +func uncoreMinFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + return uncoreMinMaxFrequencyFromOutput(false, outputs) +} + +func uncoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + return uncoreMinMaxFrequencyFromOutput(true, outputs) +} diff --git a/internal/report/table_helpers_frequency_test.go b/internal/report/table_helpers_frequency_test.go new file mode 100644 index 00000000..21a9cf7c --- /dev/null +++ b/internal/report/table_helpers_frequency_test.go @@ -0,0 +1,207 @@ +package report + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "reflect" + "testing" +) + +func TestGetFrequenciesFromHex(t *testing.T) { + tests := []struct { + name string + msr string + want []int + expectErr bool + }{ + { + name: "Valid MSR with multiple frequencies", + msr: "0x1A2B3C4D", + want: []int{0x4D, 0x3C, 0x2B, 0x1A}, + expectErr: false, + }, + { + name: "Valid MSR with single frequency", + msr: "0x1A", + want: []int{0x1A}, + expectErr: false, + }, + { + name: "Empty MSR string", + msr: "", + want: nil, + expectErr: true, + }, + { + name: "Invalid MSR string", + msr: "invalid_hex", + want: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getFrequenciesFromHex(tt.msr) + if (err != nil) != tt.expectErr { + t.Errorf("getFrequenciesFromMSR() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getFrequenciesFromMSR() = %v, want %v", got, tt.want) + } + }) + } +} +func TestGetBucketSizesFromHex(t *testing.T) { + tests := []struct { + name string + msr string + want []int + expectErr bool + }{ + { + name: "Valid MSR with 8 bucket sizes", + msr: "0x0102030405060708", + want: []int{8, 7, 6, 5, 4, 3, 2, 1}, + expectErr: false, + }, + { + name: "Valid MSR with reversed order", + msr: "0x0807060504030201", + want: []int{1, 2, 3, 4, 5, 6, 7, 8}, + expectErr: false, + }, + { + name: "Invalid MSR string", + msr: "invalid_hex", + want: nil, + expectErr: true, + }, + { + name: "MSR with less than 8 bucket sizes", + msr: "0x01020304", + want: nil, + expectErr: true, + }, + { + name: "MSR with more than 8 bucket sizes", + msr: "0x010203040506070809", + want: nil, + expectErr: true, + }, + { + name: "Empty MSR string", + msr: "", + want: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getBucketSizesFromHex(tt.msr) + if (err != nil) != tt.expectErr { + t.Errorf("getBucketSizesFromMSR() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getBucketSizesFromMSR() = %v, want %v", got, tt.want) + } + }) + } +} +func TestExpandTurboFrequencies(t *testing.T) { + tests := []struct { + name string + buckets [][]string + isa string + want []string + expectErr bool + }{ + { + name: "Valid input with single bucket", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "3.5", "3.2"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.5", "3.5"}, + expectErr: false, + }, + { + name: "Valid input with multiple buckets", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-2", "3.5", "3.2"}, + {"3-4", "3.6", "3.3"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.6", "3.6"}, + expectErr: false, + }, + { + name: "ISA column not found", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "3.5", "3.2"}, + }, + isa: "AVX512", + want: nil, + expectErr: true, + }, + { + name: "Empty buckets", + buckets: [][]string{ + {}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Invalid bucket range", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-", "3.5", "3.2"}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Empty frequency value", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "", "3.2"}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Whitespace in bucket range", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {" 1-4 ", "3.5", "3.2"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.5", "3.5"}, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := expandTurboFrequencies(tt.buckets, tt.isa) + if (err != nil) != tt.expectErr { + t.Errorf("expandTurboFrequencies() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("expandTurboFrequencies() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/report/table_helpers_nic_integration_test.go b/internal/report/table_helpers_nic_integration_test.go deleted file mode 100644 index 591cffe6..00000000 --- a/internal/report/table_helpers_nic_integration_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package report - -// Copyright (C) 2021-2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause - -import ( - "testing" - - "perfspect/internal/script" -) - -func TestParseNicInfoWithCardPort(t *testing.T) { - // Sample output simulating the scenario from the issue - sampleOutput := `Interface: eth2 -Vendor ID: 8086 -Model ID: 1593 -Vendor: Intel Corporation -Model: Ethernet Controller 10G X550T -Speed: 1000Mb/s -Link detected: yes -bus-info: 0000:32:00.0 -driver: ixgbe -version: 5.1.0-k -firmware-version: 0x800009e0 -MAC Address: aa:bb:cc:dd:ee:00 -NUMA Node: 0 -CPU Affinity: -IRQ Balance: Enabled -rx-usecs: 1 -tx-usecs: 1 -Adaptive RX: off TX: off ----------------------------------------- -Interface: eth3 -Vendor ID: 8086 -Model ID: 1593 -Vendor: Intel Corporation -Model: Ethernet Controller 10G X550T -Speed: Unknown! -Link detected: no -bus-info: 0000:32:00.1 -driver: ixgbe -version: 5.1.0-k -firmware-version: 0x800009e0 -MAC Address: aa:bb:cc:dd:ee:01 -NUMA Node: 0 -CPU Affinity: -IRQ Balance: Enabled -rx-usecs: 1 -tx-usecs: 1 -Adaptive RX: off TX: off ----------------------------------------- -Interface: eth0 -Vendor ID: 8086 -Model ID: 37d2 -Vendor: Intel Corporation -Model: Ethernet Controller E810-C for QSFP -Speed: 100000Mb/s -Link detected: yes -bus-info: 0000:c0:00.0 -driver: ice -version: K_5.19.0-41-generic_5.1.9 -firmware-version: 4.40 0x8001c967 1.3534.0 -MAC Address: aa:bb:cc:dd:ee:82 -NUMA Node: 1 -CPU Affinity: -IRQ Balance: Enabled -rx-usecs: 1 -tx-usecs: 1 -Adaptive RX: off TX: off ----------------------------------------- -Interface: eth1 -Vendor ID: 8086 -Model ID: 37d2 -Vendor: Intel Corporation -Model: Ethernet Controller E810-C for QSFP -Speed: 100000Mb/s -Link detected: yes -bus-info: 0000:c0:00.1 -driver: ice -version: K_5.19.0-41-generic_5.1.9 -firmware-version: 4.40 0x8001c967 1.3534.0 -MAC Address: aa:bb:cc:dd:ee:83 -NUMA Node: 1 -CPU Affinity: -IRQ Balance: Enabled -rx-usecs: 1 -tx-usecs: 1 -Adaptive RX: off TX: off -----------------------------------------` - - nics := parseNicInfo(sampleOutput) - - if len(nics) != 4 { - t.Fatalf("Expected 4 NICs, got %d", len(nics)) - } - - // Expected card/port assignments based on the issue example - expectedCardPort := map[string]struct { - card string - port string - }{ - "eth2": {"1", "1"}, // 0000:32:00.0 - "eth3": {"1", "2"}, // 0000:32:00.1 - "eth0": {"2", "1"}, // 0000:c0:00.0 - "eth1": {"2", "2"}, // 0000:c0:00.1 - } - - for _, nic := range nics { - expected, exists := expectedCardPort[nic.Name] - if !exists { - t.Errorf("Unexpected NIC name: %s", nic.Name) - continue - } - if nic.Card != expected.card { - t.Errorf("NIC %s: expected card %s, got %s", nic.Name, expected.card, nic.Card) - } - if nic.Port != expected.port { - t.Errorf("NIC %s: expected port %s, got %s", nic.Name, expected.port, nic.Port) - } - } -} - -func TestNicTableValuesWithCardPort(t *testing.T) { - // Sample output simulating the scenario from the issue - sampleOutput := `Interface: eth2 -bus-info: 0000:32:00.0 -Vendor: Intel Corporation -Model: Ethernet Controller 10G X550T -Speed: 1000Mb/s -Link detected: yes ----------------------------------------- -Interface: eth3 -bus-info: 0000:32:00.1 -Vendor: Intel Corporation -Model: Ethernet Controller 10G X550T -Speed: Unknown! -Link detected: no ----------------------------------------- -Interface: eth0 -bus-info: 0000:c0:00.0 -Vendor: Intel Corporation -Model: Ethernet Controller E810-C for QSFP -Speed: 100000Mb/s -Link detected: yes ----------------------------------------- -Interface: eth1 -bus-info: 0000:c0:00.1 -Vendor: Intel Corporation -Model: Ethernet Controller E810-C for QSFP -Speed: 100000Mb/s -Link detected: yes -----------------------------------------` - - outputs := map[string]script.ScriptOutput{ - script.NicInfoScriptName: {Stdout: sampleOutput}, - } - - fields := nicTableValues(outputs) - - // Find the "Card / Port" field - var cardPortField Field - found := false - for _, field := range fields { - if field.Name == "Card / Port" { - cardPortField = field - found = true - break - } - } - - if !found { - t.Fatal("Card / Port field not found in NIC table") - } - - // Verify we have 4 entries - if len(cardPortField.Values) != 4 { - t.Fatalf("Expected 4 Card / Port values, got %d", len(cardPortField.Values)) - } - - // Find the Name field to match values - var nameField Field - for _, field := range fields { - if field.Name == "Name" { - nameField = field - break - } - } - - // Verify card/port assignments - expectedCardPort := map[string]string{ - "eth2": "1 / 1", - "eth3": "1 / 2", - "eth0": "2 / 1", - "eth1": "2 / 2", - } - - for i, name := range nameField.Values { - expected := expectedCardPort[name] - actual := cardPortField.Values[i] - if actual != expected { - t.Errorf("NIC %s: expected Card / Port %q, got %q", name, expected, actual) - } - } -} diff --git a/internal/report/table_helpers_nic_test.go b/internal/report/table_helpers_nic_test.go index a5742b44..60651b36 100644 --- a/internal/report/table_helpers_nic_test.go +++ b/internal/report/table_helpers_nic_test.go @@ -4,6 +4,7 @@ package report // SPDX-License-Identifier: BSD-3-Clause import ( + "perfspect/internal/script" "testing" ) @@ -102,3 +103,197 @@ func TestExtractFunction(t *testing.T) { }) } } + +func TestParseNicInfoWithCardPort(t *testing.T) { + // Sample output simulating the scenario from the issue + sampleOutput := `Interface: eth2 +Vendor ID: 8086 +Model ID: 1593 +Vendor: Intel Corporation +Model: Ethernet Controller 10G X550T +Speed: 1000Mb/s +Link detected: yes +bus-info: 0000:32:00.0 +driver: ixgbe +version: 5.1.0-k +firmware-version: 0x800009e0 +MAC Address: aa:bb:cc:dd:ee:00 +NUMA Node: 0 +CPU Affinity: +IRQ Balance: Enabled +rx-usecs: 1 +tx-usecs: 1 +Adaptive RX: off TX: off +---------------------------------------- +Interface: eth3 +Vendor ID: 8086 +Model ID: 1593 +Vendor: Intel Corporation +Model: Ethernet Controller 10G X550T +Speed: Unknown! +Link detected: no +bus-info: 0000:32:00.1 +driver: ixgbe +version: 5.1.0-k +firmware-version: 0x800009e0 +MAC Address: aa:bb:cc:dd:ee:01 +NUMA Node: 0 +CPU Affinity: +IRQ Balance: Enabled +rx-usecs: 1 +tx-usecs: 1 +Adaptive RX: off TX: off +---------------------------------------- +Interface: eth0 +Vendor ID: 8086 +Model ID: 37d2 +Vendor: Intel Corporation +Model: Ethernet Controller E810-C for QSFP +Speed: 100000Mb/s +Link detected: yes +bus-info: 0000:c0:00.0 +driver: ice +version: K_5.19.0-41-generic_5.1.9 +firmware-version: 4.40 0x8001c967 1.3534.0 +MAC Address: aa:bb:cc:dd:ee:82 +NUMA Node: 1 +CPU Affinity: +IRQ Balance: Enabled +rx-usecs: 1 +tx-usecs: 1 +Adaptive RX: off TX: off +---------------------------------------- +Interface: eth1 +Vendor ID: 8086 +Model ID: 37d2 +Vendor: Intel Corporation +Model: Ethernet Controller E810-C for QSFP +Speed: 100000Mb/s +Link detected: yes +bus-info: 0000:c0:00.1 +driver: ice +version: K_5.19.0-41-generic_5.1.9 +firmware-version: 4.40 0x8001c967 1.3534.0 +MAC Address: aa:bb:cc:dd:ee:83 +NUMA Node: 1 +CPU Affinity: +IRQ Balance: Enabled +rx-usecs: 1 +tx-usecs: 1 +Adaptive RX: off TX: off +----------------------------------------` + + nics := parseNicInfo(sampleOutput) + + if len(nics) != 4 { + t.Fatalf("Expected 4 NICs, got %d", len(nics)) + } + + // Expected card/port assignments based on the issue example + expectedCardPort := map[string]struct { + card string + port string + }{ + "eth2": {"1", "1"}, // 0000:32:00.0 + "eth3": {"1", "2"}, // 0000:32:00.1 + "eth0": {"2", "1"}, // 0000:c0:00.0 + "eth1": {"2", "2"}, // 0000:c0:00.1 + } + + for _, nic := range nics { + expected, exists := expectedCardPort[nic.Name] + if !exists { + t.Errorf("Unexpected NIC name: %s", nic.Name) + continue + } + if nic.Card != expected.card { + t.Errorf("NIC %s: expected card %s, got %s", nic.Name, expected.card, nic.Card) + } + if nic.Port != expected.port { + t.Errorf("NIC %s: expected port %s, got %s", nic.Name, expected.port, nic.Port) + } + } +} + +func TestNicTableValuesWithCardPort(t *testing.T) { + // Sample output simulating the scenario from the issue + sampleOutput := `Interface: eth2 +bus-info: 0000:32:00.0 +Vendor: Intel Corporation +Model: Ethernet Controller 10G X550T +Speed: 1000Mb/s +Link detected: yes +---------------------------------------- +Interface: eth3 +bus-info: 0000:32:00.1 +Vendor: Intel Corporation +Model: Ethernet Controller 10G X550T +Speed: Unknown! +Link detected: no +---------------------------------------- +Interface: eth0 +bus-info: 0000:c0:00.0 +Vendor: Intel Corporation +Model: Ethernet Controller E810-C for QSFP +Speed: 100000Mb/s +Link detected: yes +---------------------------------------- +Interface: eth1 +bus-info: 0000:c0:00.1 +Vendor: Intel Corporation +Model: Ethernet Controller E810-C for QSFP +Speed: 100000Mb/s +Link detected: yes +----------------------------------------` + + outputs := map[string]script.ScriptOutput{ + script.NicInfoScriptName: {Stdout: sampleOutput}, + } + + fields := nicTableValues(outputs) + + // Find the "Card / Port" field + var cardPortField Field + found := false + for _, field := range fields { + if field.Name == "Card / Port" { + cardPortField = field + found = true + break + } + } + + if !found { + t.Fatal("Card / Port field not found in NIC table") + } + + // Verify we have 4 entries + if len(cardPortField.Values) != 4 { + t.Fatalf("Expected 4 Card / Port values, got %d", len(cardPortField.Values)) + } + + // Find the Name field to match values + var nameField Field + for _, field := range fields { + if field.Name == "Name" { + nameField = field + break + } + } + + // Verify card/port assignments + expectedCardPort := map[string]string{ + "eth2": "1 / 1", + "eth3": "1 / 2", + "eth0": "2 / 1", + "eth1": "2 / 2", + } + + for i, name := range nameField.Values { + expected := expectedCardPort[name] + actual := cardPortField.Values[i] + if actual != expected { + t.Errorf("NIC %s: expected Card / Port %q, got %q", name, expected, actual) + } + } +} diff --git a/internal/report/table_helpers_stacks.go b/internal/report/table_helpers_stacks.go index 2b9554eb..f0091043 100644 --- a/internal/report/table_helpers_stacks.go +++ b/internal/report/table_helpers_stacks.go @@ -5,7 +5,9 @@ package report import ( "fmt" + "log/slog" "math" + "perfspect/internal/script" "regexp" "strconv" "strings" @@ -163,3 +165,80 @@ func mergeSystemFolded(perfFp string, perfDwarf string) (merged string, err erro merged = mergedStacks.dumpFolded() return } + +func javaFoldedFromOutput(outputs map[string]script.ScriptOutput) string { + sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) + if len(sections) == 0 { + slog.Warn("no sections in collapsed call stack output") + return "" + } + javaFolded := make(map[string]string) + re := regexp.MustCompile(`^async-profiler (\d+) (.*)$`) + for header, stacks := range sections { + match := re.FindStringSubmatch(header) + if match == nil { + continue + } + pid := match[1] + processName := match[2] + if stacks == "" { + slog.Warn("no stacks for java process", slog.String("header", header)) + continue + } + if strings.HasPrefix(stacks, "Failed to inject profiler") { + slog.Error("profiling data error", slog.String("header", header)) + continue + } + _, ok := javaFolded[processName] + if processName == "" { + processName = "java (" + pid + ")" + } else if ok { + processName = processName + " (" + pid + ")" + } + javaFolded[processName] = stacks + } + folded, err := mergeJavaFolded(javaFolded) + if err != nil { + slog.Error("failed to merge java stacks", slog.String("error", err.Error())) + } + return folded +} + +func nativeFoldedFromOutput(outputs map[string]script.ScriptOutput) string { + sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) + if len(sections) == 0 { + slog.Warn("no sections in collapsed call stack output") + return "" + } + var dwarfFolded, fpFolded string + for header, content := range sections { + switch header { + case "perf_dwarf": + dwarfFolded = content + case "perf_fp": + fpFolded = content + } + } + if dwarfFolded == "" && fpFolded == "" { + return "" + } + folded, err := mergeSystemFolded(fpFolded, dwarfFolded) + if err != nil { + slog.Error("failed to merge native stacks", slog.String("error", err.Error())) + } + return folded +} + +func maxRenderDepthFromOutput(outputs map[string]script.ScriptOutput) string { + sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) + if len(sections) == 0 { + slog.Warn("no sections in collapsed call stack output") + return "" + } + for header, content := range sections { + if header == "maximum depth" { + return content + } + } + return "" +} diff --git a/internal/report/table_helpers_test.go b/internal/report/table_helpers_test.go index b088666e..45e19f85 100644 --- a/internal/report/table_helpers_test.go +++ b/internal/report/table_helpers_test.go @@ -267,203 +267,6 @@ On-line CPU(s) list: 0-7,32-39 } } -func TestGetFrequenciesFromMSR(t *testing.T) { - tests := []struct { - name string - msr string - want []int - expectErr bool - }{ - { - name: "Valid MSR with multiple frequencies", - msr: "0x1A2B3C4D", - want: []int{0x4D, 0x3C, 0x2B, 0x1A}, - expectErr: false, - }, - { - name: "Valid MSR with single frequency", - msr: "0x1A", - want: []int{0x1A}, - expectErr: false, - }, - { - name: "Empty MSR string", - msr: "", - want: nil, - expectErr: true, - }, - { - name: "Invalid MSR string", - msr: "invalid_hex", - want: nil, - expectErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := getFrequenciesFromHex(tt.msr) - if (err != nil) != tt.expectErr { - t.Errorf("getFrequenciesFromMSR() error = %v, expectErr %v", err, tt.expectErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("getFrequenciesFromMSR() = %v, want %v", got, tt.want) - } - }) - } -} -func TestGetBucketSizesFromMSR(t *testing.T) { - tests := []struct { - name string - msr string - want []int - expectErr bool - }{ - { - name: "Valid MSR with 8 bucket sizes", - msr: "0x0102030405060708", - want: []int{8, 7, 6, 5, 4, 3, 2, 1}, - expectErr: false, - }, - { - name: "Valid MSR with reversed order", - msr: "0x0807060504030201", - want: []int{1, 2, 3, 4, 5, 6, 7, 8}, - expectErr: false, - }, - { - name: "Invalid MSR string", - msr: "invalid_hex", - want: nil, - expectErr: true, - }, - { - name: "MSR with less than 8 bucket sizes", - msr: "0x01020304", - want: nil, - expectErr: true, - }, - { - name: "MSR with more than 8 bucket sizes", - msr: "0x010203040506070809", - want: nil, - expectErr: true, - }, - { - name: "Empty MSR string", - msr: "", - want: nil, - expectErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := getBucketSizesFromHex(tt.msr) - if (err != nil) != tt.expectErr { - t.Errorf("getBucketSizesFromMSR() error = %v, expectErr %v", err, tt.expectErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("getBucketSizesFromMSR() = %v, want %v", got, tt.want) - } - }) - } -} -func TestExpandTurboFrequencies(t *testing.T) { - tests := []struct { - name string - buckets [][]string - isa string - want []string - expectErr bool - }{ - { - name: "Valid input with single bucket", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-4", "3.5", "3.2"}, - }, - isa: "SSE", - want: []string{"3.5", "3.5", "3.5", "3.5"}, - expectErr: false, - }, - { - name: "Valid input with multiple buckets", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-2", "3.5", "3.2"}, - {"3-4", "3.6", "3.3"}, - }, - isa: "SSE", - want: []string{"3.5", "3.5", "3.6", "3.6"}, - expectErr: false, - }, - { - name: "ISA column not found", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-4", "3.5", "3.2"}, - }, - isa: "AVX512", - want: nil, - expectErr: true, - }, - { - name: "Empty buckets", - buckets: [][]string{ - {}, - }, - isa: "SSE", - want: nil, - expectErr: true, - }, - { - name: "Invalid bucket range", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-", "3.5", "3.2"}, - }, - isa: "SSE", - want: nil, - expectErr: true, - }, - { - name: "Empty frequency value", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-4", "", "3.2"}, - }, - isa: "SSE", - want: nil, - expectErr: true, - }, - { - name: "Whitespace in bucket range", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {" 1-4 ", "3.5", "3.2"}, - }, - isa: "SSE", - want: []string{"3.5", "3.5", "3.5", "3.5"}, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := expandTurboFrequencies(tt.buckets, tt.isa) - if (err != nil) != tt.expectErr { - t.Errorf("expandTurboFrequencies() error = %v, expectErr %v", err, tt.expectErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("expandTurboFrequencies() = %v, want %v", got, tt.want) - } - }) - } -} func TestGetSectionsFromOutput(t *testing.T) { tests := []struct { name string From 716954c636ddf168fa8451882e180af9d34cdb10 Mon Sep 17 00:00:00 2001 From: Jason Harper <78619061+harp-intel@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:12:36 -0800 Subject: [PATCH 3/5] prepare for 3.12.0 release (#552) Signed-off-by: Harper, Jason M --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index afad8186..92536a9e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.11.0 +3.12.0 From ac8181575316537d7d900dd2f291be30e22de3b6 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 12 Nov 2025 13:28:59 -0800 Subject: [PATCH 4/5] fix: improve error handling and input validation Signed-off-by: Harper, Jason M --- internal/report/table_helpers.go | 74 +++++++++++++++++++------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index 63c84e0d..b9e94f21 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -303,18 +303,10 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string for _, isaHex := range values[1:] { var isaFreqs []string var freqs []int - if isaHex != "0" { - var err error - freqs, err = getFrequenciesFromHex(isaHex) - if err != nil { - return nil, fmt.Errorf("failed to get frequencies from Hex string: %w", err) - } - } else { - // if the ISA is not supported, set the frequency to zero for all buckets - freqs = make([]int, len(bucketCoreCounts)) - for i := range freqs { - freqs[i] = 0 - } + var err error + freqs, err = getFrequenciesFromHex(isaHex) + if err != nil { + return nil, fmt.Errorf("failed to get frequencies from Hex string: %w", err) } if len(freqs) != len(bucketCoreCounts) { freqs, err = padFrequencies(freqs, len(bucketCoreCounts)) @@ -339,6 +331,9 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string } // add fieldNames for ISAs that have frequencies for i := range allIsaFreqs { + if len(allIsaFreqs[i]) < 1 { + return nil, fmt.Errorf("no frequencies found for isa") + } if allIsaFreqs[i][0] == "0.0" { continue } @@ -348,20 +343,25 @@ func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string row := make([]string, 0, len(allIsaFreqs)+2) // add the total core buckets for multi-die architectures if archMultiplier > 1 { + if i >= len(totalCoreBuckets) { + return nil, fmt.Errorf("index out of range for total core buckets") + } row = append(row, totalCoreBuckets[i]) } // add the die core buckets row = append(row, bucket) // add the frequencies for each ISA for _, isaFreqs := range allIsaFreqs { + if len(isaFreqs) < 1 { + return nil, fmt.Errorf("no frequencies found for isa") + } if isaFreqs[0] == "0.0" { continue - } else { - if i >= len(isaFreqs) { - return nil, fmt.Errorf("index out of range for isa frequencies") - } - row = append(row, isaFreqs[i]) } + if i >= len(isaFreqs) { + return nil, fmt.Errorf("index out of range for isa frequencies") + } + row = append(row, isaFreqs[i]) } specCoreFreqs = append(specCoreFreqs, row) } @@ -895,7 +895,11 @@ func elcFieldValuesFromOutput(outputs map[string]script.ScriptOutput) (fieldValu values := []string{} // value rows for _, row := range rows[1:] { - values = append(values, row[fieldNamesIndex]) + if fieldNamesIndex < len(row) { + values = append(values, row[fieldNamesIndex]) + } else { + values = append(values, "") + } } fieldValues = append(fieldValues, Field{Name: fieldName, Values: values}) } @@ -1010,12 +1014,16 @@ func eppFromOutput(outputs map[string]script.ScriptOutput) string { } // check if the epp valid bit is set and consistent across all cores var eppValid string - for i, line := range strings.Split(outputs[script.EppValidScriptName].Stdout, "\n") { // MSR 0x774, bit 60 + for line := range strings.SplitSeq(outputs[script.EppValidScriptName].Stdout, "\n") { // MSR 0x774, bit 60 if line == "" { continue } - currentEpbValid := strings.TrimSpace(strings.Split(line, ":")[1]) - if i == 0 { + parts := strings.Split(line, ":") + if len(parts) < 2 { + continue + } + currentEpbValid := strings.TrimSpace(parts[1]) + if eppValid == "" { eppValid = currentEpbValid continue } @@ -1026,12 +1034,16 @@ func eppFromOutput(outputs map[string]script.ScriptOutput) string { } // check if epp package control bit is set and consistent across all cores var eppPkgCtrl string - for i, line := range strings.Split(outputs[script.EppPackageControlScriptName].Stdout, "\n") { // MSR 0x774, bit 42 + for line := range strings.SplitSeq(outputs[script.EppPackageControlScriptName].Stdout, "\n") { // MSR 0x774, bit 42 if line == "" { continue } - currentEppPkgCtrl := strings.TrimSpace(strings.Split(line, ":")[1]) - if i == 0 { + parts := strings.Split(line, ":") + if len(parts) < 2 { + continue + } + currentEppPkgCtrl := strings.TrimSpace(parts[1]) + if eppPkgCtrl == "" { eppPkgCtrl = currentEppPkgCtrl continue } @@ -1050,12 +1062,16 @@ func eppFromOutput(outputs map[string]script.ScriptOutput) string { return eppValToLabel(int(msr)) } else { var epp string - for i, line := range strings.Split(outputs[script.EppScriptName].Stdout, "\n") { // MSR 0x774, bits 24-31 (per-core) + for line := range strings.SplitSeq(outputs[script.EppScriptName].Stdout, "\n") { // MSR 0x774, bits 24-31 (per-core) if line == "" { continue } - currentEpp := strings.TrimSpace(strings.Split(line, ":")[1]) - if i == 0 { + parts := strings.Split(line, ":") + if len(parts) < 2 { + continue + } + currentEpp := strings.TrimSpace(parts[1]) + if epp == "" { epp = currentEpp continue } @@ -1461,7 +1477,7 @@ func filesystemFieldValuesFromOutput(outputs map[string]script.ScriptOutput) []F } fields := strings.Fields(line) // "Mounted On" gets split into two fields, rejoin - if i == 0 && fields[len(fields)-2] == "Mounted" && fields[len(fields)-1] == "on" { + if i == 0 && len(fields) >= 2 && fields[len(fields)-2] == "Mounted" && fields[len(fields)-1] == "on" { fields[len(fields)-2] = "Mounted on" fields = fields[:len(fields)-1] for _, field := range fields { @@ -1720,7 +1736,7 @@ func getPCIDevices(class string, outputs map[string]script.ScriptOutput) (device continue } match := re.FindStringSubmatch(line) - if len(match) > 0 { + if len(match) >= 3 { key := match[1] value := match[2] device[key] = value From dde305571421709de4b17bb74c752b9fdf7afdb8 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Thu, 13 Nov 2025 17:03:36 -0800 Subject: [PATCH 5/5] refactor and move frequency functions into new source file, plus more Signed-off-by: Harper, Jason M --- internal/report/table_helpers.go | 568 +----------------- internal/report/table_helpers_accelerator.go | 93 +++ internal/report/table_helpers_frequency.go | 502 ++++++++++++++++ .../report/table_helpers_frequency_test.go | 207 +++++++ .../table_helpers_nic_integration_test.go | 204 ------- internal/report/table_helpers_nic_test.go | 195 ++++++ internal/report/table_helpers_stacks.go | 79 +++ internal/report/table_helpers_test.go | 197 ------ 8 files changed, 1077 insertions(+), 968 deletions(-) create mode 100644 internal/report/table_helpers_accelerator.go create mode 100644 internal/report/table_helpers_frequency.go create mode 100644 internal/report/table_helpers_frequency_test.go delete mode 100644 internal/report/table_helpers_nic_integration_test.go diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index b9e94f21..23c74f29 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -152,326 +152,7 @@ func UarchFromOutput(outputs map[string]script.ScriptOutput) string { return "" } -// baseFrequencyFromOutput gets base core frequency -// -// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/base_frequency -// 2nd option) from dmidecode "Current Speed" -// 3nd option) parse it from the model name -func baseFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - cmdout := strings.TrimSpace(outputs[script.BaseFrequencyScriptName].Stdout) - if cmdout != "" { - freqf, err := strconv.ParseFloat(cmdout, 64) - if err == nil { - freqf = freqf / 1000000 - return fmt.Sprintf("%.1fGHz", freqf) - } - } - currentSpeedVal := valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Current Speed:\s(.*)$`) - tokens := strings.Split(currentSpeedVal, " ") - if len(tokens) == 2 { - num, err := strconv.ParseFloat(tokens[0], 64) - if err == nil { - unit := tokens[1] - if unit == "MHz" { - num = num / 1000 - unit = "GHz" - } - return fmt.Sprintf("%.1f%s", num, unit) - } - } - // the frequency (if included) is at the end of the model name in lscpu's output - modelName := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^[Mm]odel name.*:\s*(.+?)$`) - tokens = strings.Split(modelName, " ") - if len(tokens) > 0 { - lastToken := tokens[len(tokens)-1] - if len(lastToken) > 0 && lastToken[len(lastToken)-1] == 'z' { - return lastToken - } - } - return "" -} - -// getFrequenciesFromHex -func getFrequenciesFromHex(hex string) ([]int, error) { - freqs, err := util.HexToIntList(hex) - if err != nil { - return nil, err - } - // reverse the order of the frequencies - slices.Reverse(freqs) - return freqs, nil -} - -// getBucketSizesFromHex -func getBucketSizesFromHex(hex string) ([]int, error) { - bucketSizes, err := util.HexToIntList(hex) - if err != nil { - return nil, err - } - if len(bucketSizes) != 8 { - err = fmt.Errorf("expected 8 bucket sizes, got %d", len(bucketSizes)) - return nil, err - } - // reverse the order of the core counts - slices.Reverse(bucketSizes) - return bucketSizes, nil -} - -// padFrequencies adds items to the frequencies slice until it reaches the desired length. -// The value of the added items is the same as the last item in the original slice. -func padFrequencies(freqs []int, desiredLength int) ([]int, error) { - if len(freqs) == 0 { - return nil, fmt.Errorf("cannot pad empty frequencies slice") - } - for len(freqs) < desiredLength { - freqs = append(freqs, freqs[len(freqs)-1]) - } - return freqs, nil -} - -// getSpecFrequencyBuckets -// returns slice of rows -// first row is header -// each row is a slice of strings -// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx" -// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1" -// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1" -// "64-85", "32-43", "3.5", "3.5", "3.3", "3.2", "3.1" -// ... -// the "cores per die" column is only present for some architectures -func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string, error) { - arch := UarchFromOutput(outputs) - if arch == "" { - return nil, fmt.Errorf("uarch is required") - } - out := outputs[script.SpecCoreFrequenciesScriptName].Stdout - // expected script output format, the number of fields may vary: - // "cores sse avx2 avx512 avx512h amx" - // "hex hex hex hex hex hex" - if out == "" { - return nil, fmt.Errorf("no core frequencies found") - } - lines := strings.Split(out, "\n") - if len(lines) < 2 { - return nil, fmt.Errorf("unexpected output format") - } - fieldNames := strings.Fields(lines[0]) - if len(fieldNames) < 2 { - return nil, fmt.Errorf("unexpected output format") - } - values := strings.Fields(lines[1]) - if len(values) != len(fieldNames) { - return nil, fmt.Errorf("unexpected output format") - } - // get list of buckets sizes - bucketCoreCounts, err := getBucketSizesFromHex(values[0]) - if err != nil { - return nil, fmt.Errorf("failed to get bucket sizes from Hex string: %w", err) - } - // create buckets - var totalCoreBuckets []string // only for multi-die architectures - var dieCoreBuckets []string - totalCoreStartRange := 1 - startRange := 1 - var archMultiplier int - if strings.Contains(arch, "SRF") || strings.Contains(arch, "CWF") { - archMultiplier = 4 - } else if strings.Contains(arch, "GNR_X3") { - archMultiplier = 3 - } else if strings.Contains(arch, "GNR_X2") { - archMultiplier = 2 - } else { - archMultiplier = 1 - } - for _, count := range bucketCoreCounts { - if startRange > count { - break - } - if archMultiplier > 1 { - totalCoreCount := count * archMultiplier - if totalCoreStartRange > int(totalCoreCount) { - break - } - totalCoreBuckets = append(totalCoreBuckets, fmt.Sprintf("%d-%d", totalCoreStartRange, totalCoreCount)) - totalCoreStartRange = int(totalCoreCount) + 1 - } - dieCoreBuckets = append(dieCoreBuckets, fmt.Sprintf("%d-%d", startRange, count)) - startRange = int(count) + 1 - } - // get the frequencies for each isa - var allIsaFreqs [][]string - for _, isaHex := range values[1:] { - var isaFreqs []string - var freqs []int - var err error - freqs, err = getFrequenciesFromHex(isaHex) - if err != nil { - return nil, fmt.Errorf("failed to get frequencies from Hex string: %w", err) - } - if len(freqs) != len(bucketCoreCounts) { - freqs, err = padFrequencies(freqs, len(bucketCoreCounts)) - if err != nil { - return nil, fmt.Errorf("failed to pad frequencies: %w", err) - } - } - for _, freq := range freqs { - // convert freq to GHz - freqf := float64(freq) / 10.0 - isaFreqs = append(isaFreqs, fmt.Sprintf("%.1f", freqf)) - } - allIsaFreqs = append(allIsaFreqs, isaFreqs) - } - // format the output - var specCoreFreqs [][]string - specCoreFreqs = make([][]string, 1, len(dieCoreBuckets)+1) - // add bucket field name(s) - specCoreFreqs[0] = append(specCoreFreqs[0], "Cores") - if archMultiplier > 1 { - specCoreFreqs[0] = append(specCoreFreqs[0], "Cores per Die") - } - // add fieldNames for ISAs that have frequencies - for i := range allIsaFreqs { - if len(allIsaFreqs[i]) < 1 { - return nil, fmt.Errorf("no frequencies found for isa") - } - if allIsaFreqs[i][0] == "0.0" { - continue - } - specCoreFreqs[0] = append(specCoreFreqs[0], strings.ToUpper(fieldNames[i+1])) - } - for i, bucket := range dieCoreBuckets { - row := make([]string, 0, len(allIsaFreqs)+2) - // add the total core buckets for multi-die architectures - if archMultiplier > 1 { - if i >= len(totalCoreBuckets) { - return nil, fmt.Errorf("index out of range for total core buckets") - } - row = append(row, totalCoreBuckets[i]) - } - // add the die core buckets - row = append(row, bucket) - // add the frequencies for each ISA - for _, isaFreqs := range allIsaFreqs { - if len(isaFreqs) < 1 { - return nil, fmt.Errorf("no frequencies found for isa") - } - if isaFreqs[0] == "0.0" { - continue - } - if i >= len(isaFreqs) { - return nil, fmt.Errorf("index out of range for isa frequencies") - } - row = append(row, isaFreqs[i]) - } - specCoreFreqs = append(specCoreFreqs, row) - } - return specCoreFreqs, nil -} - -// expandTurboFrequencies expands the turbo frequencies to a list of frequencies -// input is the output of getSpecFrequencyBuckets, e.g.: -// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx" -// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1" -// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1" -// ... -// output is the expanded list of the frequencies for the requested ISA -func expandTurboFrequencies(specFrequencyBuckets [][]string, isa string) ([]string, error) { - if len(specFrequencyBuckets) < 2 || len(specFrequencyBuckets[0]) < 2 { - return nil, fmt.Errorf("unable to parse core frequency buckets") - } - rangeIdx := 0 // the first column is the bucket, e.g., 1-44 - // find the index of the ISA column - var isaIdx int - for i := 1; i < len(specFrequencyBuckets[0]); i++ { - if strings.EqualFold(specFrequencyBuckets[0][i], isa) { - isaIdx = i - break - } - } - if isaIdx == 0 { - return nil, fmt.Errorf("unable to find %s frequency column", isa) - } - var freqs []string - for i := 1; i < len(specFrequencyBuckets); i++ { - bucketCores, err := util.IntRangeToIntList(strings.TrimSpace(specFrequencyBuckets[i][rangeIdx])) - if err != nil { - return nil, fmt.Errorf("unable to parse bucket range %s", specFrequencyBuckets[i][rangeIdx]) - } - bucketFreq := strings.TrimSpace(specFrequencyBuckets[i][isaIdx]) - if bucketFreq == "" { - return nil, fmt.Errorf("unable to parse bucket frequency %s", specFrequencyBuckets[i][isaIdx]) - } - for range bucketCores { - freqs = append(freqs, bucketFreq) - } - } - return freqs, nil -} - -// maxFrequencyFromOutputs gets max core frequency -// -// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq -// 2nd option) from MSR/tpmi -// 3rd option) from dmidecode "Max Speed" -func maxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - cmdout := strings.TrimSpace(outputs[script.MaximumFrequencyScriptName].Stdout) - if cmdout != "" { - freqf, err := strconv.ParseFloat(cmdout, 64) - if err == nil { - freqf = freqf / 1000000 - return fmt.Sprintf("%.1fGHz", freqf) - } - } - // get the max frequency from the MSR/tpmi - specCoreFrequencies, err := getSpecFrequencyBuckets(outputs) - if err == nil { - sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies) - if len(sseFreqs) > 0 { - // max (single-core) frequency is the first SSE frequency - return sseFreqs[0] + "GHz" - } - } - return valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Max Speed:\s(.*)`) -} - -func getSSEFreqsFromBuckets(buckets [][]string) []string { - if len(buckets) < 2 { - return nil - } - // find the SSE column - sseColumn := -1 - for i, col := range buckets[0] { - if strings.ToUpper(col) == "SSE" { - sseColumn = i - break - } - } - if sseColumn == -1 { - return nil - } - // get the SSE values from the buckets - sse := make([]string, 0, len(buckets)-1) - for i := 1; i < len(buckets); i++ { - if len(buckets[i]) > sseColumn { - sse = append(sse, buckets[i][sseColumn]) - } - } - return sse -} - -func allCoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - specCoreFrequencies, err := getSpecFrequencyBuckets(outputs) - if err != nil { - return "" - } - sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies) - if len(sseFreqs) < 1 { - return "" - } - // all core max frequency is the last SSE frequency - return sseFreqs[len(sseFreqs)-1] + "GHz" -} - +// hyperthreadingFromOutput determines if hyperthreading is enabled based on lscpu output func hyperthreadingFromOutput(outputs map[string]script.ScriptOutput) string { family := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^CPU family:\s*(.+)$`) model := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^Model:\s*(.+)$`) @@ -696,73 +377,6 @@ func prefetchersSummaryFromOutput(outputs map[string]script.ScriptOutput) string return "None" } -func acceleratorNames() []string { - var names []string - for _, accel := range acceleratorDefinitions { - names = append(names, accel.Name) - } - return names -} - -func acceleratorCountsFromOutput(outputs map[string]script.ScriptOutput) []string { - var counts []string - lshw := outputs[script.LshwScriptName].Stdout - for _, accel := range acceleratorDefinitions { - regex := fmt.Sprintf("%s:%s", accel.MfgID, accel.DevID) - re := regexp.MustCompile(regex) - count := len(re.FindAllString(lshw, -1)) - counts = append(counts, fmt.Sprintf("%d", count)) - } - return counts -} - -func acceleratorWorkQueuesFromOutput(outputs map[string]script.ScriptOutput) []string { - var queues []string - for _, accel := range acceleratorDefinitions { - if accel.Name == "IAA" || accel.Name == "DSA" { - var scriptName string - if accel.Name == "IAA" { - scriptName = script.IaaDevicesScriptName - } else { - scriptName = script.DsaDevicesScriptName - } - devices := outputs[scriptName].Stdout - lines := strings.Split(devices, "\n") - // get non-empty lines - var nonEmptyLines []string - for _, line := range lines { - if strings.TrimSpace(line) != "" { - nonEmptyLines = append(nonEmptyLines, line) - } - } - if len(nonEmptyLines) == 0 { - queues = append(queues, "None") - } else { - queues = append(queues, strings.Join(nonEmptyLines, ", ")) - } - } else { - queues = append(queues, "N/A") - } - } - return queues -} - -func acceleratorFullNamesFromYaml() []string { - var fullNames []string - for _, accel := range acceleratorDefinitions { - fullNames = append(fullNames, accel.FullName) - } - return fullNames -} - -func acceleratorDescriptionsFromYaml() []string { - var descriptions []string - for _, accel := range acceleratorDefinitions { - descriptions = append(descriptions, accel.Description) - } - return descriptions -} - func tdpFromOutput(outputs map[string]script.ScriptOutput) string { msrHex := strings.TrimSpace(outputs[script.PackagePowerLimitName].Stdout) msr, err := strconv.ParseInt(msrHex, 16, 0) @@ -772,94 +386,6 @@ func tdpFromOutput(outputs map[string]script.ScriptOutput) string { return fmt.Sprint(msr/8) + "W" } -func uncoreMinMaxDieFrequencyFromOutput(maxFreq bool, computeDie bool, outputs map[string]script.ScriptOutput) string { - // find the first die that matches requrested die type (compute or I/O) - re := regexp.MustCompile(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry (\d+) in instance (\d+)`) - var instance, entry string - found := false - for line := range strings.SplitSeq(outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout, "\n") { - match := re.FindStringSubmatch(line) - if match == nil { - continue - } - if computeDie && match[1] == "0" { - found = true - entry = match[2] - instance = match[3] - break - } - if !computeDie && match[1] == "1" { - found = true - entry = match[2] - instance = match[3] - break - } - } - if !found { - slog.Error("failed to find uncore die type in TPMI output", slog.String("output", outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout)) - return "" - } - // get the frequency for the found die - re = regexp.MustCompile(fmt.Sprintf(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry %s in instance %s`, entry, instance)) - found = false - var parsed int64 - var err error - var scriptName string - if maxFreq { - scriptName = script.UncoreMaxFromTPMIScriptName - } else { - scriptName = script.UncoreMinFromTPMIScriptName - } - for line := range strings.SplitSeq(outputs[scriptName].Stdout, "\n") { - match := re.FindStringSubmatch(line) - if len(match) > 0 { - found = true - parsed, err = strconv.ParseInt(match[1], 10, 64) - if err != nil { - slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("line", line)) - return "" - } - break - } - } - if !found { - slog.Error("failed to find uncore frequency in TPMI output", slog.String("output", outputs[scriptName].Stdout)) - return "" - } - return fmt.Sprintf("%.1fGHz", float64(parsed)/10) -} - -func uncoreMinMaxFrequencyFromOutput(maxFreq bool, outputs map[string]script.ScriptOutput) string { - var parsed int64 - var err error - var scriptName string - if maxFreq { - scriptName = script.UncoreMaxFromMSRScriptName - } else { - scriptName = script.UncoreMinFromMSRScriptName - } - hex := strings.TrimSpace(outputs[scriptName].Stdout) - if hex != "" && hex != "0" { - parsed, err = strconv.ParseInt(hex, 16, 64) - if err != nil { - slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("hex", hex)) - return "" - } - } else { - slog.Warn("failed to get uncore frequency from MSR", slog.String("hex", hex)) - return "" - } - return fmt.Sprintf("%.1fGHz", float64(parsed)/10) -} - -func uncoreMinFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - return uncoreMinMaxFrequencyFromOutput(false, outputs) -} - -func uncoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { - return uncoreMinMaxFrequencyFromOutput(true, outputs) -} - func chaCountFromOutput(outputs map[string]script.ScriptOutput) string { // output is the result of three rdmsr calls // - client cha count @@ -1823,21 +1349,6 @@ func diskSummaryFromOutput(outputs map[string]script.ScriptOutput) string { return strings.Join(summary, ", ") } -func acceleratorSummaryFromOutput(outputs map[string]script.ScriptOutput) string { - var summary []string - accelerators := acceleratorNames() - counts := acceleratorCountsFromOutput(outputs) - for i, name := range accelerators { - if strings.Contains(name, "chipset") { // skip "QAT (on chipset)" in this table - continue - } else if strings.Contains(name, "CPU") { // rename "QAT (on CPU) to simply "QAT" - name = "QAT" - } - summary = append(summary, fmt.Sprintf("%s %s [0]", name, counts[i])) - } - return strings.Join(summary, ", ") -} - func cveSummaryFromOutput(outputs map[string]script.ScriptOutput) string { cves := cveInfoFromOutput(outputs) if len(cves) == 0 { @@ -1987,80 +1498,3 @@ func sectionValueFromOutput(output string, sectionName string) string { } return sections[sectionName] } - -func javaFoldedFromOutput(outputs map[string]script.ScriptOutput) string { - sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) - if len(sections) == 0 { - slog.Warn("no sections in collapsed call stack output") - return "" - } - javaFolded := make(map[string]string) - re := regexp.MustCompile(`^async-profiler (\d+) (.*)$`) - for header, stacks := range sections { - match := re.FindStringSubmatch(header) - if match == nil { - continue - } - pid := match[1] - processName := match[2] - if stacks == "" { - slog.Warn("no stacks for java process", slog.String("header", header)) - continue - } - if strings.HasPrefix(stacks, "Failed to inject profiler") { - slog.Error("profiling data error", slog.String("header", header)) - continue - } - _, ok := javaFolded[processName] - if processName == "" { - processName = "java (" + pid + ")" - } else if ok { - processName = processName + " (" + pid + ")" - } - javaFolded[processName] = stacks - } - folded, err := mergeJavaFolded(javaFolded) - if err != nil { - slog.Error("failed to merge java stacks", slog.String("error", err.Error())) - } - return folded -} - -func nativeFoldedFromOutput(outputs map[string]script.ScriptOutput) string { - sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) - if len(sections) == 0 { - slog.Warn("no sections in collapsed call stack output") - return "" - } - var dwarfFolded, fpFolded string - for header, content := range sections { - switch header { - case "perf_dwarf": - dwarfFolded = content - case "perf_fp": - fpFolded = content - } - } - if dwarfFolded == "" && fpFolded == "" { - return "" - } - folded, err := mergeSystemFolded(fpFolded, dwarfFolded) - if err != nil { - slog.Error("failed to merge native stacks", slog.String("error", err.Error())) - } - return folded -} - -func maxRenderDepthFromOutput(outputs map[string]script.ScriptOutput) string { - sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) - if len(sections) == 0 { - slog.Warn("no sections in collapsed call stack output") - return "" - } - for header, content := range sections { - if header == "maximum depth" { - return content - } - } - return "" -} diff --git a/internal/report/table_helpers_accelerator.go b/internal/report/table_helpers_accelerator.go new file mode 100644 index 00000000..23e02800 --- /dev/null +++ b/internal/report/table_helpers_accelerator.go @@ -0,0 +1,93 @@ +package report + +import ( + "fmt" + "perfspect/internal/script" + "regexp" + "strings" +) + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +func acceleratorNames() []string { + var names []string + for _, accel := range acceleratorDefinitions { + names = append(names, accel.Name) + } + return names +} + +func acceleratorCountsFromOutput(outputs map[string]script.ScriptOutput) []string { + var counts []string + lshw := outputs[script.LshwScriptName].Stdout + for _, accel := range acceleratorDefinitions { + regex := fmt.Sprintf("%s:%s", accel.MfgID, accel.DevID) + re := regexp.MustCompile(regex) + count := len(re.FindAllString(lshw, -1)) + counts = append(counts, fmt.Sprintf("%d", count)) + } + return counts +} + +func acceleratorWorkQueuesFromOutput(outputs map[string]script.ScriptOutput) []string { + var queues []string + for _, accel := range acceleratorDefinitions { + if accel.Name == "IAA" || accel.Name == "DSA" { + var scriptName string + if accel.Name == "IAA" { + scriptName = script.IaaDevicesScriptName + } else { + scriptName = script.DsaDevicesScriptName + } + devices := outputs[scriptName].Stdout + lines := strings.Split(devices, "\n") + // get non-empty lines + var nonEmptyLines []string + for _, line := range lines { + if strings.TrimSpace(line) != "" { + nonEmptyLines = append(nonEmptyLines, line) + } + } + if len(nonEmptyLines) == 0 { + queues = append(queues, "None") + } else { + queues = append(queues, strings.Join(nonEmptyLines, ", ")) + } + } else { + queues = append(queues, "N/A") + } + } + return queues +} + +func acceleratorFullNamesFromYaml() []string { + var fullNames []string + for _, accel := range acceleratorDefinitions { + fullNames = append(fullNames, accel.FullName) + } + return fullNames +} + +func acceleratorDescriptionsFromYaml() []string { + var descriptions []string + for _, accel := range acceleratorDefinitions { + descriptions = append(descriptions, accel.Description) + } + return descriptions +} + +func acceleratorSummaryFromOutput(outputs map[string]script.ScriptOutput) string { + var summary []string + accelerators := acceleratorNames() + counts := acceleratorCountsFromOutput(outputs) + for i, name := range accelerators { + if strings.Contains(name, "chipset") { // skip "QAT (on chipset)" in this table + continue + } else if strings.Contains(name, "CPU") { // rename "QAT (on CPU) to simply "QAT" + name = "QAT" + } + summary = append(summary, fmt.Sprintf("%s %s [0]", name, counts[i])) + } + return strings.Join(summary, ", ") +} diff --git a/internal/report/table_helpers_frequency.go b/internal/report/table_helpers_frequency.go new file mode 100644 index 00000000..f71a41ae --- /dev/null +++ b/internal/report/table_helpers_frequency.go @@ -0,0 +1,502 @@ +package report + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// table_helpers_frequency.go contains helper functions for parsing and processing CPU frequency data. + +import ( + "fmt" + "log/slog" + "regexp" + "strconv" + "strings" + + "perfspect/internal/script" + "perfspect/internal/util" + + "slices" +) + +// getFrequenciesFromHex converts a hex string to a list of frequency integers. +// The frequencies are reversed to match the expected order. +func getFrequenciesFromHex(hex string) ([]int, error) { + freqs, err := util.HexToIntList(hex) + if err != nil { + return nil, err + } + // reverse the order of the frequencies + slices.Reverse(freqs) + return freqs, nil +} + +// getBucketSizesFromHex extracts bucket sizes from a hex string. +// Expects exactly 8 bucket sizes and reverses their order. +func getBucketSizesFromHex(hex string) ([]int, error) { + bucketSizes, err := util.HexToIntList(hex) + if err != nil { + return nil, err + } + if len(bucketSizes) != 8 { + err = fmt.Errorf("expected 8 bucket sizes, got %d", len(bucketSizes)) + return nil, err + } + // reverse the order of the core counts + slices.Reverse(bucketSizes) + return bucketSizes, nil +} + +// padFrequencies adds items to the frequencies slice until it reaches the desired length. +// The value of the added items is the same as the last item in the original slice. +func padFrequencies(freqs []int, desiredLength int) ([]int, error) { + if len(freqs) == 0 { + return nil, fmt.Errorf("cannot pad empty frequencies slice") + } + for len(freqs) < desiredLength { + freqs = append(freqs, freqs[len(freqs)-1]) + } + return freqs, nil +} + +// getArchMultiplier returns the die multiplier for multi-die architectures. +// Returns 1 for single-die architectures. +func getArchMultiplier(arch string) int { + if strings.Contains(arch, "SRF") || strings.Contains(arch, "CWF") { + return 4 + } else if strings.Contains(arch, "GNR_X3") { + return 3 + } else if strings.Contains(arch, "GNR_X2") { + return 2 + } + return 1 +} + +// parseFrequencyScriptOutput validates and parses the raw script output. +// Returns field names and hex values. +func parseFrequencyScriptOutput(output string) (fieldNames []string, hexValues []string, err error) { + if output == "" { + return nil, nil, fmt.Errorf("no core frequencies found") + } + + lines := strings.Split(output, "\n") + if len(lines) < 2 { + return nil, nil, fmt.Errorf("unexpected output format: need at least 2 lines") + } + + fieldNames = strings.Fields(lines[0]) + if len(fieldNames) < 2 { + return nil, nil, fmt.Errorf("unexpected output format: need at least 2 fields") + } + + hexValues = strings.Fields(lines[1]) + if len(hexValues) != len(fieldNames) { + return nil, nil, fmt.Errorf("unexpected output format: field count mismatch") + } + + return fieldNames, hexValues, nil +} + +// buildCoreBuckets creates core range strings for both total and per-die cores. +// For single-die architectures, totalCoreBuckets will be empty. +func buildCoreBuckets(bucketCoreCounts []int, archMultiplier int) (totalCoreBuckets, dieCoreBuckets []string) { + totalCoreStart := 1 + dieStart := 1 + + for _, count := range bucketCoreCounts { + if dieStart > count { + break + } + + // Build per-die bucket + dieCoreBuckets = append(dieCoreBuckets, fmt.Sprintf("%d-%d", dieStart, count)) + dieStart = count + 1 + + // Build total bucket for multi-die architectures + if archMultiplier > 1 { + totalCoreCount := count * archMultiplier + if totalCoreStart > totalCoreCount { + break + } + totalCoreBuckets = append(totalCoreBuckets, fmt.Sprintf("%d-%d", totalCoreStart, totalCoreCount)) + totalCoreStart = totalCoreCount + 1 + } + } + + return totalCoreBuckets, dieCoreBuckets +} + +// parseISAFrequencies converts hex frequency values to GHz strings. +func parseISAFrequencies(isaHex string, bucketCount int) ([]string, error) { + freqs, err := getFrequenciesFromHex(isaHex) + if err != nil { + return nil, fmt.Errorf("failed to get frequencies from hex: %w", err) + } + + // Pad if necessary to match bucket count + if len(freqs) != bucketCount { + freqs, err = padFrequencies(freqs, bucketCount) + if err != nil { + return nil, fmt.Errorf("failed to pad frequencies: %w", err) + } + } + + // Convert to GHz strings + isaFreqs := make([]string, len(freqs)) + for i, freq := range freqs { + freqGHz := float64(freq) / 10.0 + isaFreqs[i] = fmt.Sprintf("%.1f", freqGHz) + } + + return isaFreqs, nil +} + +// isISASupported checks if an ISA has non-zero frequencies. +func isISASupported(isaFreqs []string) bool { + return len(isaFreqs) > 0 && isaFreqs[0] != "0.0" +} + +// buildFrequencyTableHeader creates the header row for the frequency table. +func buildFrequencyTableHeader(fieldNames []string, allIsaFreqs [][]string, archMultiplier int) []string { + header := []string{"Cores"} + + if archMultiplier > 1 { + header = append(header, "Cores per Die") + } + + // Add ISA names for supported ISAs only + for i, isaFreqs := range allIsaFreqs { + if isISASupported(isaFreqs) { + header = append(header, strings.ToUpper(fieldNames[i+1])) + } + } + + return header +} + +// buildFrequencyTableRow creates a single data row for the frequency table. +func buildFrequencyTableRow(bucketIdx int, totalCoreBuckets, dieCoreBuckets []string, + allIsaFreqs [][]string, archMultiplier int) ([]string, error) { + + row := make([]string, 0, len(allIsaFreqs)+2) + + // Add total core bucket for multi-die architectures + if archMultiplier > 1 { + if bucketIdx >= len(totalCoreBuckets) { + return nil, fmt.Errorf("bucket index %d out of range for total core buckets", bucketIdx) + } + row = append(row, totalCoreBuckets[bucketIdx]) + } + + // Add per-die core bucket + row = append(row, dieCoreBuckets[bucketIdx]) + + // Add frequency values for supported ISAs + for _, isaFreqs := range allIsaFreqs { + if !isISASupported(isaFreqs) { + continue + } + if bucketIdx >= len(isaFreqs) { + return nil, fmt.Errorf("bucket index %d out of range for ISA frequencies", bucketIdx) + } + row = append(row, isaFreqs[bucketIdx]) + } + + return row, nil +} + +// getSpecFrequencyBuckets parses turbo frequency data and returns a formatted table. +// The table structure is: +// - First row: header with column names (Cores, [Cores per Die], ISA1, ISA2, ...) +// - Subsequent rows: frequency data for each core count bucket +// +// Example output for multi-die architecture: +// +// ["Cores", "Cores per Die", "SSE", "AVX2", "AVX512"] +// ["0-41", "0-20", "3.5", "3.5", "3.3"] +// ["42-63", "21-31", "3.5", "3.5", "3.3"] +// +// The "Cores per Die" column is only present for multi-die architectures (GNR_X2, GNR_X3, SRF, CWF). +func getSpecFrequencyBuckets(outputs map[string]script.ScriptOutput) ([][]string, error) { + // Get architecture to determine die multiplier + arch := UarchFromOutput(outputs) + if arch == "" { + return nil, fmt.Errorf("uarch is required") + } + archMultiplier := getArchMultiplier(arch) + + // Parse script output + fieldNames, hexValues, err := parseFrequencyScriptOutput(outputs[script.SpecCoreFrequenciesScriptName].Stdout) + if err != nil { + return nil, err + } + + // Extract bucket sizes from first hex value + bucketCoreCounts, err := getBucketSizesFromHex(hexValues[0]) + if err != nil { + return nil, fmt.Errorf("failed to get bucket sizes: %w", err) + } + + // Build core range strings + totalCoreBuckets, dieCoreBuckets := buildCoreBuckets(bucketCoreCounts, archMultiplier) + + // Parse ISA frequencies from remaining hex values + allIsaFreqs := make([][]string, 0, len(hexValues)-1) + for _, isaHex := range hexValues[1:] { + isaFreqs, err := parseISAFrequencies(isaHex, len(bucketCoreCounts)) + if err != nil { + return nil, err + } + allIsaFreqs = append(allIsaFreqs, isaFreqs) + } + + // Build output table + table := make([][]string, 0, len(dieCoreBuckets)+1) + + // Add header row + header := buildFrequencyTableHeader(fieldNames, allIsaFreqs, archMultiplier) + table = append(table, header) + + // Add data rows + for i := range dieCoreBuckets { + row, err := buildFrequencyTableRow(i, totalCoreBuckets, dieCoreBuckets, allIsaFreqs, archMultiplier) + if err != nil { + return nil, err + } + table = append(table, row) + } + + return table, nil +} + +// expandTurboFrequencies expands the turbo frequencies to a list of frequencies +// input is the output of getSpecFrequencyBuckets, e.g.: +// "cores", "cores per die", "sse", "avx2", "avx512", "avx512h", "amx" +// "0-41", "0-20", "3.5", "3.5", "3.3", "3.2", "3.1" +// "42-63", "21-31", "3.5", "3.5", "3.3", "3.2", "3.1" +// ... +// output is the expanded list of the frequencies for the requested ISA +func expandTurboFrequencies(specFrequencyBuckets [][]string, isa string) ([]string, error) { + if len(specFrequencyBuckets) < 2 || len(specFrequencyBuckets[0]) < 2 { + return nil, fmt.Errorf("unable to parse core frequency buckets") + } + rangeIdx := 0 // the first column is the bucket, e.g., 1-44 + // find the index of the ISA column + var isaIdx int + for i := 1; i < len(specFrequencyBuckets[0]); i++ { + if strings.EqualFold(specFrequencyBuckets[0][i], isa) { + isaIdx = i + break + } + } + if isaIdx == 0 { + return nil, fmt.Errorf("unable to find %s frequency column", isa) + } + var freqs []string + for i := 1; i < len(specFrequencyBuckets); i++ { + bucketCores, err := util.IntRangeToIntList(strings.TrimSpace(specFrequencyBuckets[i][rangeIdx])) + if err != nil { + return nil, fmt.Errorf("unable to parse bucket range %s", specFrequencyBuckets[i][rangeIdx]) + } + bucketFreq := strings.TrimSpace(specFrequencyBuckets[i][isaIdx]) + if bucketFreq == "" { + return nil, fmt.Errorf("unable to parse bucket frequency %s", specFrequencyBuckets[i][isaIdx]) + } + for range bucketCores { + freqs = append(freqs, bucketFreq) + } + } + return freqs, nil +} + +// maxFrequencyFromOutput gets max core frequency +// +// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq +// 2nd option) from MSR/tpmi +// 3rd option) from dmidecode "Max Speed" +func maxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + cmdout := strings.TrimSpace(outputs[script.MaximumFrequencyScriptName].Stdout) + if cmdout != "" { + freqf, err := strconv.ParseFloat(cmdout, 64) + if err == nil { + freqf = freqf / 1000000 + return fmt.Sprintf("%.1fGHz", freqf) + } + } + // get the max frequency from the MSR/tpmi + specCoreFrequencies, err := getSpecFrequencyBuckets(outputs) + if err == nil { + sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies) + if len(sseFreqs) > 0 { + // max (single-core) frequency is the first SSE frequency + return sseFreqs[0] + "GHz" + } + } + return valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Max Speed:\s(.*)`) +} + +// getSSEFreqsFromBuckets extracts SSE frequency values from frequency buckets. +func getSSEFreqsFromBuckets(buckets [][]string) []string { + if len(buckets) < 2 { + return nil + } + // find the SSE column + sseColumn := -1 + for i, col := range buckets[0] { + if strings.ToUpper(col) == "SSE" { + sseColumn = i + break + } + } + if sseColumn == -1 { + return nil + } + // get the SSE values from the buckets + sse := make([]string, 0, len(buckets)-1) + for i := 1; i < len(buckets); i++ { + if len(buckets[i]) > sseColumn { + sse = append(sse, buckets[i][sseColumn]) + } + } + return sse +} + +// allCoreMaxFrequencyFromOutput gets the all-core max frequency. +func allCoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + specCoreFrequencies, err := getSpecFrequencyBuckets(outputs) + if err != nil { + return "" + } + sseFreqs := getSSEFreqsFromBuckets(specCoreFrequencies) + if len(sseFreqs) < 1 { + return "" + } + // all core max frequency is the last SSE frequency + return sseFreqs[len(sseFreqs)-1] + "GHz" +} + +// baseFrequencyFromOutput gets base core frequency +// +// 1st option) /sys/devices/system/cpu/cpu0/cpufreq/base_frequency +// 2nd option) from dmidecode "Current Speed" +// 3nd option) parse it from the model name +func baseFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + cmdout := strings.TrimSpace(outputs[script.BaseFrequencyScriptName].Stdout) + if cmdout != "" { + freqf, err := strconv.ParseFloat(cmdout, 64) + if err == nil { + freqf = freqf / 1000000 + return fmt.Sprintf("%.1fGHz", freqf) + } + } + currentSpeedVal := valFromDmiDecodeRegexSubmatch(outputs[script.DmidecodeScriptName].Stdout, "4", `Current Speed:\s(.*)$`) + tokens := strings.Split(currentSpeedVal, " ") + if len(tokens) == 2 { + num, err := strconv.ParseFloat(tokens[0], 64) + if err == nil { + unit := tokens[1] + if unit == "MHz" { + num = num / 1000 + unit = "GHz" + } + return fmt.Sprintf("%.1f%s", num, unit) + } + } + // the frequency (if included) is at the end of the model name in lscpu's output + modelName := valFromRegexSubmatch(outputs[script.LscpuScriptName].Stdout, `^[Mm]odel name.*:\s*(.+?)$`) + tokens = strings.Split(modelName, " ") + if len(tokens) > 0 { + lastToken := tokens[len(tokens)-1] + if len(lastToken) > 0 && lastToken[len(lastToken)-1] == 'z' { + return lastToken + } + } + return "" +} + +func uncoreMinMaxDieFrequencyFromOutput(maxFreq bool, computeDie bool, outputs map[string]script.ScriptOutput) string { + // find the first die that matches requrested die type (compute or I/O) + re := regexp.MustCompile(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry (\d+) in instance (\d+)`) + var instance, entry string + found := false + for line := range strings.SplitSeq(outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout, "\n") { + match := re.FindStringSubmatch(line) + if match == nil { + continue + } + if computeDie && match[1] == "0" { + found = true + entry = match[2] + instance = match[3] + break + } + if !computeDie && match[1] == "1" { + found = true + entry = match[2] + instance = match[3] + break + } + } + if !found { + slog.Error("failed to find uncore die type in TPMI output", slog.String("output", outputs[script.UncoreDieTypesFromTPMIScriptName].Stdout)) + return "" + } + // get the frequency for the found die + re = regexp.MustCompile(fmt.Sprintf(`Read bits \d+:\d+ value (\d+) from TPMI ID .* for entry %s in instance %s`, entry, instance)) + found = false + var parsed int64 + var err error + var scriptName string + if maxFreq { + scriptName = script.UncoreMaxFromTPMIScriptName + } else { + scriptName = script.UncoreMinFromTPMIScriptName + } + for line := range strings.SplitSeq(outputs[scriptName].Stdout, "\n") { + match := re.FindStringSubmatch(line) + if len(match) > 0 { + found = true + parsed, err = strconv.ParseInt(match[1], 10, 64) + if err != nil { + slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("line", line)) + return "" + } + break + } + } + if !found { + slog.Error("failed to find uncore frequency in TPMI output", slog.String("output", outputs[scriptName].Stdout)) + return "" + } + return fmt.Sprintf("%.1fGHz", float64(parsed)/10) +} + +func uncoreMinMaxFrequencyFromOutput(maxFreq bool, outputs map[string]script.ScriptOutput) string { + var parsed int64 + var err error + var scriptName string + if maxFreq { + scriptName = script.UncoreMaxFromMSRScriptName + } else { + scriptName = script.UncoreMinFromMSRScriptName + } + hex := strings.TrimSpace(outputs[scriptName].Stdout) + if hex != "" && hex != "0" { + parsed, err = strconv.ParseInt(hex, 16, 64) + if err != nil { + slog.Error("failed to parse uncore frequency", slog.String("error", err.Error()), slog.String("hex", hex)) + return "" + } + } else { + slog.Warn("failed to get uncore frequency from MSR", slog.String("hex", hex)) + return "" + } + return fmt.Sprintf("%.1fGHz", float64(parsed)/10) +} + +func uncoreMinFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + return uncoreMinMaxFrequencyFromOutput(false, outputs) +} + +func uncoreMaxFrequencyFromOutput(outputs map[string]script.ScriptOutput) string { + return uncoreMinMaxFrequencyFromOutput(true, outputs) +} diff --git a/internal/report/table_helpers_frequency_test.go b/internal/report/table_helpers_frequency_test.go new file mode 100644 index 00000000..21a9cf7c --- /dev/null +++ b/internal/report/table_helpers_frequency_test.go @@ -0,0 +1,207 @@ +package report + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "reflect" + "testing" +) + +func TestGetFrequenciesFromHex(t *testing.T) { + tests := []struct { + name string + msr string + want []int + expectErr bool + }{ + { + name: "Valid MSR with multiple frequencies", + msr: "0x1A2B3C4D", + want: []int{0x4D, 0x3C, 0x2B, 0x1A}, + expectErr: false, + }, + { + name: "Valid MSR with single frequency", + msr: "0x1A", + want: []int{0x1A}, + expectErr: false, + }, + { + name: "Empty MSR string", + msr: "", + want: nil, + expectErr: true, + }, + { + name: "Invalid MSR string", + msr: "invalid_hex", + want: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getFrequenciesFromHex(tt.msr) + if (err != nil) != tt.expectErr { + t.Errorf("getFrequenciesFromMSR() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getFrequenciesFromMSR() = %v, want %v", got, tt.want) + } + }) + } +} +func TestGetBucketSizesFromHex(t *testing.T) { + tests := []struct { + name string + msr string + want []int + expectErr bool + }{ + { + name: "Valid MSR with 8 bucket sizes", + msr: "0x0102030405060708", + want: []int{8, 7, 6, 5, 4, 3, 2, 1}, + expectErr: false, + }, + { + name: "Valid MSR with reversed order", + msr: "0x0807060504030201", + want: []int{1, 2, 3, 4, 5, 6, 7, 8}, + expectErr: false, + }, + { + name: "Invalid MSR string", + msr: "invalid_hex", + want: nil, + expectErr: true, + }, + { + name: "MSR with less than 8 bucket sizes", + msr: "0x01020304", + want: nil, + expectErr: true, + }, + { + name: "MSR with more than 8 bucket sizes", + msr: "0x010203040506070809", + want: nil, + expectErr: true, + }, + { + name: "Empty MSR string", + msr: "", + want: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getBucketSizesFromHex(tt.msr) + if (err != nil) != tt.expectErr { + t.Errorf("getBucketSizesFromMSR() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getBucketSizesFromMSR() = %v, want %v", got, tt.want) + } + }) + } +} +func TestExpandTurboFrequencies(t *testing.T) { + tests := []struct { + name string + buckets [][]string + isa string + want []string + expectErr bool + }{ + { + name: "Valid input with single bucket", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "3.5", "3.2"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.5", "3.5"}, + expectErr: false, + }, + { + name: "Valid input with multiple buckets", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-2", "3.5", "3.2"}, + {"3-4", "3.6", "3.3"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.6", "3.6"}, + expectErr: false, + }, + { + name: "ISA column not found", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "3.5", "3.2"}, + }, + isa: "AVX512", + want: nil, + expectErr: true, + }, + { + name: "Empty buckets", + buckets: [][]string{ + {}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Invalid bucket range", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-", "3.5", "3.2"}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Empty frequency value", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {"1-4", "", "3.2"}, + }, + isa: "SSE", + want: nil, + expectErr: true, + }, + { + name: "Whitespace in bucket range", + buckets: [][]string{ + {"Cores", "SSE", "AVX2"}, + {" 1-4 ", "3.5", "3.2"}, + }, + isa: "SSE", + want: []string{"3.5", "3.5", "3.5", "3.5"}, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := expandTurboFrequencies(tt.buckets, tt.isa) + if (err != nil) != tt.expectErr { + t.Errorf("expandTurboFrequencies() error = %v, expectErr %v", err, tt.expectErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("expandTurboFrequencies() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/report/table_helpers_nic_integration_test.go b/internal/report/table_helpers_nic_integration_test.go deleted file mode 100644 index 591cffe6..00000000 --- a/internal/report/table_helpers_nic_integration_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package report - -// Copyright (C) 2021-2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause - -import ( - "testing" - - "perfspect/internal/script" -) - -func TestParseNicInfoWithCardPort(t *testing.T) { - // Sample output simulating the scenario from the issue - sampleOutput := `Interface: eth2 -Vendor ID: 8086 -Model ID: 1593 -Vendor: Intel Corporation -Model: Ethernet Controller 10G X550T -Speed: 1000Mb/s -Link detected: yes -bus-info: 0000:32:00.0 -driver: ixgbe -version: 5.1.0-k -firmware-version: 0x800009e0 -MAC Address: aa:bb:cc:dd:ee:00 -NUMA Node: 0 -CPU Affinity: -IRQ Balance: Enabled -rx-usecs: 1 -tx-usecs: 1 -Adaptive RX: off TX: off ----------------------------------------- -Interface: eth3 -Vendor ID: 8086 -Model ID: 1593 -Vendor: Intel Corporation -Model: Ethernet Controller 10G X550T -Speed: Unknown! -Link detected: no -bus-info: 0000:32:00.1 -driver: ixgbe -version: 5.1.0-k -firmware-version: 0x800009e0 -MAC Address: aa:bb:cc:dd:ee:01 -NUMA Node: 0 -CPU Affinity: -IRQ Balance: Enabled -rx-usecs: 1 -tx-usecs: 1 -Adaptive RX: off TX: off ----------------------------------------- -Interface: eth0 -Vendor ID: 8086 -Model ID: 37d2 -Vendor: Intel Corporation -Model: Ethernet Controller E810-C for QSFP -Speed: 100000Mb/s -Link detected: yes -bus-info: 0000:c0:00.0 -driver: ice -version: K_5.19.0-41-generic_5.1.9 -firmware-version: 4.40 0x8001c967 1.3534.0 -MAC Address: aa:bb:cc:dd:ee:82 -NUMA Node: 1 -CPU Affinity: -IRQ Balance: Enabled -rx-usecs: 1 -tx-usecs: 1 -Adaptive RX: off TX: off ----------------------------------------- -Interface: eth1 -Vendor ID: 8086 -Model ID: 37d2 -Vendor: Intel Corporation -Model: Ethernet Controller E810-C for QSFP -Speed: 100000Mb/s -Link detected: yes -bus-info: 0000:c0:00.1 -driver: ice -version: K_5.19.0-41-generic_5.1.9 -firmware-version: 4.40 0x8001c967 1.3534.0 -MAC Address: aa:bb:cc:dd:ee:83 -NUMA Node: 1 -CPU Affinity: -IRQ Balance: Enabled -rx-usecs: 1 -tx-usecs: 1 -Adaptive RX: off TX: off -----------------------------------------` - - nics := parseNicInfo(sampleOutput) - - if len(nics) != 4 { - t.Fatalf("Expected 4 NICs, got %d", len(nics)) - } - - // Expected card/port assignments based on the issue example - expectedCardPort := map[string]struct { - card string - port string - }{ - "eth2": {"1", "1"}, // 0000:32:00.0 - "eth3": {"1", "2"}, // 0000:32:00.1 - "eth0": {"2", "1"}, // 0000:c0:00.0 - "eth1": {"2", "2"}, // 0000:c0:00.1 - } - - for _, nic := range nics { - expected, exists := expectedCardPort[nic.Name] - if !exists { - t.Errorf("Unexpected NIC name: %s", nic.Name) - continue - } - if nic.Card != expected.card { - t.Errorf("NIC %s: expected card %s, got %s", nic.Name, expected.card, nic.Card) - } - if nic.Port != expected.port { - t.Errorf("NIC %s: expected port %s, got %s", nic.Name, expected.port, nic.Port) - } - } -} - -func TestNicTableValuesWithCardPort(t *testing.T) { - // Sample output simulating the scenario from the issue - sampleOutput := `Interface: eth2 -bus-info: 0000:32:00.0 -Vendor: Intel Corporation -Model: Ethernet Controller 10G X550T -Speed: 1000Mb/s -Link detected: yes ----------------------------------------- -Interface: eth3 -bus-info: 0000:32:00.1 -Vendor: Intel Corporation -Model: Ethernet Controller 10G X550T -Speed: Unknown! -Link detected: no ----------------------------------------- -Interface: eth0 -bus-info: 0000:c0:00.0 -Vendor: Intel Corporation -Model: Ethernet Controller E810-C for QSFP -Speed: 100000Mb/s -Link detected: yes ----------------------------------------- -Interface: eth1 -bus-info: 0000:c0:00.1 -Vendor: Intel Corporation -Model: Ethernet Controller E810-C for QSFP -Speed: 100000Mb/s -Link detected: yes -----------------------------------------` - - outputs := map[string]script.ScriptOutput{ - script.NicInfoScriptName: {Stdout: sampleOutput}, - } - - fields := nicTableValues(outputs) - - // Find the "Card / Port" field - var cardPortField Field - found := false - for _, field := range fields { - if field.Name == "Card / Port" { - cardPortField = field - found = true - break - } - } - - if !found { - t.Fatal("Card / Port field not found in NIC table") - } - - // Verify we have 4 entries - if len(cardPortField.Values) != 4 { - t.Fatalf("Expected 4 Card / Port values, got %d", len(cardPortField.Values)) - } - - // Find the Name field to match values - var nameField Field - for _, field := range fields { - if field.Name == "Name" { - nameField = field - break - } - } - - // Verify card/port assignments - expectedCardPort := map[string]string{ - "eth2": "1 / 1", - "eth3": "1 / 2", - "eth0": "2 / 1", - "eth1": "2 / 2", - } - - for i, name := range nameField.Values { - expected := expectedCardPort[name] - actual := cardPortField.Values[i] - if actual != expected { - t.Errorf("NIC %s: expected Card / Port %q, got %q", name, expected, actual) - } - } -} diff --git a/internal/report/table_helpers_nic_test.go b/internal/report/table_helpers_nic_test.go index a5742b44..60651b36 100644 --- a/internal/report/table_helpers_nic_test.go +++ b/internal/report/table_helpers_nic_test.go @@ -4,6 +4,7 @@ package report // SPDX-License-Identifier: BSD-3-Clause import ( + "perfspect/internal/script" "testing" ) @@ -102,3 +103,197 @@ func TestExtractFunction(t *testing.T) { }) } } + +func TestParseNicInfoWithCardPort(t *testing.T) { + // Sample output simulating the scenario from the issue + sampleOutput := `Interface: eth2 +Vendor ID: 8086 +Model ID: 1593 +Vendor: Intel Corporation +Model: Ethernet Controller 10G X550T +Speed: 1000Mb/s +Link detected: yes +bus-info: 0000:32:00.0 +driver: ixgbe +version: 5.1.0-k +firmware-version: 0x800009e0 +MAC Address: aa:bb:cc:dd:ee:00 +NUMA Node: 0 +CPU Affinity: +IRQ Balance: Enabled +rx-usecs: 1 +tx-usecs: 1 +Adaptive RX: off TX: off +---------------------------------------- +Interface: eth3 +Vendor ID: 8086 +Model ID: 1593 +Vendor: Intel Corporation +Model: Ethernet Controller 10G X550T +Speed: Unknown! +Link detected: no +bus-info: 0000:32:00.1 +driver: ixgbe +version: 5.1.0-k +firmware-version: 0x800009e0 +MAC Address: aa:bb:cc:dd:ee:01 +NUMA Node: 0 +CPU Affinity: +IRQ Balance: Enabled +rx-usecs: 1 +tx-usecs: 1 +Adaptive RX: off TX: off +---------------------------------------- +Interface: eth0 +Vendor ID: 8086 +Model ID: 37d2 +Vendor: Intel Corporation +Model: Ethernet Controller E810-C for QSFP +Speed: 100000Mb/s +Link detected: yes +bus-info: 0000:c0:00.0 +driver: ice +version: K_5.19.0-41-generic_5.1.9 +firmware-version: 4.40 0x8001c967 1.3534.0 +MAC Address: aa:bb:cc:dd:ee:82 +NUMA Node: 1 +CPU Affinity: +IRQ Balance: Enabled +rx-usecs: 1 +tx-usecs: 1 +Adaptive RX: off TX: off +---------------------------------------- +Interface: eth1 +Vendor ID: 8086 +Model ID: 37d2 +Vendor: Intel Corporation +Model: Ethernet Controller E810-C for QSFP +Speed: 100000Mb/s +Link detected: yes +bus-info: 0000:c0:00.1 +driver: ice +version: K_5.19.0-41-generic_5.1.9 +firmware-version: 4.40 0x8001c967 1.3534.0 +MAC Address: aa:bb:cc:dd:ee:83 +NUMA Node: 1 +CPU Affinity: +IRQ Balance: Enabled +rx-usecs: 1 +tx-usecs: 1 +Adaptive RX: off TX: off +----------------------------------------` + + nics := parseNicInfo(sampleOutput) + + if len(nics) != 4 { + t.Fatalf("Expected 4 NICs, got %d", len(nics)) + } + + // Expected card/port assignments based on the issue example + expectedCardPort := map[string]struct { + card string + port string + }{ + "eth2": {"1", "1"}, // 0000:32:00.0 + "eth3": {"1", "2"}, // 0000:32:00.1 + "eth0": {"2", "1"}, // 0000:c0:00.0 + "eth1": {"2", "2"}, // 0000:c0:00.1 + } + + for _, nic := range nics { + expected, exists := expectedCardPort[nic.Name] + if !exists { + t.Errorf("Unexpected NIC name: %s", nic.Name) + continue + } + if nic.Card != expected.card { + t.Errorf("NIC %s: expected card %s, got %s", nic.Name, expected.card, nic.Card) + } + if nic.Port != expected.port { + t.Errorf("NIC %s: expected port %s, got %s", nic.Name, expected.port, nic.Port) + } + } +} + +func TestNicTableValuesWithCardPort(t *testing.T) { + // Sample output simulating the scenario from the issue + sampleOutput := `Interface: eth2 +bus-info: 0000:32:00.0 +Vendor: Intel Corporation +Model: Ethernet Controller 10G X550T +Speed: 1000Mb/s +Link detected: yes +---------------------------------------- +Interface: eth3 +bus-info: 0000:32:00.1 +Vendor: Intel Corporation +Model: Ethernet Controller 10G X550T +Speed: Unknown! +Link detected: no +---------------------------------------- +Interface: eth0 +bus-info: 0000:c0:00.0 +Vendor: Intel Corporation +Model: Ethernet Controller E810-C for QSFP +Speed: 100000Mb/s +Link detected: yes +---------------------------------------- +Interface: eth1 +bus-info: 0000:c0:00.1 +Vendor: Intel Corporation +Model: Ethernet Controller E810-C for QSFP +Speed: 100000Mb/s +Link detected: yes +----------------------------------------` + + outputs := map[string]script.ScriptOutput{ + script.NicInfoScriptName: {Stdout: sampleOutput}, + } + + fields := nicTableValues(outputs) + + // Find the "Card / Port" field + var cardPortField Field + found := false + for _, field := range fields { + if field.Name == "Card / Port" { + cardPortField = field + found = true + break + } + } + + if !found { + t.Fatal("Card / Port field not found in NIC table") + } + + // Verify we have 4 entries + if len(cardPortField.Values) != 4 { + t.Fatalf("Expected 4 Card / Port values, got %d", len(cardPortField.Values)) + } + + // Find the Name field to match values + var nameField Field + for _, field := range fields { + if field.Name == "Name" { + nameField = field + break + } + } + + // Verify card/port assignments + expectedCardPort := map[string]string{ + "eth2": "1 / 1", + "eth3": "1 / 2", + "eth0": "2 / 1", + "eth1": "2 / 2", + } + + for i, name := range nameField.Values { + expected := expectedCardPort[name] + actual := cardPortField.Values[i] + if actual != expected { + t.Errorf("NIC %s: expected Card / Port %q, got %q", name, expected, actual) + } + } +} diff --git a/internal/report/table_helpers_stacks.go b/internal/report/table_helpers_stacks.go index 2b9554eb..f0091043 100644 --- a/internal/report/table_helpers_stacks.go +++ b/internal/report/table_helpers_stacks.go @@ -5,7 +5,9 @@ package report import ( "fmt" + "log/slog" "math" + "perfspect/internal/script" "regexp" "strconv" "strings" @@ -163,3 +165,80 @@ func mergeSystemFolded(perfFp string, perfDwarf string) (merged string, err erro merged = mergedStacks.dumpFolded() return } + +func javaFoldedFromOutput(outputs map[string]script.ScriptOutput) string { + sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) + if len(sections) == 0 { + slog.Warn("no sections in collapsed call stack output") + return "" + } + javaFolded := make(map[string]string) + re := regexp.MustCompile(`^async-profiler (\d+) (.*)$`) + for header, stacks := range sections { + match := re.FindStringSubmatch(header) + if match == nil { + continue + } + pid := match[1] + processName := match[2] + if stacks == "" { + slog.Warn("no stacks for java process", slog.String("header", header)) + continue + } + if strings.HasPrefix(stacks, "Failed to inject profiler") { + slog.Error("profiling data error", slog.String("header", header)) + continue + } + _, ok := javaFolded[processName] + if processName == "" { + processName = "java (" + pid + ")" + } else if ok { + processName = processName + " (" + pid + ")" + } + javaFolded[processName] = stacks + } + folded, err := mergeJavaFolded(javaFolded) + if err != nil { + slog.Error("failed to merge java stacks", slog.String("error", err.Error())) + } + return folded +} + +func nativeFoldedFromOutput(outputs map[string]script.ScriptOutput) string { + sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) + if len(sections) == 0 { + slog.Warn("no sections in collapsed call stack output") + return "" + } + var dwarfFolded, fpFolded string + for header, content := range sections { + switch header { + case "perf_dwarf": + dwarfFolded = content + case "perf_fp": + fpFolded = content + } + } + if dwarfFolded == "" && fpFolded == "" { + return "" + } + folded, err := mergeSystemFolded(fpFolded, dwarfFolded) + if err != nil { + slog.Error("failed to merge native stacks", slog.String("error", err.Error())) + } + return folded +} + +func maxRenderDepthFromOutput(outputs map[string]script.ScriptOutput) string { + sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout) + if len(sections) == 0 { + slog.Warn("no sections in collapsed call stack output") + return "" + } + for header, content := range sections { + if header == "maximum depth" { + return content + } + } + return "" +} diff --git a/internal/report/table_helpers_test.go b/internal/report/table_helpers_test.go index b088666e..45e19f85 100644 --- a/internal/report/table_helpers_test.go +++ b/internal/report/table_helpers_test.go @@ -267,203 +267,6 @@ On-line CPU(s) list: 0-7,32-39 } } -func TestGetFrequenciesFromMSR(t *testing.T) { - tests := []struct { - name string - msr string - want []int - expectErr bool - }{ - { - name: "Valid MSR with multiple frequencies", - msr: "0x1A2B3C4D", - want: []int{0x4D, 0x3C, 0x2B, 0x1A}, - expectErr: false, - }, - { - name: "Valid MSR with single frequency", - msr: "0x1A", - want: []int{0x1A}, - expectErr: false, - }, - { - name: "Empty MSR string", - msr: "", - want: nil, - expectErr: true, - }, - { - name: "Invalid MSR string", - msr: "invalid_hex", - want: nil, - expectErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := getFrequenciesFromHex(tt.msr) - if (err != nil) != tt.expectErr { - t.Errorf("getFrequenciesFromMSR() error = %v, expectErr %v", err, tt.expectErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("getFrequenciesFromMSR() = %v, want %v", got, tt.want) - } - }) - } -} -func TestGetBucketSizesFromMSR(t *testing.T) { - tests := []struct { - name string - msr string - want []int - expectErr bool - }{ - { - name: "Valid MSR with 8 bucket sizes", - msr: "0x0102030405060708", - want: []int{8, 7, 6, 5, 4, 3, 2, 1}, - expectErr: false, - }, - { - name: "Valid MSR with reversed order", - msr: "0x0807060504030201", - want: []int{1, 2, 3, 4, 5, 6, 7, 8}, - expectErr: false, - }, - { - name: "Invalid MSR string", - msr: "invalid_hex", - want: nil, - expectErr: true, - }, - { - name: "MSR with less than 8 bucket sizes", - msr: "0x01020304", - want: nil, - expectErr: true, - }, - { - name: "MSR with more than 8 bucket sizes", - msr: "0x010203040506070809", - want: nil, - expectErr: true, - }, - { - name: "Empty MSR string", - msr: "", - want: nil, - expectErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := getBucketSizesFromHex(tt.msr) - if (err != nil) != tt.expectErr { - t.Errorf("getBucketSizesFromMSR() error = %v, expectErr %v", err, tt.expectErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("getBucketSizesFromMSR() = %v, want %v", got, tt.want) - } - }) - } -} -func TestExpandTurboFrequencies(t *testing.T) { - tests := []struct { - name string - buckets [][]string - isa string - want []string - expectErr bool - }{ - { - name: "Valid input with single bucket", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-4", "3.5", "3.2"}, - }, - isa: "SSE", - want: []string{"3.5", "3.5", "3.5", "3.5"}, - expectErr: false, - }, - { - name: "Valid input with multiple buckets", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-2", "3.5", "3.2"}, - {"3-4", "3.6", "3.3"}, - }, - isa: "SSE", - want: []string{"3.5", "3.5", "3.6", "3.6"}, - expectErr: false, - }, - { - name: "ISA column not found", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-4", "3.5", "3.2"}, - }, - isa: "AVX512", - want: nil, - expectErr: true, - }, - { - name: "Empty buckets", - buckets: [][]string{ - {}, - }, - isa: "SSE", - want: nil, - expectErr: true, - }, - { - name: "Invalid bucket range", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-", "3.5", "3.2"}, - }, - isa: "SSE", - want: nil, - expectErr: true, - }, - { - name: "Empty frequency value", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {"1-4", "", "3.2"}, - }, - isa: "SSE", - want: nil, - expectErr: true, - }, - { - name: "Whitespace in bucket range", - buckets: [][]string{ - {"Cores", "SSE", "AVX2"}, - {" 1-4 ", "3.5", "3.2"}, - }, - isa: "SSE", - want: []string{"3.5", "3.5", "3.5", "3.5"}, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := expandTurboFrequencies(tt.buckets, tt.isa) - if (err != nil) != tt.expectErr { - t.Errorf("expandTurboFrequencies() error = %v, expectErr %v", err, tt.expectErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("expandTurboFrequencies() = %v, want %v", got, tt.want) - } - }) - } -} func TestGetSectionsFromOutput(t *testing.T) { tests := []struct { name string