diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 410b5cdce51e..a282921a5110 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -458,7 +458,7 @@ func comparePorts(i, j container.PortSummary) bool { } if i.IP != j.IP { - return i.IP.String() < j.IP.String() + return i.IP.Less(j.IP) } if i.PublicPort != j.PublicPort { diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 1c68aeaea059..e2f3fc76a0ba 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "net/netip" + "strconv" "strings" "testing" "time" @@ -946,6 +947,24 @@ func TestDisplayablePorts(t *testing.T) { }, expected: "80/tcp, 80/udp, 1024/tcp, 1024/udp, 12345/sctp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp", //nolint:revive // ignore line-length-limit (revive) }, + { + // host IPs are ordered numerically, not lexicographically: + // "10.0.0.2" sorts as a string before "9.0.0.1". + ports: []container.PortSummary{ + { + IP: netip.MustParseAddr("10.0.0.2"), + PublicPort: 8080, + PrivatePort: 80, + Type: "tcp", + }, { + IP: netip.MustParseAddr("9.0.0.1"), + PublicPort: 8081, + PrivatePort: 80, + Type: "tcp", + }, + }, + expected: "9.0.0.1:8081->80/tcp, 10.0.0.2:8080->80/tcp", + }, } for _, port := range cases { @@ -953,3 +972,47 @@ func TestDisplayablePorts(t *testing.T) { assert.Check(t, is.Equal(port.expected, actual)) } } + +func BenchmarkDisplayablePorts(b *testing.B) { + // Every port shares a container port, so + // each comparison falls through to the + // host-IP comparison. + // Descending order keeps the input unsorted. + const ( + sharedContainerPort = 80 + firstHostPort = 30000 + ) + + // Distinct host IP per port: 10.0.0.1, 10.0.0.2, ... + hostIP := func(n int) netip.Addr { + return netip.AddrFrom4([4]byte{10, 0, byte(n >> 8), byte(n)}) + } + + for _, numPorts := range []int{64, 256} { + b.Run(strconv.Itoa(numPorts), func(b *testing.B) { + ports := make([]container.PortSummary, numPorts) + + for i := range ports { + n := numPorts - i + ports[i] = container.PortSummary{ + IP: hostIP(n), + PublicPort: uint16(firstHostPort + n), + PrivatePort: sharedContainerPort, + Type: "tcp", + } + } + + scratch := make([]container.PortSummary, numPorts) + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + // DisplayablePorts sorts in place; + // restore the unsorted input. + copy(scratch, ports) + + _ = DisplayablePorts(scratch) + } + }) + } +}