diff --git a/cmd/report/report.go b/cmd/report/report.go index ce008cdb..d3cbf2e2 100644 --- a/cmd/report/report.go +++ b/cmd/report/report.go @@ -171,7 +171,7 @@ var categories = []common.Category{ {FlagName: flagElcName, FlagVar: &flagElc, Help: "Efficiency Latency Control Settings", TableNames: []string{report.ElcTableName}}, {FlagName: flagMemoryName, FlagVar: &flagMemory, Help: "Memory Configuration", TableNames: []string{report.MemoryTableName}}, {FlagName: flagDimmName, FlagVar: &flagDimm, Help: "DIMM Population", TableNames: []string{report.DIMMTableName}}, - {FlagName: flagNicName, FlagVar: &flagNic, Help: "Network Cards", TableNames: []string{report.NICTableName}}, + {FlagName: flagNicName, FlagVar: &flagNic, Help: "Network Cards", TableNames: []string{report.NICTableName, report.NICPacketSteeringTableName}}, {FlagName: flagNetConfigName, FlagVar: &flagNetConfig, Help: "Network Configuration", TableNames: []string{report.NetworkConfigTableName}}, {FlagName: flagNetIrqName, FlagVar: &flagNetIrq, Help: "Network IRQ to CPU Mapping", TableNames: []string{report.NetworkIRQMappingTableName}}, {FlagName: flagDiskName, FlagVar: &flagDisk, Help: "Storage Devices", TableNames: []string{report.DiskTableName}}, diff --git a/internal/report/table_defs.go b/internal/report/table_defs.go index f42d6751..27ebabe1 100644 --- a/internal/report/table_defs.go +++ b/internal/report/table_defs.go @@ -11,6 +11,7 @@ import ( "log/slog" "regexp" "slices" + "sort" "strconv" "strings" "time" @@ -94,6 +95,7 @@ const ( NICTableName = "NIC" NetworkIRQMappingTableName = "Network IRQ Mapping" NetworkConfigTableName = "Network Configuration" + NICPacketSteeringTableName = "NIC Packet Steering" DiskTableName = "Disk" FilesystemTableName = "Filesystem" GPUTableName = "GPU" @@ -380,6 +382,13 @@ var tableDefinitions = map[string]TableDefinition{ script.SysctlScriptName, }, FieldsFunc: networkConfigTableValues}, + NICPacketSteeringTableName: { + Name: NICPacketSteeringTableName, + HasRows: true, + ScriptNames: []string{ + script.NicInfoScriptName, + }, + FieldsFunc: nicPacketSteeringTableValues}, NetworkIRQMappingTableName: { Name: NetworkIRQMappingTableName, HasRows: true, @@ -1625,6 +1634,9 @@ func nicTableValues(outputs map[string]script.ScriptOutput) []Field { {Name: "Driver"}, {Name: "Driver Version"}, {Name: "Firmware Version"}, + {Name: "MTU", Description: "Maximum Transmission Unit. The largest size packet or frame, specified in octets (eight-bit bytes), that can be sent in a packet- or frame-based network such as the Internet."}, + {Name: "TX Queues"}, + {Name: "RX Queues"}, {Name: "IRQBalance", Description: "System level setting. Dynamically monitors system activity and spreads IRQs across available cores, aiming to balance CPU load, improve throughput, and reduce latency for interrupt-heavy workloads."}, {Name: "Adaptive RX", Description: "Enables dynamic adjustment of receive interrupt coalescing based on traffic patterns."}, {Name: "Adaptive TX", Description: "Enables dynamic adjustment of transmit interrupt coalescing based on traffic patterns."}, @@ -1660,15 +1672,80 @@ func nicTableValues(outputs map[string]script.ScriptOutput) []Field { fields[9].Values = append(fields[9].Values, nicInfo.Driver) fields[10].Values = append(fields[10].Values, nicInfo.DriverVersion) fields[11].Values = append(fields[11].Values, nicInfo.FirmwareVersion) - fields[12].Values = append(fields[12].Values, nicInfo.IRQBalance) - fields[13].Values = append(fields[13].Values, nicInfo.AdaptiveRX) - fields[14].Values = append(fields[14].Values, nicInfo.AdaptiveTX) - fields[15].Values = append(fields[15].Values, nicInfo.RxUsecs) - fields[16].Values = append(fields[16].Values, nicInfo.TxUsecs) + fields[12].Values = append(fields[12].Values, nicInfo.MTU) + fields[13].Values = append(fields[13].Values, nicInfo.TXQueues) + fields[14].Values = append(fields[14].Values, nicInfo.RXQueues) + fields[15].Values = append(fields[15].Values, nicInfo.IRQBalance) + fields[16].Values = append(fields[16].Values, nicInfo.AdaptiveRX) + fields[17].Values = append(fields[17].Values, nicInfo.AdaptiveTX) + fields[18].Values = append(fields[18].Values, nicInfo.RxUsecs) + fields[19].Values = append(fields[19].Values, nicInfo.TxUsecs) } return fields } +func nicPacketSteeringTableValues(outputs map[string]script.ScriptOutput) []Field { + allNicsInfo := parseNicInfo(outputs[script.NicInfoScriptName].Stdout) + if len(allNicsInfo) == 0 { + return []Field{} + } + + fields := []Field{ + {Name: "Interface"}, + {Name: "Type", Description: "XPS (Transmit Packet Steering) and RPS (Receive Packet Steering) are software-based mechanisms that allow the selection of a specific logical CPU core to handle the transmission or processing of network packets for a given queue."}, + {Name: "Queue:CPU(s) | Queue|CPU(s) | ..."}, + } + + for _, nicInfo := range allNicsInfo { + // XPS row + if nicInfo.TXQueues != "0" { + fields[0].Values = append(fields[0].Values, nicInfo.Name) + fields[1].Values = append(fields[1].Values, "xps_cpus") + fields[2].Values = append(fields[2].Values, formatQueueCPUMappings(nicInfo.XPSCPUs, "tx-")) + } + + // RPS row + if nicInfo.RXQueues != "0" { + fields[0].Values = append(fields[0].Values, nicInfo.Name) + fields[1].Values = append(fields[1].Values, "rps_cpus") + fields[2].Values = append(fields[2].Values, formatQueueCPUMappings(nicInfo.RPSCPUs, "rx-")) + } + } + + if len(fields[0].Values) == 0 { + return []Field{} + } + return fields +} + +func formatQueueCPUMappings(mappings map[string]string, prefix string) string { + var queueMappings []string + + // Extract and sort queue numbers to ensure consistent output + var queues []int + for queueStr := range mappings { + queueNum, err := strconv.Atoi(strings.TrimPrefix(queueStr, prefix)) + if err == nil { + queues = append(queues, queueNum) + } + } + sort.Ints(queues) + + for _, queueNum := range queues { + queueStr := fmt.Sprintf("%s%d", prefix, queueNum) + cpus := mappings[queueStr] + // a nil value can be returned from the map if the key does not exist, so check for that + if cpus != "" { + queueMappings = append(queueMappings, fmt.Sprintf("%d:%s", queueNum, cpus)) + } + } + + if len(queueMappings) == 0 { + return "N/A" + } + return strings.Join(queueMappings, " | ") +} + func networkIRQMappingTableValues(outputs map[string]script.ScriptOutput) []Field { nicIRQMappings := nicIRQMappingsFromOutput(outputs) if len(nicIRQMappings) == 0 { diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index 63c84e0d..8e9eb752 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -9,6 +9,7 @@ import ( "encoding/csv" "fmt" "log/slog" + "math/big" "regexp" "sort" "strconv" @@ -1260,7 +1261,12 @@ type nicInfo struct { TxUsecs string Card string Port string + MTU string IsVirtual bool + TXQueues string + RXQueues string + XPSCPUs map[string]string + RPSCPUs map[string]string } func parseNicInfo(scriptOutput string) []nicInfo { @@ -1270,6 +1276,8 @@ func parseNicInfo(scriptOutput string) []nicInfo { continue } var nic nicInfo + nic.XPSCPUs = make(map[string]string) + nic.RPSCPUs = make(map[string]string) // Map of prefixes to field pointers fieldMap := map[string]*string{ "Interface: ": &nic.Name, @@ -1289,6 +1297,9 @@ func parseNicInfo(scriptOutput string) []nicInfo { "IRQ Balance: ": &nic.IRQBalance, "rx-usecs: ": &nic.RxUsecs, "tx-usecs: ": &nic.TxUsecs, + "MTU: ": &nic.MTU, + "TX Queues: ": &nic.TXQueues, + "RX Queues: ": &nic.RXQueues, } for line := range strings.SplitSeq(nicOutput, "\n") { line = strings.TrimSpace(line) @@ -1306,6 +1317,23 @@ func parseNicInfo(scriptOutput string) []nicInfo { nic.IsVirtual = (strings.TrimSpace(value) == "yes") continue } + // Special parsing for xps_cpus and rps_cpus + if strings.HasPrefix(line, "xps_cpus tx-") { + parts := strings.SplitN(line, ": ", 2) + if len(parts) == 2 { + queue := strings.TrimPrefix(parts[0], "xps_cpus ") + nic.XPSCPUs[queue] = hexBitmapToCPUList(parts[1]) + } + continue + } + if strings.HasPrefix(line, "rps_cpus rx-") { + parts := strings.SplitN(line, ": ", 2) + if len(parts) == 2 { + queue := strings.TrimPrefix(parts[0], "rps_cpus ") + nic.RPSCPUs[queue] = hexBitmapToCPUList(parts[1]) + } + continue + } for prefix, fieldPtr := range fieldMap { if after, ok := strings.CutPrefix(line, prefix); ok { *fieldPtr = after @@ -1322,6 +1350,35 @@ func parseNicInfo(scriptOutput string) []nicInfo { return nics } +func hexBitmapToCPUList(hexBitmap string) string { + if hexBitmap == "" { + return "" + } + + // Remove commas to form a single continuous hex string. + // This assumes the comma-separated parts are in big-endian order. + fullHexBitmap := strings.ReplaceAll(hexBitmap, ",", "") + + i := new(big.Int) + // The string is a hex string, so the base is 16. + if _, success := i.SetString(fullHexBitmap, 16); !success { + // If parsing fails, it might not be a hex string. Return as is. + return hexBitmap + } + + var cpus []string + // Iterate through the bits of the big integer. + for bit := 0; bit < i.BitLen(); bit++ { + if i.Bit(bit) == 1 { + cpus = append(cpus, fmt.Sprintf("%d", bit)) + } + } + if len(cpus) == 0 { + return "" + } + return strings.Join(cpus, ",") +} + // assignCardAndPort assigns card and port numbers to NICs based on their PCI addresses func assignCardAndPort(nics []nicInfo) { if len(nics) == 0 { diff --git a/internal/script/script_defs.go b/internal/script/script_defs.go index c506d3af..7dabee94 100644 --- a/internal/script/script_defs.go +++ b/internal/script/script_defs.go @@ -758,6 +758,7 @@ rdmsr 0x2FFE echo "Model ID: $(echo "$udevadm_out" | grep ID_MODEL_ID= | cut -d'=' -f2)" echo "Vendor: $(echo "$udevadm_out" | grep ID_VENDOR_FROM_DATABASE= | cut -d'=' -f2)" echo "Model: $(echo "$udevadm_out" | grep ID_MODEL_FROM_DATABASE= | cut -d'=' -f2)" + echo "MTU: $(cat /sys/class/net/"$ifc"/mtu 2>/dev/null)" echo "$ethtool_out" echo "$ethtool_i_out" if ethtool_c_out=$(ethtool -c "$ifc" 2>/dev/null); then @@ -779,6 +780,18 @@ rdmsr 0x2FFE done printf "\n" echo "IRQ Balance: $(pgrep irqbalance >/dev/null 2>&1 && echo "Enabled" || echo "Disabled")" + echo "TX Queues: $(ls -d /sys/class/net/"$ifc"/queues/tx-* | wc -l)" + echo "RX Queues: $(ls -d /sys/class/net/"$ifc"/queues/rx-* | wc -l)" + for q in /sys/class/net/"$ifc"/queues/tx-*; do + if [ -f "$q/xps_cpus" ]; then + echo "xps_cpus $(basename "$q"): $(cat "$q/xps_cpus")" + fi + done + for q in /sys/class/net/"$ifc"/queues/rx-*; do + if [ -f "$q/rps_cpus" ]; then + echo "rps_cpus $(basename "$q"): $(cat "$q/rps_cpus")" + fi + done echo "----------------------------------------" done `,