⚡ Bolt: Pre-parse IP addresses before sorting discovery sweep results - #130
⚡ Bolt: Pre-parse IP addresses before sorting discovery sweep results#130blue4209211 wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request optimizes the IP address sorting logic in the host discovery sweep by pre-parsing IP strings into netip.Addr structs before sorting, avoiding expensive string parsing within the comparator function. It also adds a performance journal entry and a benchmark to measure sorting performance. The feedback suggests further optimizing this by storing pointers to SweepHost rather than copying the entire 96-byte struct during sorting swaps, and updating the benchmark accordingly.
| type sweepHostAddr struct { | ||
| host SweepHost | ||
| addr netip.Addr | ||
| } | ||
| items := make([]sweepHostAddr, 0, len(hosts)) | ||
| for _, h := range hosts { | ||
| out = append(out, *h) | ||
| addr, err := netip.ParseAddr(h.IP) | ||
| if err != nil { | ||
| items = append(items, sweepHostAddr{host: *h}) | ||
| continue | ||
| } | ||
| items = append(items, sweepHostAddr{host: *h, addr: addr}) | ||
| } | ||
| sort.Slice(items, func(i, j int) bool { | ||
| if !items[i].addr.IsValid() || !items[j].addr.IsValid() { | ||
| return items[i].host.IP < items[j].host.IP | ||
| } | ||
| return items[i].addr.Less(items[j].addr) | ||
| }) | ||
|
|
||
| out := make([]SweepHost, len(items)) | ||
| for i, item := range items { | ||
| out[i] = item.host | ||
| } |
There was a problem hiding this comment.
Storing the entire SweepHost struct in sweepHostAddr causes the 96-byte struct to be copied multiple times during sort.Slice swaps. Since hosts is a map of pointers (map[string]*SweepHost), we can store the pointer *SweepHost instead. This reduces the size of sweepHostAddr from 120 bytes to 32 bytes, significantly speeding up the sorting process by avoiding expensive struct copies.
type sweepHostAddr struct {
host *SweepHost
addr netip.Addr
}
items := make([]sweepHostAddr, 0, len(hosts))
for _, h := range hosts {
addr, err := netip.ParseAddr(h.IP)
if err != nil {
items = append(items, sweepHostAddr{host: h})
continue
}
items = append(items, sweepHostAddr{host: h, addr: addr})
}
sort.Slice(items, func(i, j int) bool {
if !items[i].addr.IsValid() || !items[j].addr.IsValid() {
return items[i].host.IP < items[j].host.IP
}
return items[i].addr.Less(items[j].addr)
})
out := make([]SweepHost, len(items))
for i, item := range items {
out[i] = *item.host
}| type sweepHostAddr struct { | ||
| host SweepHost | ||
| addr netip.Addr | ||
| } | ||
| items := make([]sweepHostAddr, 0, len(hosts)) | ||
| for _, h := range hosts { | ||
| addr, err := netip.ParseAddr(h.IP) | ||
| if err != nil { | ||
| items = append(items, sweepHostAddr{host: *h}) | ||
| continue | ||
| } | ||
| items = append(items, sweepHostAddr{host: *h, addr: addr}) | ||
| } | ||
| sort.Slice(items, func(i, j int) bool { | ||
| if !items[i].addr.IsValid() || !items[j].addr.IsValid() { | ||
| return items[i].host.IP < items[j].host.IP | ||
| } | ||
| return items[i].addr.Less(items[j].addr) | ||
| }) | ||
| out := make([]SweepHost, len(items)) | ||
| for i, item := range items { | ||
| out[i] = item.host | ||
| } |
There was a problem hiding this comment.
Update the benchmark to use the pointer-based sweepHostAddr struct to match the optimized implementation and ensure accurate performance measurements.
type sweepHostAddr struct {
host *SweepHost
addr netip.Addr
}
items := make([]sweepHostAddr, 0, len(hosts))
for _, h := range hosts {
addr, err := netip.ParseAddr(h.IP)
if err != nil {
items = append(items, sweepHostAddr{host: h})
continue
}
items = append(items, sweepHostAddr{host: h, addr: addr})
}
sort.Slice(items, func(i, j int) bool {
if !items[i].addr.IsValid() || !items[j].addr.IsValid() {
return items[i].host.IP < items[j].host.IP
}
return items[i].addr.Less(items[j].addr)
})
out := make([]SweepHost, len(items))
for i, item := range items {
out[i] = *item.host
}|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request optimizes the sorting of sweep results by pre-parsing IP addresses into netip.Addr structs once before sorting, rather than parsing them repeatedly inside the sort comparator. It also adds a benchmark to measure sorting performance. The review feedback points out that the new sort comparator violates the strict weak ordering requirement of sort.Slice when handling a mix of valid and invalid IP addresses, which can lead to undefined behavior or panics. Corrected comparator implementations are suggested for both the main code and the benchmark.
| sort.Slice(items, func(i, j int) bool { | ||
| if !items[i].addr.IsValid() || !items[j].addr.IsValid() { | ||
| return items[i].host.IP < items[j].host.IP | ||
| } | ||
| return items[i].addr.Less(items[j].addr) | ||
| }) |
There was a problem hiding this comment.
The comparator function violates the strict weak ordering requirement of sort.Slice when dealing with a mix of valid and invalid IP addresses.
Specifically, if one IP is invalid and the other is valid, comparing them using their raw string values (items[i].host.IP < items[j].host.IP) can create comparison cycles (e.g., A < C < B < A), violating transitivity. This can lead to unpredictable sorting behavior or incorrect ordering.
To fix this, ensure that invalid IP addresses are consistently sorted to one end (e.g., the end of the slice) and compared using string comparison only when both are invalid.
sort.Slice(items, func(i, j int) bool {
iValid := items[i].addr.IsValid()
jValid := items[j].addr.IsValid()
if !iValid && !jValid {
return items[i].host.IP < items[j].host.IP
}
if !iValid {
return false
}
if !jValid {
return true
}
return items[i].addr.Less(items[j].addr)
})| sort.Slice(items, func(i, j int) bool { | ||
| if !items[i].addr.IsValid() || !items[j].addr.IsValid() { | ||
| return items[i].host.IP < items[j].host.IP | ||
| } | ||
| return items[i].addr.Less(items[j].addr) | ||
| }) |
There was a problem hiding this comment.
The comparator function in the benchmark also violates the strict weak ordering requirement when handling invalid IP addresses. It should be updated to match the corrected comparator logic used in the main implementation.
sort.Slice(items, func(i, j int) bool {
iValid := items[i].addr.IsValid()
jValid := items[j].addr.IsValid()
if !iValid && !jValid {
return items[i].host.IP < items[j].host.IP
}
if !iValid {
return false
}
if !jValid {
return true
}
return items[i].addr.Less(items[j].addr)
})|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request optimizes the IP address sorting logic in runSweep by pre-parsing IP strings into netip.Addr structs once before sorting, preventing redundant parses inside the comparator. It also documents this optimization in a performance journal and adds a benchmark. The review feedback suggests refactoring this inline sorting logic into a dedicated sortHosts helper function to eliminate code duplication and allow the benchmark to test the production implementation directly.
| // Pre-parse IP addresses once to avoid O(N log N) string parses in sort comparator. | ||
| // Store pointers (*SweepHost) to keep sweepHostAddr small (32 bytes instead of 120 bytes) | ||
| // and eliminate struct copying overhead during sort.Slice swaps. | ||
| type sweepHostAddr struct { | ||
| host *SweepHost | ||
| addr netip.Addr | ||
| } | ||
| items := make([]sweepHostAddr, 0, len(hosts)) | ||
| for _, h := range hosts { | ||
| out = append(out, *h) | ||
| addr, err := netip.ParseAddr(h.IP) | ||
| if err != nil { | ||
| items = append(items, sweepHostAddr{host: h}) | ||
| continue | ||
| } | ||
| items = append(items, sweepHostAddr{host: h, addr: addr}) | ||
| } | ||
| sort.Slice(items, func(i, j int) bool { | ||
| iValid := items[i].addr.IsValid() | ||
| jValid := items[j].addr.IsValid() | ||
| if !iValid && !jValid { | ||
| return items[i].host.IP < items[j].host.IP | ||
| } | ||
| if !iValid { | ||
| return false | ||
| } | ||
| if !jValid { | ||
| return true | ||
| } | ||
| return items[i].addr.Less(items[j].addr) | ||
| }) | ||
|
|
||
| out := make([]SweepHost, len(items)) | ||
| for i, item := range items { | ||
| out[i] = *item.host | ||
| } |
There was a problem hiding this comment.
Instead of inlining the complex sorting logic directly inside runSweep, we should extract it into a dedicated helper function, e.g., sortHosts(hosts map[string]*SweepHost) []SweepHost. This avoids code duplication and allows the benchmark in sweep_test.go to directly test the production implementation rather than a copy-pasted version of it.
| // Pre-parse IP addresses once to avoid O(N log N) string parses in sort comparator. | |
| // Store pointers (*SweepHost) to keep sweepHostAddr small (32 bytes instead of 120 bytes) | |
| // and eliminate struct copying overhead during sort.Slice swaps. | |
| type sweepHostAddr struct { | |
| host *SweepHost | |
| addr netip.Addr | |
| } | |
| items := make([]sweepHostAddr, 0, len(hosts)) | |
| for _, h := range hosts { | |
| out = append(out, *h) | |
| addr, err := netip.ParseAddr(h.IP) | |
| if err != nil { | |
| items = append(items, sweepHostAddr{host: h}) | |
| continue | |
| } | |
| items = append(items, sweepHostAddr{host: h, addr: addr}) | |
| } | |
| sort.Slice(items, func(i, j int) bool { | |
| iValid := items[i].addr.IsValid() | |
| jValid := items[j].addr.IsValid() | |
| if !iValid && !jValid { | |
| return items[i].host.IP < items[j].host.IP | |
| } | |
| if !iValid { | |
| return false | |
| } | |
| if !jValid { | |
| return true | |
| } | |
| return items[i].addr.Less(items[j].addr) | |
| }) | |
| out := make([]SweepHost, len(items)) | |
| for i, item := range items { | |
| out[i] = *item.host | |
| } | |
| out := sortHosts(hosts) |
| @@ -272,15 +302,6 @@ func trimTrailingDot(s string) string { | |||
| return s | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
Define the helper function sortHosts here to encapsulate the pre-parsing and sorting logic. This keeps runSweep clean and allows the benchmark to reuse the exact same sorting implementation.
func sortHosts(hosts map[string]*SweepHost) []SweepHost {
type sweepHostAddr struct {
host *SweepHost
addr netip.Addr
}
items := make([]sweepHostAddr, len(hosts))
idx := 0
for _, h := range hosts {
addr, err := netip.ParseAddr(h.IP)
if err != nil {
items[idx] = sweepHostAddr{host: h}
} else {
items[idx] = sweepHostAddr{host: h, addr: addr}
}
idx++
}
sort.Slice(items, func(i, j int) bool {
iValid := items[i].addr.IsValid()
jValid := items[j].addr.IsValid()
if !iValid && !jValid {
return items[i].host.IP < items[j].host.IP
}
if !iValid {
return false
}
if !jValid {
return true
}
return items[i].addr.Less(items[j].addr)
})
out := make([]SweepHost, len(items))
for i, item := range items {
out[i] = *item.host
}
return out
}| type sweepHostAddr struct { | ||
| host *SweepHost | ||
| addr netip.Addr | ||
| } | ||
| items := make([]sweepHostAddr, 0, len(hosts)) | ||
| for _, h := range hosts { | ||
| addr, err := netip.ParseAddr(h.IP) | ||
| if err != nil { | ||
| items = append(items, sweepHostAddr{host: h}) | ||
| continue | ||
| } | ||
| items = append(items, sweepHostAddr{host: h, addr: addr}) | ||
| } | ||
| sort.Slice(items, func(i, j int) bool { | ||
| iValid := items[i].addr.IsValid() | ||
| jValid := items[j].addr.IsValid() | ||
| if !iValid && !jValid { | ||
| return items[i].host.IP < items[j].host.IP | ||
| } | ||
| if !iValid { | ||
| return false | ||
| } | ||
| if !jValid { | ||
| return true | ||
| } | ||
| return items[i].addr.Less(items[j].addr) | ||
| }) | ||
| out := make([]SweepHost, len(items)) | ||
| for i, item := range items { | ||
| out[i] = *item.host | ||
| } | ||
| _ = out |
There was a problem hiding this comment.
Description
⚡ Bolt Performance Optimization
Pre-parsed string IP addresses into
netip.Addrstructs once into a slice before sorting discovery sweep results in [pkg/proxy/discovery/sweep.go].💡 What
Updated
runSweepto pre-parse string IP addresses once into a wrapper slice prior to sorting, rather than repeatedly callingnetip.ParseAddrinsidesort.Slice.🎯 Why
In
runSweep, discovered hosts are sorted by IP address before being returned. Previously,compareIPswas invoked directly insidesort.Slice, causingnetip.ParseAddrto be executed on every single comparison in the O(N log N) sorting loop. For large sweeps (up to 65,536 hosts), this resulted in over 2,000,000 redundant string parsing calls and excessive allocations during result presentation.📊 Impact
netip.Addr.Lesscomparison inside the sort loop instead of parsing IP strings on each comparison.171 µs/op).Type of change
How Has This Been Tested?
Run the benchmark in
pkg/proxy/discovery/sweep_test.go:go test -bench=BenchmarkSweepResultSorting -benchmem ./pkg/proxy/discovery/...Checklist
make validatepasses (fmt + lint + test)