diff --git a/.gitignore b/.gitignore index 3a7a191d..1b3b8149 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /internal/script/resources/aarch64 /test /__debug_bin*.log +/internal/script/resources/README.md diff --git a/internal/report/table_defs.go b/internal/report/table_defs.go index 27c41954..2050e38a 100644 --- a/internal/report/table_defs.go +++ b/internal/report/table_defs.go @@ -1607,14 +1607,15 @@ func nicTableValues(outputs map[string]script.ScriptOutput) []Field { {Name: "Name"}, {Name: "Vendor (ID)"}, {Name: "Model (ID)"}, + {Name: "MAC Address"}, {Name: "Speed"}, {Name: "Link"}, {Name: "Bus"}, + {Name: "Card / Port"}, + {Name: "NUMA Node"}, {Name: "Driver"}, {Name: "Driver Version"}, {Name: "Firmware Version"}, - {Name: "MAC Address"}, - {Name: "NUMA Node"}, {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."}, @@ -1631,19 +1632,25 @@ func nicTableValues(outputs map[string]script.ScriptOutput) []Field { if nicInfo.ModelID != "" { fields[2].Values[len(fields[2].Values)-1] += fmt.Sprintf(" (%s)", nicInfo.ModelID) } - fields[3].Values = append(fields[3].Values, nicInfo.Speed) - fields[4].Values = append(fields[4].Values, nicInfo.Link) - fields[5].Values = append(fields[5].Values, nicInfo.Bus) - fields[6].Values = append(fields[6].Values, nicInfo.Driver) - fields[7].Values = append(fields[7].Values, nicInfo.DriverVersion) - fields[8].Values = append(fields[8].Values, nicInfo.FirmwareVersion) - fields[9].Values = append(fields[9].Values, nicInfo.MACAddress) - fields[10].Values = append(fields[10].Values, nicInfo.NUMANode) - fields[11].Values = append(fields[11].Values, nicInfo.IRQBalance) - fields[12].Values = append(fields[12].Values, nicInfo.AdaptiveRX) - fields[13].Values = append(fields[13].Values, nicInfo.AdaptiveTX) - fields[14].Values = append(fields[14].Values, nicInfo.RxUsecs) - fields[15].Values = append(fields[15].Values, nicInfo.TxUsecs) + fields[3].Values = append(fields[3].Values, nicInfo.MACAddress) + fields[4].Values = append(fields[4].Values, nicInfo.Speed) + fields[5].Values = append(fields[5].Values, nicInfo.Link) + fields[6].Values = append(fields[6].Values, nicInfo.Bus) + // Add Card / Port column + cardPort := "" + if nicInfo.Card != "" && nicInfo.Port != "" { + cardPort = nicInfo.Card + " / " + nicInfo.Port + } + fields[7].Values = append(fields[7].Values, cardPort) + fields[8].Values = append(fields[8].Values, nicInfo.NUMANode) + 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) } return fields } diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go index 2c016331..2052545e 100644 --- a/internal/report/table_helpers.go +++ b/internal/report/table_helpers.go @@ -1233,6 +1233,8 @@ type nicInfo struct { AdaptiveTX string RxUsecs string TxUsecs string + Card string + Port string } func parseNicInfo(scriptOutput string) []nicInfo { @@ -1284,9 +1286,82 @@ func parseNicInfo(scriptOutput string) []nicInfo { nic.Model = strings.TrimSpace(strings.Split(nic.Model, "(")[0]) nics = append(nics, nic) } + // Assign card and port information + assignCardAndPort(nics) return nics } +// assignCardAndPort assigns card and port numbers to NICs based on their PCI addresses +func assignCardAndPort(nics []nicInfo) { + if len(nics) == 0 { + return + } + + // Map to store card identifiers (domain:bus:device) to card numbers + cardMap := make(map[string]int) + // Map to track ports within each card + portMap := make(map[string][]int) // card identifier -> list of indices in nics slice + cardCounter := 1 + + // First pass: identify cards and group NICs by card + for i := range nics { + if nics[i].Bus == "" { + continue + } + // PCI address format: domain:bus:device.function (e.g., 0000:32:00.0) + // Extract domain:bus:device as the card identifier + parts := strings.Split(nics[i].Bus, ":") + if len(parts) != 3 { + continue + } + // Further split the last part to separate device from function + deviceFunc := strings.Split(parts[2], ".") + if len(deviceFunc) != 2 { + continue + } + // Card identifier is domain:bus:device + cardID := parts[0] + ":" + parts[1] + ":" + deviceFunc[0] + + // Assign card number if not already assigned + if _, exists := cardMap[cardID]; !exists { + cardMap[cardID] = cardCounter + cardCounter++ + } + // Add this NIC index to the card's port list + portMap[cardID] = append(portMap[cardID], i) + } + + // Second pass: assign card and port numbers + for cardID, nicIndices := range portMap { + cardNum := cardMap[cardID] + // Sort NICs within a card by their function number + sort.Slice(nicIndices, func(i, j int) bool { + // Extract function numbers + funcI := extractFunction(nics[nicIndices[i]].Bus) + funcJ := extractFunction(nics[nicIndices[j]].Bus) + return funcI < funcJ + }) + // Assign port numbers + for portNum, nicIdx := range nicIndices { + nics[nicIdx].Card = fmt.Sprintf("%d", cardNum) + nics[nicIdx].Port = fmt.Sprintf("%d", portNum+1) + } + } +} + +// extractFunction extracts the function number from a PCI address +func extractFunction(busAddr string) int { + parts := strings.Split(busAddr, ".") + if len(parts) != 2 { + return 0 + } + funcNum, err := strconv.Atoi(parts[1]) + if err != nil { + return 0 + } + return funcNum +} + type diskInfo struct { Name string Model string diff --git a/internal/report/table_helpers_nic_integration_test.go b/internal/report/table_helpers_nic_integration_test.go new file mode 100644 index 00000000..591cffe6 --- /dev/null +++ b/internal/report/table_helpers_nic_integration_test.go @@ -0,0 +1,204 @@ +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 new file mode 100644 index 00000000..a5742b44 --- /dev/null +++ b/internal/report/table_helpers_nic_test.go @@ -0,0 +1,104 @@ +package report + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "testing" +) + +func TestAssignCardAndPort(t *testing.T) { + tests := []struct { + name string + nics []nicInfo + expected map[string]string // map of NIC name to expected "Card / Port" + }{ + { + name: "Two cards with two ports each", + nics: []nicInfo{ + {Name: "eth2", Bus: "0000:32:00.0"}, + {Name: "eth3", Bus: "0000:32:00.1"}, + {Name: "eth0", Bus: "0000:c0:00.0"}, + {Name: "eth1", Bus: "0000:c0:00.1"}, + }, + expected: map[string]string{ + "eth2": "1 / 1", + "eth3": "1 / 2", + "eth0": "2 / 1", + "eth1": "2 / 2", + }, + }, + { + name: "Single card with four ports", + nics: []nicInfo{ + {Name: "eth0", Bus: "0000:19:00.0"}, + {Name: "eth1", Bus: "0000:19:00.1"}, + {Name: "eth2", Bus: "0000:19:00.2"}, + {Name: "eth3", Bus: "0000:19:00.3"}, + }, + expected: map[string]string{ + "eth0": "1 / 1", + "eth1": "1 / 2", + "eth2": "1 / 3", + "eth3": "1 / 4", + }, + }, + { + name: "Three different cards", + nics: []nicInfo{ + {Name: "eth0", Bus: "0000:19:00.0"}, + {Name: "eth1", Bus: "0000:1a:00.0"}, + {Name: "eth2", Bus: "0000:1b:00.0"}, + }, + expected: map[string]string{ + "eth0": "1 / 1", + "eth1": "2 / 1", + "eth2": "3 / 1", + }, + }, + { + name: "Empty bus address should not assign card/port", + nics: []nicInfo{ + {Name: "eth0", Bus: ""}, + }, + expected: map[string]string{ + "eth0": " / ", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assignCardAndPort(tt.nics) + for _, nic := range tt.nics { + expected := tt.expected[nic.Name] + actual := nic.Card + " / " + nic.Port + if actual != expected { + t.Errorf("NIC %s: expected %q, got %q", nic.Name, expected, actual) + } + } + }) + } +} + +func TestExtractFunction(t *testing.T) { + tests := []struct { + busAddr string + expected int + }{ + {"0000:32:00.0", 0}, + {"0000:32:00.1", 1}, + {"0000:32:00.3", 3}, + {"invalid", 0}, + {"", 0}, + } + + for _, tt := range tests { + t.Run(tt.busAddr, func(t *testing.T) { + result := extractFunction(tt.busAddr) + if result != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, result) + } + }) + } +}