net/art: allow non-pointers as values

Values are still turned into pointers internally to maintain the
invariants of strideTable, but from the user's perspective it's
now possible to tbl.Insert(pfx, true) rather than
tbl.Insert(pfx, ptr.To(true)).

Updates #7781

Signed-off-by: David Anderson <danderson@tailscale.com>
pull/8937/head
David Anderson 1 year ago committed by Dave Anderson
parent bc0eb6b914
commit e92adfe5e4

@ -18,19 +18,6 @@ const (
debugStrideDelete = false debugStrideDelete = false
) )
// strideEntry is a strideTable entry.
type strideEntry[T any] struct {
// prefixIndex is the prefixIndex(...) value that caused this stride entry's
// value to be populated, or 0 if value is nil.
//
// We need to keep track of this because allot() uses it to determine
// whether an entry was propagated from a parent entry, or if it's a
// different independent route.
prefixIndex int
// value is the value associated with the strideEntry, if any.
value *T
}
// strideTable is a binary tree that implements an 8-bit routing table. // strideTable is a binary tree that implements an 8-bit routing table.
// //
// The leaves of the binary tree are host routes (/8s). Each parent is a // The leaves of the binary tree are host routes (/8s). Each parent is a
@ -54,7 +41,9 @@ type strideTable[T any] struct {
// paper, it's hijacked through sneaky C memory trickery to store // paper, it's hijacked through sneaky C memory trickery to store
// the refcount, but this is Go, where we don't store random bits // the refcount, but this is Go, where we don't store random bits
// in pointers lest we confuse the GC) // in pointers lest we confuse the GC)
entries [lastHostIndex + 1]strideEntry[T] //
// A nil value means no route matches the queried route.
entries [lastHostIndex + 1]*T
// children are the child tables of this table. Each child // children are the child tables of this table. Each child
// represents the address space within one of this table's host // represents the address space within one of this table's host
// routes (/8). // routes (/8).
@ -112,13 +101,6 @@ func (t *strideTable[T]) getOrCreateChild(addr uint8) (child *strideTable[T], cr
return ret, false return ret, false
} }
// getValAndChild returns both the prefix and child strideTable for
// addr. Both returned values can be nil if no entry of that type
// exists for addr.
func (t *strideTable[T]) getValAndChild(addr uint8) (*T, *strideTable[T]) {
return t.entries[hostIndex(addr)].value, t.children[addr]
}
// findFirstChild returns the first child strideTable in t, or nil if // findFirstChild returns the first child strideTable in t, or nil if
// t has no children. // t has no children.
func (t *strideTable[T]) findFirstChild() *strideTable[T] { func (t *strideTable[T]) findFirstChild() *strideTable[T] {
@ -130,21 +112,41 @@ func (t *strideTable[T]) findFirstChild() *strideTable[T] {
return nil return nil
} }
// hasPrefixRootedAt reports whether t.entries[idx] is the root node of
// a prefix.
func (t *strideTable[T]) hasPrefixRootedAt(idx int) bool {
val := t.entries[idx]
if val == nil {
return false
}
parentIdx := parentIndex(idx)
if parentIdx == 0 {
// idx is non-nil, and is at the 0/0 route position.
return true
}
if parent := t.entries[parentIdx]; val != parent {
// parent node in the tree isn't the same prefix, so idx must
// be a root.
return true
}
return false
}
// allot updates entries whose stored prefixIndex matches oldPrefixIndex, in the // allot updates entries whose stored prefixIndex matches oldPrefixIndex, in the
// subtree rooted at idx. Matching entries have their stored prefixIndex set to // subtree rooted at idx. Matching entries have their stored prefixIndex set to
// newPrefixIndex, and their value set to val. // newPrefixIndex, and their value set to val.
// //
// allot is the core of the ART algorithm, enabling efficient insertion/deletion // allot is the core of the ART algorithm, enabling efficient insertion/deletion
// while preserving very fast lookups. // while preserving very fast lookups.
func (t *strideTable[T]) allot(idx int, oldPrefixIndex, newPrefixIndex int, val *T) { func (t *strideTable[T]) allot(idx int, old, new *T) {
if t.entries[idx].prefixIndex != oldPrefixIndex { if t.entries[idx] != old {
// current prefixIndex isn't what we expect. This is a recursive call // current idx isn't what we expect. This is a recursive call
// that found a child subtree that already has a more specific route // that found a child subtree that already has a more specific
// installed. Don't touch it. // route installed. Don't touch it.
return return
} }
t.entries[idx].value = val t.entries[idx] = new
t.entries[idx].prefixIndex = newPrefixIndex
if idx >= firstHostIndex { if idx >= firstHostIndex {
// The entry we just updated was a host route, we're at the bottom of // The entry we just updated was a host route, we're at the bottom of
// the binary tree. // the binary tree.
@ -152,51 +154,73 @@ func (t *strideTable[T]) allot(idx int, oldPrefixIndex, newPrefixIndex int, val
} }
// Propagate the allotment to this node's children. // Propagate the allotment to this node's children.
left := idx << 1 left := idx << 1
t.allot(left, oldPrefixIndex, newPrefixIndex, val) t.allot(left, old, new)
right := left + 1 right := left + 1
t.allot(right, oldPrefixIndex, newPrefixIndex, val) t.allot(right, old, new)
} }
// insert adds the route addr/prefixLen to t, with value val. // insert adds the route addr/prefixLen to t, with value val.
func (t *strideTable[T]) insert(addr uint8, prefixLen int, val *T) { func (t *strideTable[T]) insert(addr uint8, prefixLen int, val T) {
idx := prefixIndex(addr, prefixLen) idx := prefixIndex(addr, prefixLen)
old := t.entries[idx].value if !t.hasPrefixRootedAt(idx) {
oldIdx := t.entries[idx].prefixIndex // This route entry is being freshly created (not just
if oldIdx == idx && old == val { // updated), that's a new reference.
// This exact prefix+value is already in the table.
return
}
t.allot(idx, oldIdx, idx, val)
if oldIdx != idx {
// This route entry was freshly created (not just updated), that's a new
// reference.
t.routeRefs++ t.routeRefs++
} }
old := t.entries[idx]
// For allot to work correctly, each distinct prefix in the
// strideTable must have a different value pointer, even if val is
// identical. This new()+assignment guarantees that each inserted
// prefix gets a unique address.
p := new(T)
*p = val
t.allot(idx, old, p)
return return
} }
// delete removes the route addr/prefixLen from t. Returns the value // delete removes the route addr/prefixLen from t. Reports whether the
// that was associated with the deleted prefix, or nil if the prefix // prefix existed in the table prior to deletion.
// wasn't in the strideTable. func (t *strideTable[T]) delete(addr uint8, prefixLen int) (wasPresent bool) {
func (t *strideTable[T]) delete(addr uint8, prefixLen int) *T {
idx := prefixIndex(addr, prefixLen) idx := prefixIndex(addr, prefixLen)
recordedIdx := t.entries[idx].prefixIndex if !t.hasPrefixRootedAt(idx) {
if recordedIdx != idx {
// Route entry doesn't exist // Route entry doesn't exist
return nil return false
} }
val := t.entries[idx].value
parentIdx := idx >> 1 val := t.entries[idx]
t.allot(idx, idx, t.entries[parentIdx].prefixIndex, t.entries[parentIdx].value) var parentVal *T
if parentIdx := parentIndex(idx); parentIdx != 0 {
parentVal = t.entries[parentIdx]
}
t.allot(idx, val, parentVal)
t.routeRefs-- t.routeRefs--
return val return true
} }
// get does a route lookup for addr and returns the associated value, or nil if // get does a route lookup for addr and (value, true) if a matching
// no route matched. // route exists, or (zero, false) otherwise.
func (t *strideTable[T]) get(addr uint8) *T { func (t *strideTable[T]) get(addr uint8) (ret T, ok bool) {
return t.entries[hostIndex(addr)].value if val := t.entries[hostIndex(addr)]; val != nil {
return *val, true
}
return ret, false
}
// getValAndChild returns both the prefix value and child strideTable
// for addr. valOK reports whether a prefix value exists for addr, and
// child is non-nil if a child exists for addr.
func (t *strideTable[T]) getValAndChild(addr uint8) (val T, valOK bool, child *strideTable[T]) {
vp := t.entries[hostIndex(addr)]
if vp != nil {
val = *vp
valOK = true
}
child = t.children[addr]
return
} }
// TableDebugString returns the contents of t, formatted as a table with one // TableDebugString returns the contents of t, formatted as a table with one
@ -208,10 +232,10 @@ func (t *strideTable[T]) tableDebugString() string {
continue continue
} }
v := "(nil)" v := "(nil)"
if ent.value != nil { if ent != nil {
v = fmt.Sprint(*ent.value) v = fmt.Sprint(*ent)
} }
fmt.Fprintf(&ret, "idx=%3d (%s), parent=%3d (%s), val=%v\n", i, formatPrefixTable(inversePrefixIndex(i)), ent.prefixIndex, formatPrefixTable(inversePrefixIndex((ent.prefixIndex))), v) fmt.Fprintf(&ret, "idx=%3d (%s), val=%v\n", i, formatPrefixTable(inversePrefixIndex(i)), v)
} }
return ret.String() return ret.String()
} }
@ -227,8 +251,8 @@ func (t *strideTable[T]) treeDebugString() string {
func (t *strideTable[T]) treeDebugStringRec(w io.Writer, idx, indent int) { func (t *strideTable[T]) treeDebugStringRec(w io.Writer, idx, indent int) {
addr, len := inversePrefixIndex(idx) addr, len := inversePrefixIndex(idx)
if t.entries[idx].prefixIndex != 0 && t.entries[idx].prefixIndex == idx { if t.hasPrefixRootedAt(idx) {
fmt.Fprintf(w, "%s%d/%d (%02x/%d) = %v\n", strings.Repeat(" ", indent), addr, len, addr, len, *t.entries[idx].value) fmt.Fprintf(w, "%s%d/%d (%02x/%d) = %v\n", strings.Repeat(" ", indent), addr, len, addr, len, *t.entries[idx])
indent += 2 indent += 2
} }
if idx >= firstHostIndex { if idx >= firstHostIndex {
@ -251,6 +275,12 @@ func prefixIndex(addr uint8, prefixLen int) int {
return (int(addr) >> (8 - prefixLen)) + (1 << prefixLen) return (int(addr) >> (8 - prefixLen)) + (1 << prefixLen)
} }
// parentIndex returns the index of idx's parent prefix, or 0 if idx
// is the index of 0/0.
func parentIndex(idx int) int {
return idx >> 1
}
// hostIndex returns the array index of the host route for addr. // hostIndex returns the array index of the host route for addr.
// It is equivalent to prefixIndex(addr, 8). // It is equivalent to prefixIndex(addr, 8).
func hostIndex(addr uint8) int { func hostIndex(addr uint8) int {

@ -8,12 +8,12 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"net/netip" "net/netip"
"runtime"
"sort" "sort"
"strings" "strings"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"tailscale.com/types/ptr"
) )
func TestInversePrefix(t *testing.T) { func TestInversePrefix(t *testing.T) {
@ -65,10 +65,10 @@ func TestStrideTableInsert(t *testing.T) {
for i := 0; i < 256; i++ { for i := 0; i < 256; i++ {
addr := uint8(i) addr := uint8(i)
slowVal := slow.get(addr) slowVal, slowOK := slow.get(addr)
fastVal := fast.get(addr) fastVal, fastOK := fast.get(addr)
if slowVal != fastVal { if !getsEqual(fastVal, fastOK, slowVal, slowOK) {
t.Fatalf("strideTable.get(%d) = %v, want %v", addr, *fastVal, *slowVal) t.Fatalf("strideTable.get(%d) = (%v, %v), want (%v, %v)", addr, fastVal, fastOK, slowVal, slowOK)
} }
} }
} }
@ -91,10 +91,14 @@ func TestStrideTableInsertShuffled(t *testing.T) {
zero := 0 zero := 0
rt := strideTable[int]{} rt := strideTable[int]{}
// strideTable has a value interface, but internally has to keep
// track of distinct routes even if they all have the same
// value. rtZero uses the same value for all routes, and expects
// correct behavior.
rtZero := strideTable[int]{} rtZero := strideTable[int]{}
for _, route := range routes { for _, route := range routes {
rt.insert(route.addr, route.len, route.val) rt.insert(route.addr, route.len, route.val)
rtZero.insert(route.addr, route.len, &zero) rtZero.insert(route.addr, route.len, zero)
} }
// Order of insertion should not affect the final shape of the stride table. // Order of insertion should not affect the final shape of the stride table.
@ -105,15 +109,15 @@ func TestStrideTableInsertShuffled(t *testing.T) {
for _, route := range routes2 { for _, route := range routes2 {
rt2.insert(route.addr, route.len, route.val) rt2.insert(route.addr, route.len, route.val)
} }
if diff := cmp.Diff(rt, rt2, cmpDiffOpts...); diff != "" { if diff := cmp.Diff(rt.tableDebugString(), rt2.tableDebugString()); diff != "" {
t.Errorf("tables ended up different with different insertion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(routes), formatSlowEntriesShort(routes2)) t.Errorf("tables ended up different with different insertion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(routes), formatSlowEntriesShort(routes2))
} }
rtZero2 := strideTable[int]{} rtZero2 := strideTable[int]{}
for _, route := range routes2 { for _, route := range routes2 {
rtZero2.insert(route.addr, route.len, &zero) rtZero2.insert(route.addr, route.len, zero)
} }
if diff := cmp.Diff(rtZero, rtZero2, cmpDiffOpts...); diff != "" { if diff := cmp.Diff(rtZero.tableDebugString(), rtZero2.tableDebugString(), cmpDiffOpts...); diff != "" {
t.Errorf("tables with identical vals ended up different with different insertion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(routes), formatSlowEntriesShort(routes2)) t.Errorf("tables with identical vals ended up different with different insertion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(routes), formatSlowEntriesShort(routes2))
} }
} }
@ -150,10 +154,10 @@ func TestStrideTableDelete(t *testing.T) {
for i := 0; i < 256; i++ { for i := 0; i < 256; i++ {
addr := uint8(i) addr := uint8(i)
slowVal := slow.get(addr) slowVal, slowOK := slow.get(addr)
fastVal := fast.get(addr) fastVal, fastOK := fast.get(addr)
if slowVal != fastVal { if !getsEqual(fastVal, fastOK, slowVal, slowOK) {
t.Fatalf("strideTable.get(%d) = %v, want %v", addr, *fastVal, *slowVal) t.Fatalf("strideTable.get(%d) = (%v, %v), want (%v, %v)", addr, fastVal, fastOK, slowVal, slowOK)
} }
} }
} }
@ -168,10 +172,14 @@ func TestStrideTableDeleteShuffle(t *testing.T) {
zero := 0 zero := 0
rt := strideTable[int]{} rt := strideTable[int]{}
// strideTable has a value interface, but internally has to keep
// track of distinct routes even if they all have the same
// value. rtZero uses the same value for all routes, and expects
// correct behavior.
rtZero := strideTable[int]{} rtZero := strideTable[int]{}
for _, route := range routes { for _, route := range routes {
rt.insert(route.addr, route.len, route.val) rt.insert(route.addr, route.len, route.val)
rtZero.insert(route.addr, route.len, &zero) rtZero.insert(route.addr, route.len, zero)
} }
for _, route := range toDelete { for _, route := range toDelete {
rt.delete(route.addr, route.len) rt.delete(route.addr, route.len)
@ -189,18 +197,18 @@ func TestStrideTableDeleteShuffle(t *testing.T) {
for _, route := range toDelete2 { for _, route := range toDelete2 {
rt2.delete(route.addr, route.len) rt2.delete(route.addr, route.len)
} }
if diff := cmp.Diff(rt, rt2, cmpDiffOpts...); diff != "" { if diff := cmp.Diff(rt.tableDebugString(), rt2.tableDebugString(), cmpDiffOpts...); diff != "" {
t.Errorf("tables ended up different with different deletion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(toDelete), formatSlowEntriesShort(toDelete2)) t.Errorf("tables ended up different with different deletion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(toDelete), formatSlowEntriesShort(toDelete2))
} }
rtZero2 := strideTable[int]{} rtZero2 := strideTable[int]{}
for _, route := range routes { for _, route := range routes {
rtZero2.insert(route.addr, route.len, &zero) rtZero2.insert(route.addr, route.len, zero)
} }
for _, route := range toDelete2 { for _, route := range toDelete2 {
rtZero2.delete(route.addr, route.len) rtZero2.delete(route.addr, route.len)
} }
if diff := cmp.Diff(rtZero, rtZero2, cmpDiffOpts...); diff != "" { if diff := cmp.Diff(rtZero.tableDebugString(), rtZero2.tableDebugString(), cmpDiffOpts...); diff != "" {
t.Errorf("tables with identical vals ended up different with different deletion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(toDelete), formatSlowEntriesShort(toDelete2)) t.Errorf("tables with identical vals ended up different with different deletion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(toDelete), formatSlowEntriesShort(toDelete2))
} }
} }
@ -218,31 +226,35 @@ func forStrideCountAndOrdering(b *testing.B, fn func(b *testing.B, routes []slow
routes := shufflePrefixes(allPrefixes()) routes := shufflePrefixes(allPrefixes())
for _, nroutes := range strideRouteCount { for _, nroutes := range strideRouteCount {
b.Run(fmt.Sprint(nroutes), func(b *testing.B) { b.Run(fmt.Sprint(nroutes), func(b *testing.B) {
routes := append([]slowEntry[int](nil), routes[:nroutes]...) runAndRecord := func(b *testing.B) {
b.Run("random_order", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
var startMem, endMem runtime.MemStats
runtime.ReadMemStats(&startMem)
fn(b, routes) fn(b, routes)
}) runtime.ReadMemStats(&endMem)
ops := float64(b.N) * float64(len(routes))
allocs := float64(endMem.Mallocs - startMem.Mallocs)
bytes := float64(endMem.TotalAlloc - startMem.TotalAlloc)
b.ReportMetric(roundFloat64(allocs/ops), "allocs/op")
b.ReportMetric(roundFloat64(bytes/ops), "B/op")
}
routes := append([]slowEntry[int](nil), routes[:nroutes]...)
b.Run("random_order", runAndRecord)
sort.Slice(routes, func(i, j int) bool { sort.Slice(routes, func(i, j int) bool {
if routes[i].len < routes[j].len { if routes[i].len < routes[j].len {
return true return true
} }
return routes[i].addr < routes[j].addr return routes[i].addr < routes[j].addr
}) })
b.Run("largest_first", func(b *testing.B) { b.Run("largest_first", runAndRecord)
b.ReportAllocs()
fn(b, routes)
})
sort.Slice(routes, func(i, j int) bool { sort.Slice(routes, func(i, j int) bool {
if routes[j].len < routes[i].len { if routes[j].len < routes[i].len {
return true return true
} }
return routes[j].addr < routes[i].addr return routes[j].addr < routes[i].addr
}) })
b.Run("smallest_first", func(b *testing.B) { b.Run("smallest_first", runAndRecord)
b.ReportAllocs()
fn(b, routes)
})
}) })
} }
} }
@ -253,7 +265,7 @@ func BenchmarkStrideTableInsertion(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var rt strideTable[int] var rt strideTable[int]
for _, route := range routes { for _, route := range routes {
rt.insert(route.addr, route.len, &val) rt.insert(route.addr, route.len, val)
} }
} }
inserts := float64(b.N) * float64(len(routes)) inserts := float64(b.N) * float64(len(routes))
@ -269,7 +281,7 @@ func BenchmarkStrideTableDeletion(b *testing.B) {
val := 0 val := 0
var rt strideTable[int] var rt strideTable[int]
for _, route := range routes { for _, route := range routes {
rt.insert(route.addr, route.len, &val) rt.insert(route.addr, route.len, val)
} }
b.ResetTimer() b.ResetTimer()
@ -287,7 +299,7 @@ func BenchmarkStrideTableDeletion(b *testing.B) {
}) })
} }
var writeSink *int var writeSink int
func BenchmarkStrideTableGet(b *testing.B) { func BenchmarkStrideTableGet(b *testing.B) {
// No need to forCountAndOrdering here, route lookup time is independent of // No need to forCountAndOrdering here, route lookup time is independent of
@ -300,7 +312,7 @@ func BenchmarkStrideTableGet(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
writeSink = rt.get(uint8(i)) writeSink, _ = rt.get(uint8(i))
} }
gets := float64(b.N) gets := float64(b.N)
elapsedSec := b.Elapsed().Seconds() elapsedSec := b.Elapsed().Seconds()
@ -318,7 +330,7 @@ type slowTable[T any] struct {
type slowEntry[T any] struct { type slowEntry[T any] struct {
addr uint8 addr uint8
len int len int
val *T val T
} }
func (t *slowTable[T]) String() string { func (t *slowTable[T]) String() string {
@ -331,13 +343,14 @@ func (t *slowTable[T]) String() string {
}) })
var ret bytes.Buffer var ret bytes.Buffer
for _, pfx := range pfxs { for _, pfx := range pfxs {
fmt.Fprintf(&ret, "%3d/%d (%08b/%08b) = %v\n", pfx.addr, pfx.len, pfx.addr, pfxMask(pfx.len), *pfx.val) fmt.Fprintf(&ret, "%3d/%d (%08b/%08b) = %v\n", pfx.addr, pfx.len, pfx.addr, pfxMask(pfx.len), pfx.val)
} }
return ret.String() return ret.String()
} }
func (t *slowTable[T]) insert(addr uint8, prefixLen int, val *T) { func (t *slowTable[T]) insert(addr uint8, prefixLen int, val T) {
t.delete(addr, prefixLen) // no-op if prefix doesn't exist t.delete(addr, prefixLen) // no-op if prefix doesn't exist
t.prefixes = append(t.prefixes, slowEntry[T]{addr, prefixLen, val}) t.prefixes = append(t.prefixes, slowEntry[T]{addr, prefixLen, val})
} }
@ -352,18 +365,15 @@ func (t *slowTable[T]) delete(addr uint8, prefixLen int) {
t.prefixes = pfx t.prefixes = pfx
} }
func (t *slowTable[T]) get(addr uint8) *T { func (t *slowTable[T]) get(addr uint8) (ret T, ok bool) {
var ( var curLen = -1
ret *T
curLen = -1
)
for _, e := range t.prefixes { for _, e := range t.prefixes {
if addr&pfxMask(e.len) == e.addr && e.len >= curLen { if addr&pfxMask(e.len) == e.addr && e.len >= curLen {
ret = e.val ret = e.val
curLen = e.len curLen = e.len
} }
} }
return ret return ret, curLen != -1
} }
func pfxMask(pfxLen int) uint8 { func pfxMask(pfxLen int) uint8 {
@ -374,7 +384,7 @@ func allPrefixes() []slowEntry[int] {
ret := make([]slowEntry[int], 0, lastHostIndex) ret := make([]slowEntry[int], 0, lastHostIndex)
for i := 1; i < lastHostIndex+1; i++ { for i := 1; i < lastHostIndex+1; i++ {
a, l := inversePrefixIndex(i) a, l := inversePrefixIndex(i)
ret = append(ret, slowEntry[int]{a, l, ptr.To(i)}) ret = append(ret, slowEntry[int]{a, l, i})
} }
return ret return ret
} }
@ -393,6 +403,15 @@ func formatSlowEntriesShort[T any](ents []slowEntry[T]) string {
} }
var cmpDiffOpts = []cmp.Option{ var cmpDiffOpts = []cmp.Option{
cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{}),
cmp.Comparer(func(a, b netip.Prefix) bool { return a == b }), cmp.Comparer(func(a, b netip.Prefix) bool { return a == b }),
} }
func getsEqual[T comparable](a T, aOK bool, b T, bOK bool) bool {
if !aOK && !bOK {
return true
}
if aOK != bOK {
return false
}
return a == b
}

@ -51,7 +51,7 @@ func (t *Table[T]) tableForAddr(addr netip.Addr) *strideTable[T] {
// Get does a route lookup for addr and returns the associated value, or nil if // Get does a route lookup for addr and returns the associated value, or nil if
// no route matched. // no route matched.
func (t *Table[T]) Get(addr netip.Addr) *T { func (t *Table[T]) Get(addr netip.Addr) (ret T, ok bool) {
t.init() t.init()
// Ideally we would use addr.AsSlice here, but AsSlice is just // Ideally we would use addr.AsSlice here, but AsSlice is just
@ -84,13 +84,13 @@ func (t *Table[T]) Get(addr netip.Addr) *T {
const maxDepth = 16 const maxDepth = 16
type prefixAndRoute struct { type prefixAndRoute struct {
prefix netip.Prefix prefix netip.Prefix
route *T route T
} }
strideMatch := make([]prefixAndRoute, 0, maxDepth) strideMatch := make([]prefixAndRoute, 0, maxDepth)
findLeaf: findLeaf:
for { for {
rt, child := st.getValAndChild(bs[i]) rt, rtOK, child := st.getValAndChild(bs[i])
if rt != nil { if rtOK {
// This strideTable contains a route that may be relevant to our // This strideTable contains a route that may be relevant to our
// search, remember it. // search, remember it.
strideMatch = append(strideMatch, prefixAndRoute{st.prefix, rt}) strideMatch = append(strideMatch, prefixAndRoute{st.prefix, rt})
@ -115,7 +115,7 @@ findLeaf:
// the correct most-specific route. // the correct most-specific route.
for i := len(strideMatch) - 1; i >= 0; i-- { for i := len(strideMatch) - 1; i >= 0; i-- {
if m := strideMatch[i]; m.prefix.Contains(addr) { if m := strideMatch[i]; m.prefix.Contains(addr) {
return m.route return m.route, true
} }
} }
@ -123,16 +123,13 @@ findLeaf:
// immediately), or we went on a wild goose chase down a compressed path for // immediately), or we went on a wild goose chase down a compressed path for
// the wrong prefix, and also found no usable routes on the way back up to // the wrong prefix, and also found no usable routes on the way back up to
// the root. This is a miss. // the root. This is a miss.
return nil return ret, false
} }
// Insert adds pfx to the table, with value val. // Insert adds pfx to the table, with value val.
// If pfx is already present in the table, its value is set to val. // If pfx is already present in the table, its value is set to val.
func (t *Table[T]) Insert(pfx netip.Prefix, val *T) { func (t *Table[T]) Insert(pfx netip.Prefix, val T) {
t.init() t.init()
if val == nil {
panic("Table.Insert called with nil value")
}
// The standard library doesn't enforce normalized prefixes (where // The standard library doesn't enforce normalized prefixes (where
// the non-prefix bits are all zero). These algorithms require // the non-prefix bits are all zero). These algorithms require
@ -423,7 +420,7 @@ func (t *Table[T]) Delete(pfx netip.Prefix) {
if debugDelete { if debugDelete {
fmt.Printf("delete: delete from st.prefix=%s addr=%d/%d\n", st.prefix, bs[byteIdx], numBits) fmt.Printf("delete: delete from st.prefix=%s addr=%d/%d\n", st.prefix, bs[byteIdx], numBits)
} }
if st.delete(bs[byteIdx], numBits) == nil { if routeExisted := st.delete(bs[byteIdx], numBits); !routeExisted {
// We're in the right strideTable, but pfx wasn't in // We're in the right strideTable, but pfx wasn't in
// it. Refcounts haven't changed, so we can skip cleanup. // it. Refcounts haven't changed, so we can skip cleanup.
if debugDelete { if debugDelete {

@ -12,8 +12,6 @@ import (
"strconv" "strconv"
"testing" "testing"
"time" "time"
"tailscale.com/types/ptr"
) )
func TestRegression(t *testing.T) { func TestRegression(t *testing.T) {
@ -30,17 +28,16 @@ func TestRegression(t *testing.T) {
slow := slowPrefixTable[int]{} slow := slowPrefixTable[int]{}
p := netip.MustParsePrefix p := netip.MustParsePrefix
v := ptr.To(1) tbl.Insert(p("226.205.197.0/24"), 1)
tbl.Insert(p("226.205.197.0/24"), v) slow.insert(p("226.205.197.0/24"), 1)
slow.insert(p("226.205.197.0/24"), v) tbl.Insert(p("226.205.0.0/16"), 2)
v = ptr.To(2) slow.insert(p("226.205.0.0/16"), 2)
tbl.Insert(p("226.205.0.0/16"), v)
slow.insert(p("226.205.0.0/16"), v)
probe := netip.MustParseAddr("226.205.121.152") probe := netip.MustParseAddr("226.205.121.152")
got, want := tbl.Get(probe), slow.get(probe) got, gotOK := tbl.Get(probe)
if got != want { want, wantOK := slow.get(probe)
t.Fatalf("got %v, want %v", got, want) if !getsEqual(got, gotOK, want, wantOK) {
t.Fatalf("got (%v, %v), want (%v, %v)", got, gotOK, want, wantOK)
} }
}) })
@ -49,18 +46,18 @@ func TestRegression(t *testing.T) {
// within computePrefixSplit. // within computePrefixSplit.
t1, t2 := &Table[int]{}, &Table[int]{} t1, t2 := &Table[int]{}, &Table[int]{}
p := netip.MustParsePrefix p := netip.MustParsePrefix
v1, v2 := ptr.To(1), ptr.To(2)
t1.Insert(p("136.20.0.0/16"), v1) t1.Insert(p("136.20.0.0/16"), 1)
t1.Insert(p("136.20.201.62/32"), v2) t1.Insert(p("136.20.201.62/32"), 2)
t2.Insert(p("136.20.201.62/32"), v2) t2.Insert(p("136.20.201.62/32"), 2)
t2.Insert(p("136.20.0.0/16"), v1) t2.Insert(p("136.20.0.0/16"), 1)
a := netip.MustParseAddr("136.20.54.139") a := netip.MustParseAddr("136.20.54.139")
got, want := t2.Get(a), t1.Get(a) got1, ok1 := t1.Get(a)
if got != want { got2, ok2 := t2.Get(a)
t.Errorf("Get(%q) is insertion order dependent (t1=%v, t2=%v)", a, want, got) if !getsEqual(got1, ok1, got2, ok2) {
t.Errorf("Get(%q) is insertion order dependent: t1=(%v, %v), t2=(%v, %v)", a, got1, ok1, got2, ok2)
} }
}) })
} }
@ -99,7 +96,7 @@ func TestInsert(t *testing.T) {
p := netip.MustParsePrefix p := netip.MustParsePrefix
// Create a new leaf strideTable, with compressed path // Create a new leaf strideTable, with compressed path
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", -1}, {"192.168.0.2", -1},
@ -114,7 +111,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert into previous leaf, no tree changes // Insert into previous leaf, no tree changes
tbl.Insert(p("192.168.0.2/32"), ptr.To(2)) tbl.Insert(p("192.168.0.2/32"), 2)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -129,7 +126,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert into previous leaf, unaligned prefix covering the /32s // Insert into previous leaf, unaligned prefix covering the /32s
tbl.Insert(p("192.168.0.0/26"), ptr.To(7)) tbl.Insert(p("192.168.0.0/26"), 7)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -144,7 +141,7 @@ func TestInsert(t *testing.T) {
}) })
// Create a different leaf elsewhere // Create a different leaf elsewhere
tbl.Insert(p("10.0.0.0/27"), ptr.To(3)) tbl.Insert(p("10.0.0.0/27"), 3)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -159,7 +156,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert that creates a new intermediate table and a new child // Insert that creates a new intermediate table and a new child
tbl.Insert(p("192.168.1.1/32"), ptr.To(4)) tbl.Insert(p("192.168.1.1/32"), 4)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -174,7 +171,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert that creates a new intermediate table but no new child // Insert that creates a new intermediate table but no new child
tbl.Insert(p("192.170.0.0/16"), ptr.To(5)) tbl.Insert(p("192.170.0.0/16"), 5)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -190,7 +187,7 @@ func TestInsert(t *testing.T) {
// New leaf in a different subtree, so the next insert can test a // New leaf in a different subtree, so the next insert can test a
// variant of decompression. // variant of decompression.
tbl.Insert(p("192.180.0.1/32"), ptr.To(8)) tbl.Insert(p("192.180.0.1/32"), 8)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -206,7 +203,7 @@ func TestInsert(t *testing.T) {
// Insert that creates a new intermediate table but no new child, // Insert that creates a new intermediate table but no new child,
// with an unaligned intermediate // with an unaligned intermediate
tbl.Insert(p("192.180.0.0/21"), ptr.To(9)) tbl.Insert(p("192.180.0.0/21"), 9)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -221,7 +218,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert a default route, those have their own codepath. // Insert a default route, those have their own codepath.
tbl.Insert(p("0.0.0.0/0"), ptr.To(6)) tbl.Insert(p("0.0.0.0/0"), 6)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -238,7 +235,7 @@ func TestInsert(t *testing.T) {
// Now all of the above again, but for IPv6. // Now all of the above again, but for IPv6.
// Create a new leaf strideTable, with compressed path // Create a new leaf strideTable, with compressed path
tbl.Insert(p("ff:aaaa::1/128"), ptr.To(1)) tbl.Insert(p("ff:aaaa::1/128"), 1)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", -1}, {"ff:aaaa::2", -1},
@ -253,7 +250,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert into previous leaf, no tree changes // Insert into previous leaf, no tree changes
tbl.Insert(p("ff:aaaa::2/128"), ptr.To(2)) tbl.Insert(p("ff:aaaa::2/128"), 2)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", 2}, {"ff:aaaa::2", 2},
@ -268,7 +265,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert into previous leaf, unaligned prefix covering the /128s // Insert into previous leaf, unaligned prefix covering the /128s
tbl.Insert(p("ff:aaaa::/125"), ptr.To(7)) tbl.Insert(p("ff:aaaa::/125"), 7)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", 2}, {"ff:aaaa::2", 2},
@ -283,7 +280,7 @@ func TestInsert(t *testing.T) {
}) })
// Create a different leaf elsewhere // Create a different leaf elsewhere
tbl.Insert(p("ffff:bbbb::/120"), ptr.To(3)) tbl.Insert(p("ffff:bbbb::/120"), 3)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", 2}, {"ff:aaaa::2", 2},
@ -298,7 +295,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert that creates a new intermediate table and a new child // Insert that creates a new intermediate table and a new child
tbl.Insert(p("ff:aaaa:aaaa::1/128"), ptr.To(4)) tbl.Insert(p("ff:aaaa:aaaa::1/128"), 4)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", 2}, {"ff:aaaa::2", 2},
@ -313,7 +310,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert that creates a new intermediate table but no new child // Insert that creates a new intermediate table but no new child
tbl.Insert(p("ff:aaaa:aaaa:bb00::/56"), ptr.To(5)) tbl.Insert(p("ff:aaaa:aaaa:bb00::/56"), 5)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", 2}, {"ff:aaaa::2", 2},
@ -329,7 +326,7 @@ func TestInsert(t *testing.T) {
// New leaf in a different subtree, so the next insert can test a // New leaf in a different subtree, so the next insert can test a
// variant of decompression. // variant of decompression.
tbl.Insert(p("ff:cccc::1/128"), ptr.To(8)) tbl.Insert(p("ff:cccc::1/128"), 8)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", 2}, {"ff:aaaa::2", 2},
@ -345,7 +342,7 @@ func TestInsert(t *testing.T) {
// Insert that creates a new intermediate table but no new child, // Insert that creates a new intermediate table but no new child,
// with an unaligned intermediate // with an unaligned intermediate
tbl.Insert(p("ff:cccc::/37"), ptr.To(9)) tbl.Insert(p("ff:cccc::/37"), 9)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", 2}, {"ff:aaaa::2", 2},
@ -360,7 +357,7 @@ func TestInsert(t *testing.T) {
}) })
// Insert a default route, those have their own codepath. // Insert a default route, those have their own codepath.
tbl.Insert(p("::/0"), ptr.To(6)) tbl.Insert(p("::/0"), 6)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"ff:aaaa::1", 1}, {"ff:aaaa::1", 1},
{"ff:aaaa::2", 2}, {"ff:aaaa::2", 2},
@ -384,7 +381,7 @@ func TestDelete(t *testing.T) {
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("10.0.0.0/8"), ptr.To(1)) tbl.Insert(p("10.0.0.0/8"), 1)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"10.0.0.1", 1}, {"10.0.0.1", 1},
{"255.255.255.255", -1}, {"255.255.255.255", -1},
@ -403,7 +400,7 @@ func TestDelete(t *testing.T) {
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"255.255.255.255", -1}, {"255.255.255.255", -1},
@ -421,8 +418,8 @@ func TestDelete(t *testing.T) {
// Create an intermediate with 2 children, then delete one leaf. // Create an intermediate with 2 children, then delete one leaf.
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
tbl.Insert(p("192.180.0.1/32"), ptr.To(2)) tbl.Insert(p("192.180.0.1/32"), 2)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.180.0.1", 2}, {"192.180.0.1", 2},
@ -442,9 +439,9 @@ func TestDelete(t *testing.T) {
// Same, but the intermediate carries a route as well. // Same, but the intermediate carries a route as well.
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
tbl.Insert(p("192.180.0.1/32"), ptr.To(2)) tbl.Insert(p("192.180.0.1/32"), 2)
tbl.Insert(p("192.0.0.0/10"), ptr.To(3)) tbl.Insert(p("192.0.0.0/10"), 3)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.180.0.1", 2}, {"192.180.0.1", 2},
@ -466,9 +463,9 @@ func TestDelete(t *testing.T) {
// Intermediate with 3 leaves, then delete one leaf. // Intermediate with 3 leaves, then delete one leaf.
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
tbl.Insert(p("192.180.0.1/32"), ptr.To(2)) tbl.Insert(p("192.180.0.1/32"), 2)
tbl.Insert(p("192.200.0.1/32"), ptr.To(3)) tbl.Insert(p("192.200.0.1/32"), 3)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.180.0.1", 2}, {"192.180.0.1", 2},
@ -490,7 +487,7 @@ func TestDelete(t *testing.T) {
// Delete non-existent prefix, missing strideTable path. // Delete non-existent prefix, missing strideTable path.
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.255.0.1", -1}, {"192.255.0.1", -1},
@ -509,7 +506,7 @@ func TestDelete(t *testing.T) {
// with a wrong turn. // with a wrong turn.
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.255.0.1", -1}, {"192.255.0.1", -1},
@ -528,7 +525,7 @@ func TestDelete(t *testing.T) {
// leaf doesn't contain route. // leaf doesn't contain route.
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.255.0.1", -1}, {"192.255.0.1", -1},
@ -547,8 +544,8 @@ func TestDelete(t *testing.T) {
// compactable. // compactable.
tbl := &Table[int]{} tbl := &Table[int]{}
checkSize(t, tbl, 2) checkSize(t, tbl, 2)
tbl.Insert(p("192.168.0.1/32"), ptr.To(1)) tbl.Insert(p("192.168.0.1/32"), 1)
tbl.Insert(p("192.168.0.0/22"), ptr.To(2)) tbl.Insert(p("192.168.0.0/22"), 2)
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
{"192.168.0.1", 1}, {"192.168.0.1", 1},
{"192.168.0.2", 2}, {"192.168.0.2", 2},
@ -568,7 +565,7 @@ func TestDelete(t *testing.T) {
// Default routes have a special case in the code. // Default routes have a special case in the code.
tbl := &Table[int]{} tbl := &Table[int]{}
tbl.Insert(p("0.0.0.0/0"), ptr.To(1)) tbl.Insert(p("0.0.0.0/0"), 1)
tbl.Delete(p("0.0.0.0/0")) tbl.Delete(p("0.0.0.0/0"))
checkRoutes(t, tbl, []tableTest{ checkRoutes(t, tbl, []tableTest{
@ -595,20 +592,20 @@ func TestInsertCompare(t *testing.T) {
t.Logf(fast.debugSummary()) t.Logf(fast.debugSummary())
} }
seenVals4 := map[*int]bool{} seenVals4 := map[int]bool{}
seenVals6 := map[*int]bool{} seenVals6 := map[int]bool{}
for i := 0; i < 10_000; i++ { for i := 0; i < 10_000; i++ {
a := randomAddr() a := randomAddr()
slowVal := slow.get(a) slowVal, slowOK := slow.get(a)
fastVal := fast.Get(a) fastVal, fastOK := fast.Get(a)
if !getsEqual(slowVal, slowOK, fastVal, fastOK) {
t.Fatalf("get(%q) = (%v, %v), want (%v, %v)", a, fastVal, fastOK, slowVal, slowOK)
}
if a.Is6() { if a.Is6() {
seenVals6[fastVal] = true seenVals6[fastVal] = true
} else { } else {
seenVals4[fastVal] = true seenVals4[fastVal] = true
} }
if slowVal != fastVal {
t.Fatalf("get(%q) = %p, want %p", a, fastVal, slowVal)
}
} }
// Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in // Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in
@ -667,13 +664,10 @@ func TestInsertShuffled(t *testing.T) {
} }
for _, a := range addrs { for _, a := range addrs {
val1 := rt.Get(a) val1, ok1 := rt.Get(a)
val2 := rt2.Get(a) val2, ok2 := rt2.Get(a)
if val1 == nil && val2 == nil { if !getsEqual(val1, ok1, val2, ok2) {
continue t.Fatalf("get(%q) = (%v, %v), want (%v, %v)", a, val2, ok2, val1, ok1)
}
if (val1 == nil && val2 != nil) || (val1 != nil && val2 == nil) || (*val1 != *val2) {
t.Fatalf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1))
} }
} }
} }
@ -727,20 +721,20 @@ func TestDeleteCompare(t *testing.T) {
fast.Delete(pfx.pfx) fast.Delete(pfx.pfx)
} }
seenVals4 := map[*int]bool{} seenVals4 := map[int]bool{}
seenVals6 := map[*int]bool{} seenVals6 := map[int]bool{}
for i := 0; i < numProbes; i++ { for i := 0; i < numProbes; i++ {
a := randomAddr() a := randomAddr()
slowVal := slow.get(a) slowVal, slowOK := slow.get(a)
fastVal := fast.Get(a) fastVal, fastOK := fast.Get(a)
if !getsEqual(slowVal, slowOK, fastVal, fastOK) {
t.Fatalf("get(%q) = (%v, %v), want (%v, %v)", a, fastVal, fastOK, slowVal, slowOK)
}
if a.Is6() { if a.Is6() {
seenVals6[fastVal] = true seenVals6[fastVal] = true
} else { } else {
seenVals4[fastVal] = true seenVals4[fastVal] = true
} }
if slowVal != fastVal {
t.Fatalf("get(%q) = %p, want %p", a, fastVal, slowVal)
}
} }
// Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in // Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in
// ~1k distinct values for v4 and ~300 for v6. distinct routes. This sanity // ~1k distinct values for v4 and ~300 for v6. distinct routes. This sanity
@ -814,13 +808,10 @@ func TestDeleteShuffled(t *testing.T) {
// test for equivalence statistically with random probes instead. // test for equivalence statistically with random probes instead.
for i := 0; i < numProbes; i++ { for i := 0; i < numProbes; i++ {
a := randomAddr() a := randomAddr()
val1 := rt.Get(a) val1, ok1 := rt.Get(a)
val2 := rt2.Get(a) val2, ok2 := rt2.Get(a)
if val1 == nil && val2 == nil { if !getsEqual(val1, ok1, val2, ok2) {
continue t.Errorf("get(%q) = (%v, %v), want (%v, %v)", a, val2, ok2, val1, ok1)
}
if (val1 == nil && val2 != nil) || (val1 != nil && val2 == nil) || (*val1 != *val2) {
t.Errorf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1))
} }
} }
} }
@ -868,12 +859,12 @@ type tableTest struct {
func checkRoutes(t *testing.T, tbl *Table[int], tt []tableTest) { func checkRoutes(t *testing.T, tbl *Table[int], tt []tableTest) {
t.Helper() t.Helper()
for _, tc := range tt { for _, tc := range tt {
v := tbl.Get(netip.MustParseAddr(tc.addr)) v, ok := tbl.Get(netip.MustParseAddr(tc.addr))
if v == nil && tc.want != -1 { if !ok && tc.want != -1 {
t.Errorf("lookup %q got nil, want %d", tc.addr, tc.want) t.Errorf("lookup %q got (%v, %v), want (_, false)", tc.addr, v, ok)
} }
if v != nil && *v != tc.want { if ok && v != tc.want {
t.Errorf("lookup %q got %d, want %d", tc.addr, *v, tc.want) t.Errorf("lookup %q got (%v, %v), want (%v, true)", tc.addr, v, ok, tc.want)
} }
} }
} }
@ -1005,7 +996,7 @@ func BenchmarkTableGet(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
addr := genAddr() addr := genAddr()
t.Start() t.Start()
writeSink = rt.Get(addr) writeSink, _ = rt.Get(addr)
t.Stop() t.Stop()
} }
}) })
@ -1112,7 +1103,7 @@ type slowPrefixTable[T any] struct {
type slowPrefixEntry[T any] struct { type slowPrefixEntry[T any] struct {
pfx netip.Prefix pfx netip.Prefix
val *T val T
} }
func (t *slowPrefixTable[T]) delete(pfx netip.Prefix) { func (t *slowPrefixTable[T]) delete(pfx netip.Prefix) {
@ -1127,7 +1118,7 @@ func (t *slowPrefixTable[T]) delete(pfx netip.Prefix) {
t.prefixes = ret t.prefixes = ret
} }
func (t *slowPrefixTable[T]) insert(pfx netip.Prefix, val *T) { func (t *slowPrefixTable[T]) insert(pfx netip.Prefix, val T) {
pfx = pfx.Masked() pfx = pfx.Masked()
for i, ent := range t.prefixes { for i, ent := range t.prefixes {
if ent.pfx == pfx { if ent.pfx == pfx {
@ -1138,11 +1129,8 @@ func (t *slowPrefixTable[T]) insert(pfx netip.Prefix, val *T) {
t.prefixes = append(t.prefixes, slowPrefixEntry[T]{pfx, val}) t.prefixes = append(t.prefixes, slowPrefixEntry[T]{pfx, val})
} }
func (t *slowPrefixTable[T]) get(addr netip.Addr) *T { func (t *slowPrefixTable[T]) get(addr netip.Addr) (ret T, ok bool) {
var ( bestLen := -1
ret *T
bestLen = -1
)
for _, pfx := range t.prefixes { for _, pfx := range t.prefixes {
if pfx.pfx.Contains(addr) && pfx.pfx.Bits() > bestLen { if pfx.pfx.Contains(addr) && pfx.pfx.Bits() > bestLen {
@ -1150,7 +1138,7 @@ func (t *slowPrefixTable[T]) get(addr netip.Addr) *T {
bestLen = pfx.pfx.Bits() bestLen = pfx.pfx.Bits()
} }
} }
return ret return ret, bestLen != -1
} }
// randomPrefixes returns n randomly generated prefixes and associated values, // randomPrefixes returns n randomly generated prefixes and associated values,
@ -1176,7 +1164,7 @@ func randomPrefixes4(n int) []slowPrefixEntry[int] {
ret := make([]slowPrefixEntry[int], 0, len(pfxs)) ret := make([]slowPrefixEntry[int], 0, len(pfxs))
for pfx := range pfxs { for pfx := range pfxs {
ret = append(ret, slowPrefixEntry[int]{pfx, ptr.To(rand.Int())}) ret = append(ret, slowPrefixEntry[int]{pfx, rand.Int()})
} }
return ret return ret
@ -1197,7 +1185,7 @@ func randomPrefixes6(n int) []slowPrefixEntry[int] {
ret := make([]slowPrefixEntry[int], 0, len(pfxs)) ret := make([]slowPrefixEntry[int], 0, len(pfxs))
for pfx := range pfxs { for pfx := range pfxs {
ret = append(ret, slowPrefixEntry[int]{pfx, ptr.To(rand.Int())}) ret = append(ret, slowPrefixEntry[int]{pfx, rand.Int()})
} }
return ret return ret
@ -1230,14 +1218,6 @@ func randomAddr6() netip.Addr {
return netip.AddrFrom16(b) return netip.AddrFrom16(b)
} }
// printIntPtr returns *v as a string, or the literal "<nil>" if v is nil.
func printIntPtr(v *int) string {
if v == nil {
return "<nil>"
}
return fmt.Sprint(*v)
}
// roundFloat64 rounds f to 2 decimal places, for display. // roundFloat64 rounds f to 2 decimal places, for display.
// //
// It round-trips through a float->string->float conversion, so should not be // It round-trips through a float->string->float conversion, so should not be

Loading…
Cancel
Save