2023-04-03 23:29:36 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package art
import (
"bytes"
"fmt"
"math/rand"
2023-04-06 18:20:33 +00:00
"net/netip"
2023-08-16 22:51:57 +00:00
"runtime"
2023-04-03 23:29:36 +00:00
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestInversePrefix ( t * testing . T ) {
2023-04-04 16:00:51 +00:00
t . Parallel ( )
2023-04-03 23:29:36 +00:00
for i := 0 ; i < 256 ; i ++ {
for len := 0 ; len < 9 ; len ++ {
addr := i & ( 0xFF << ( 8 - len ) )
idx := prefixIndex ( uint8 ( addr ) , len )
addr2 , len2 := inversePrefixIndex ( idx )
if addr2 != uint8 ( addr ) || len2 != len {
t . Errorf ( "inverse(index(%d/%d)) != %d/%d" , addr , len , addr2 , len2 )
}
}
}
}
func TestHostIndex ( t * testing . T ) {
2023-04-04 16:00:51 +00:00
t . Parallel ( )
2023-04-03 23:29:36 +00:00
for i := 0 ; i < 256 ; i ++ {
got := hostIndex ( uint8 ( i ) )
want := prefixIndex ( uint8 ( i ) , 8 )
if got != want {
t . Errorf ( "hostIndex(%d) = %d, want %d" , i , got , want )
}
}
}
func TestStrideTableInsert ( t * testing . T ) {
2023-04-04 16:00:51 +00:00
t . Parallel ( )
2023-04-03 23:29:36 +00:00
// Verify that strideTable's lookup results after a bunch of inserts exactly
// match those of a naive implementation that just scans all prefixes on
// every lookup. The naive implementation is very slow, but its behavior is
// easy to verify by inspection.
pfxs := shufflePrefixes ( allPrefixes ( ) ) [ : 100 ]
slow := slowTable [ int ] { pfxs }
fast := strideTable [ int ] { }
2023-07-13 19:25:08 +00:00
if debugStrideInsert {
t . Logf ( "slow table:\n%s" , slow . String ( ) )
}
2023-04-03 23:29:36 +00:00
for _ , pfx := range pfxs {
fast . insert ( pfx . addr , pfx . len , pfx . val )
2023-07-13 19:25:08 +00:00
if debugStrideInsert {
t . Logf ( "after insert %d/%d:\n%s" , pfx . addr , pfx . len , fast . tableDebugString ( ) )
}
2023-04-03 23:29:36 +00:00
}
for i := 0 ; i < 256 ; i ++ {
addr := uint8 ( i )
2023-08-16 22:51:57 +00:00
slowVal , slowOK := slow . get ( addr )
fastVal , fastOK := fast . get ( addr )
if ! getsEqual ( fastVal , fastOK , slowVal , slowOK ) {
t . Fatalf ( "strideTable.get(%d) = (%v, %v), want (%v, %v)" , addr , fastVal , fastOK , slowVal , slowOK )
2023-04-03 23:29:36 +00:00
}
}
}
func TestStrideTableInsertShuffled ( t * testing . T ) {
2023-04-04 16:00:51 +00:00
t . Parallel ( )
2023-04-03 23:29:36 +00:00
// The order in which routes are inserted into a route table does not
// influence the final shape of the table, as long as the same set of
// prefixes is being inserted. This test verifies that strideTable behaves
// this way.
//
// In addition to the basic shuffle test, we also check that this behavior
// is maintained if all inserted routes have the same value pointer. This
// shouldn't matter (the strideTable still needs to correctly account for
// each inserted route, regardless of associated value), but during initial
// development a subtle bug made the table corrupt itself in that setup, so
// this test includes a regression test for that.
routes := shufflePrefixes ( allPrefixes ( ) ) [ : 100 ]
zero := 0
rt := strideTable [ int ] { }
2023-08-16 22:51:57 +00:00
// 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.
2023-04-03 23:29:36 +00:00
rtZero := strideTable [ int ] { }
for _ , route := range routes {
rt . insert ( route . addr , route . len , route . val )
2023-08-16 22:51:57 +00:00
rtZero . insert ( route . addr , route . len , zero )
2023-04-03 23:29:36 +00:00
}
// Order of insertion should not affect the final shape of the stride table.
routes2 := append ( [ ] slowEntry [ int ] ( nil ) , routes ... ) // dup so we can print both slices on fail
for i := 0 ; i < 100 ; i ++ {
rand . Shuffle ( len ( routes2 ) , func ( i , j int ) { routes2 [ i ] , routes2 [ j ] = routes2 [ j ] , routes2 [ i ] } )
rt2 := strideTable [ int ] { }
for _ , route := range routes2 {
rt2 . insert ( route . addr , route . len , route . val )
}
2023-08-16 22:51:57 +00:00
if diff := cmp . Diff ( rt . tableDebugString ( ) , rt2 . tableDebugString ( ) ) ; diff != "" {
2023-04-03 23:29:36 +00:00
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 ] { }
for _ , route := range routes2 {
2023-08-16 22:51:57 +00:00
rtZero2 . insert ( route . addr , route . len , zero )
2023-04-03 23:29:36 +00:00
}
2023-08-16 22:51:57 +00:00
if diff := cmp . Diff ( rtZero . tableDebugString ( ) , rtZero2 . tableDebugString ( ) , cmpDiffOpts ... ) ; diff != "" {
2023-04-03 23:29:36 +00:00
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 ) )
}
}
}
func TestStrideTableDelete ( t * testing . T ) {
2023-04-04 16:00:51 +00:00
t . Parallel ( )
2023-04-03 23:29:36 +00:00
// Compare route deletion to our reference slowTable.
pfxs := shufflePrefixes ( allPrefixes ( ) ) [ : 100 ]
slow := slowTable [ int ] { pfxs }
fast := strideTable [ int ] { }
2023-07-13 19:25:08 +00:00
if debugStrideDelete {
t . Logf ( "slow table:\n%s" , slow . String ( ) )
}
2023-04-03 23:29:36 +00:00
for _ , pfx := range pfxs {
fast . insert ( pfx . addr , pfx . len , pfx . val )
2023-07-13 19:25:08 +00:00
if debugStrideDelete {
t . Logf ( "after insert %d/%d:\n%s" , pfx . addr , pfx . len , fast . tableDebugString ( ) )
}
2023-04-03 23:29:36 +00:00
}
toDelete := pfxs [ : 50 ]
for _ , pfx := range toDelete {
slow . delete ( pfx . addr , pfx . len )
fast . delete ( pfx . addr , pfx . len )
}
// Sanity check that slowTable seems to have done the right thing.
if cnt := len ( slow . prefixes ) ; cnt != 50 {
t . Fatalf ( "slowTable has %d entries after deletes, want 50" , cnt )
}
for i := 0 ; i < 256 ; i ++ {
addr := uint8 ( i )
2023-08-16 22:51:57 +00:00
slowVal , slowOK := slow . get ( addr )
fastVal , fastOK := fast . get ( addr )
if ! getsEqual ( fastVal , fastOK , slowVal , slowOK ) {
t . Fatalf ( "strideTable.get(%d) = (%v, %v), want (%v, %v)" , addr , fastVal , fastOK , slowVal , slowOK )
2023-04-03 23:29:36 +00:00
}
}
}
func TestStrideTableDeleteShuffle ( t * testing . T ) {
2023-04-04 16:00:51 +00:00
t . Parallel ( )
2023-04-03 23:29:36 +00:00
// Same as TestStrideTableInsertShuffle, the order in which prefixes are
// deleted should not impact the final shape of the route table.
routes := shufflePrefixes ( allPrefixes ( ) ) [ : 100 ]
toDelete := routes [ : 50 ]
zero := 0
rt := strideTable [ int ] { }
2023-08-16 22:51:57 +00:00
// 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.
2023-04-03 23:29:36 +00:00
rtZero := strideTable [ int ] { }
for _ , route := range routes {
rt . insert ( route . addr , route . len , route . val )
2023-08-16 22:51:57 +00:00
rtZero . insert ( route . addr , route . len , zero )
2023-04-03 23:29:36 +00:00
}
for _ , route := range toDelete {
rt . delete ( route . addr , route . len )
rtZero . delete ( route . addr , route . len )
}
// Order of deletion should not affect the final shape of the stride table.
toDelete2 := append ( [ ] slowEntry [ int ] ( nil ) , toDelete ... ) // dup so we can print both slices on fail
for i := 0 ; i < 100 ; i ++ {
rand . Shuffle ( len ( toDelete2 ) , func ( i , j int ) { toDelete2 [ i ] , toDelete2 [ j ] = toDelete2 [ j ] , toDelete2 [ i ] } )
rt2 := strideTable [ int ] { }
for _ , route := range routes {
rt2 . insert ( route . addr , route . len , route . val )
}
for _ , route := range toDelete2 {
rt2 . delete ( route . addr , route . len )
}
2023-08-16 22:51:57 +00:00
if diff := cmp . Diff ( rt . tableDebugString ( ) , rt2 . tableDebugString ( ) , cmpDiffOpts ... ) ; diff != "" {
2023-04-03 23:29:36 +00:00
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 ] { }
for _ , route := range routes {
2023-08-16 22:51:57 +00:00
rtZero2 . insert ( route . addr , route . len , zero )
2023-04-03 23:29:36 +00:00
}
for _ , route := range toDelete2 {
rtZero2 . delete ( route . addr , route . len )
}
2023-08-16 22:51:57 +00:00
if diff := cmp . Diff ( rtZero . tableDebugString ( ) , rtZero2 . tableDebugString ( ) , cmpDiffOpts ... ) ; diff != "" {
2023-04-03 23:29:36 +00:00
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 ) )
}
}
}
2023-04-04 16:00:51 +00:00
var strideRouteCount = [ ] int { 10 , 50 , 100 , 200 }
2023-04-03 23:29:36 +00:00
// forCountAndOrdering runs the benchmark fn with different sets of routes.
//
// fn is called once for each combination of {num_routes, order}, where
2023-04-04 16:00:51 +00:00
// num_routes is the values in strideRouteCount, and order is the order of the
2023-04-03 23:29:36 +00:00
// routes in the list: random, largest prefix first (/0 to /8), and smallest
// prefix first (/8 to /0).
2023-04-04 16:00:51 +00:00
func forStrideCountAndOrdering ( b * testing . B , fn func ( b * testing . B , routes [ ] slowEntry [ int ] ) ) {
2023-04-03 23:29:36 +00:00
routes := shufflePrefixes ( allPrefixes ( ) )
2023-04-04 16:00:51 +00:00
for _ , nroutes := range strideRouteCount {
2023-04-03 23:29:36 +00:00
b . Run ( fmt . Sprint ( nroutes ) , func ( b * testing . B ) {
2023-08-16 22:51:57 +00:00
runAndRecord := func ( b * testing . B ) {
2023-04-03 23:29:36 +00:00
b . ReportAllocs ( )
2023-08-16 22:51:57 +00:00
var startMem , endMem runtime . MemStats
runtime . ReadMemStats ( & startMem )
2023-04-03 23:29:36 +00:00
fn ( b , routes )
2023-08-16 22:51:57 +00:00
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 )
2023-04-03 23:29:36 +00:00
sort . Slice ( routes , func ( i , j int ) bool {
if routes [ i ] . len < routes [ j ] . len {
return true
}
return routes [ i ] . addr < routes [ j ] . addr
} )
2023-08-16 22:51:57 +00:00
b . Run ( "largest_first" , runAndRecord )
2023-04-03 23:29:36 +00:00
sort . Slice ( routes , func ( i , j int ) bool {
if routes [ j ] . len < routes [ i ] . len {
return true
}
return routes [ j ] . addr < routes [ i ] . addr
} )
2023-08-16 22:51:57 +00:00
b . Run ( "smallest_first" , runAndRecord )
2023-04-03 23:29:36 +00:00
} )
}
}
func BenchmarkStrideTableInsertion ( b * testing . B ) {
2023-04-04 16:00:51 +00:00
forStrideCountAndOrdering ( b , func ( b * testing . B , routes [ ] slowEntry [ int ] ) {
2023-04-03 23:29:36 +00:00
val := 0
for i := 0 ; i < b . N ; i ++ {
var rt strideTable [ int ]
for _ , route := range routes {
2023-08-16 22:51:57 +00:00
rt . insert ( route . addr , route . len , val )
2023-04-03 23:29:36 +00:00
}
}
inserts := float64 ( b . N ) * float64 ( len ( routes ) )
elapsed := float64 ( b . Elapsed ( ) . Nanoseconds ( ) )
elapsedSec := b . Elapsed ( ) . Seconds ( )
b . ReportMetric ( elapsed / inserts , "ns/op" )
b . ReportMetric ( inserts / elapsedSec , "routes/s" )
} )
}
func BenchmarkStrideTableDeletion ( b * testing . B ) {
2023-04-04 16:00:51 +00:00
forStrideCountAndOrdering ( b , func ( b * testing . B , routes [ ] slowEntry [ int ] ) {
2023-04-03 23:29:36 +00:00
val := 0
var rt strideTable [ int ]
for _ , route := range routes {
2023-08-16 22:51:57 +00:00
rt . insert ( route . addr , route . len , val )
2023-04-03 23:29:36 +00:00
}
b . ResetTimer ( )
for i := 0 ; i < b . N ; i ++ {
rt2 := rt
for _ , route := range routes {
rt2 . delete ( route . addr , route . len )
}
}
deletes := float64 ( b . N ) * float64 ( len ( routes ) )
elapsed := float64 ( b . Elapsed ( ) . Nanoseconds ( ) )
elapsedSec := b . Elapsed ( ) . Seconds ( )
b . ReportMetric ( elapsed / deletes , "ns/op" )
b . ReportMetric ( deletes / elapsedSec , "routes/s" )
} )
}
2023-08-16 22:51:57 +00:00
var writeSink int
2023-04-03 23:29:36 +00:00
func BenchmarkStrideTableGet ( b * testing . B ) {
// No need to forCountAndOrdering here, route lookup time is independent of
// the route count.
routes := shufflePrefixes ( allPrefixes ( ) ) [ : 100 ]
var rt strideTable [ int ]
for _ , route := range routes {
rt . insert ( route . addr , route . len , route . val )
}
b . ResetTimer ( )
for i := 0 ; i < b . N ; i ++ {
2023-08-16 22:51:57 +00:00
writeSink , _ = rt . get ( uint8 ( i ) )
2023-04-03 23:29:36 +00:00
}
gets := float64 ( b . N )
elapsedSec := b . Elapsed ( ) . Seconds ( )
b . ReportMetric ( gets / elapsedSec , "routes/s" )
}
// slowTable is an 8-bit routing table implemented as a set of prefixes that are
// explicitly scanned in full for every route lookup. It is very slow, but also
// reasonably easy to verify by inspection, and so a good comparison target for
// strideTable.
type slowTable [ T any ] struct {
prefixes [ ] slowEntry [ T ]
}
type slowEntry [ T any ] struct {
addr uint8
len int
2023-08-16 22:51:57 +00:00
val T
2023-04-03 23:29:36 +00:00
}
func ( t * slowTable [ T ] ) String ( ) string {
pfxs := append ( [ ] slowEntry [ T ] ( nil ) , t . prefixes ... )
sort . Slice ( pfxs , func ( i , j int ) bool {
if pfxs [ i ] . len != pfxs [ j ] . len {
return pfxs [ i ] . len < pfxs [ j ] . len
}
return pfxs [ i ] . addr < pfxs [ j ] . addr
} )
var ret bytes . Buffer
for _ , pfx := range pfxs {
2023-08-16 22:51:57 +00:00
fmt . Fprintf ( & ret , "%3d/%d (%08b/%08b) = %v\n" , pfx . addr , pfx . len , pfx . addr , pfxMask ( pfx . len ) , pfx . val )
2023-04-03 23:29:36 +00:00
}
return ret . String ( )
}
2023-08-16 22:51:57 +00:00
func ( t * slowTable [ T ] ) insert ( addr uint8 , prefixLen int , val T ) {
2023-04-03 23:29:36 +00:00
t . delete ( addr , prefixLen ) // no-op if prefix doesn't exist
2023-08-16 22:51:57 +00:00
2023-04-03 23:29:36 +00:00
t . prefixes = append ( t . prefixes , slowEntry [ T ] { addr , prefixLen , val } )
}
func ( t * slowTable [ T ] ) delete ( addr uint8 , prefixLen int ) {
pfx := make ( [ ] slowEntry [ T ] , 0 , len ( t . prefixes ) )
for _ , e := range t . prefixes {
if e . addr == addr && e . len == prefixLen {
continue
}
pfx = append ( pfx , e )
}
t . prefixes = pfx
}
2023-08-16 22:51:57 +00:00
func ( t * slowTable [ T ] ) get ( addr uint8 ) ( ret T , ok bool ) {
var curLen = - 1
2023-04-03 23:29:36 +00:00
for _ , e := range t . prefixes {
if addr & pfxMask ( e . len ) == e . addr && e . len >= curLen {
ret = e . val
curLen = e . len
}
}
2023-08-16 22:51:57 +00:00
return ret , curLen != - 1
2023-04-03 23:29:36 +00:00
}
func pfxMask ( pfxLen int ) uint8 {
return 0xFF << ( 8 - pfxLen )
}
func allPrefixes ( ) [ ] slowEntry [ int ] {
ret := make ( [ ] slowEntry [ int ] , 0 , lastHostIndex )
for i := 1 ; i < lastHostIndex + 1 ; i ++ {
a , l := inversePrefixIndex ( i )
2023-08-16 22:51:57 +00:00
ret = append ( ret , slowEntry [ int ] { a , l , i } )
2023-04-03 23:29:36 +00:00
}
return ret
}
func shufflePrefixes ( pfxs [ ] slowEntry [ int ] ) [ ] slowEntry [ int ] {
rand . Shuffle ( len ( pfxs ) , func ( i , j int ) { pfxs [ i ] , pfxs [ j ] = pfxs [ j ] , pfxs [ i ] } )
return pfxs
}
func formatSlowEntriesShort [ T any ] ( ents [ ] slowEntry [ T ] ) string {
var ret [ ] string
for _ , ent := range ents {
ret = append ( ret , fmt . Sprintf ( "%d/%d" , ent . addr , ent . len ) )
}
return "[" + strings . Join ( ret , " " ) + "]"
}
2023-04-06 18:20:33 +00:00
var cmpDiffOpts = [ ] cmp . Option {
cmp . Comparer ( func ( a , b netip . Prefix ) bool { return a == b } ) ,
}
2023-08-16 22:51:57 +00:00
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
}