package routetable_test import ( "fmt" "math/rand/v2" "net/netip" "runtime" "testing" "time" "git.eeqj.de/sneak/routewatch/internal/routetable" ) // Parameters for BenchmarkApplyMixedBatch. const ( // benchPeers is how many peers there are, about as many as RIS has. benchPeers = 1400 // benchPrefixes is how many IPv4 /24 prefixes the table holds, and // benchPeersPerPrefix how many peers, picked at random, carry each. benchPrefixes = 100000 benchPeersPerPrefix = 20 // benchBatchSize is how many updates the timed batch holds, as many as // the prefix handler's largest batch (prefixBatchMaxSize in // internal/routewatch). benchBatchSize = 250000 // In the timed batch every benchWithdrawalEvery-th update is a // withdrawal, and every benchNewOriginEvery-th announcement has an // origin AS other than the prefix's. benchWithdrawalEvery = 5 benchNewOriginEvery = 7 // benchOrigins is how many origin ASes the prefixes are spread over, // from benchFirstASN on: 20 prefixes each, about as many as an origin AS // has on average in the full table. benchOrigins = 5000 benchFirstASN = 64500 // benchUpdatesPerSecond is how many updates share one time. benchUpdatesPerSecond = 1000 benchMaskLength = 24 ) // benchUpdate is one update of the benchmark. type benchUpdate struct { prefix netip.Prefix origin int route routetable.Route withdraw bool } // BenchmarkApplyMixedBatch times one batch of 250,000 updates of routes picked // at random: announcements, withdrawals and announcements with a new origin AS. // The table it applies them to holds 100,000 IPv4 prefixes from 5,000 origin // ASes, each carried by 20 of 1,400 peers. It reports the heap bytes that table // takes per prefix and origin AS pair. It is run by hand, not by make test, for // example in the test image: // // go test -run '^$' -bench MixedBatch -benchtime 10x ./internal/routetable/ func BenchmarkApplyMixedBatch(b *testing.B) { //nolint:gosec // The benchmark needs the same updates every run. rng := rand.New(rand.NewPCG(1, 2)) peers := make([]string, benchPeers) for i := range peers { peers[i] = fmt.Sprintf("198.18.%d.%d", i/256, i%256) } before := heapBytes() table := routetable.New(routetable.Params{}) updates := 0 for prefix := range benchPrefixes { for range benchPeersPerPrefix { peer := peers[rng.IntN(benchPeers)] apply(table, benchAnnouncement(rng, prefix, peer, updates, false)) updates++ } } // The record of changes is handed over every second while routewatch // runs, so it is not part of the table's size. table.TakeChanges() bytesPerPair := float64(heapBytes()-before) / benchPrefixes batch := make([]benchUpdate, 0, benchBatchSize) for i := range benchBatchSize { update := benchAnnouncement(rng, rng.IntN(benchPrefixes), peers[rng.IntN(benchPeers)], updates+i, i%benchNewOriginEvery == 0) update.withdraw = i%benchWithdrawalEvery == 0 batch = append(batch, update) } for b.Loop() { for _, update := range batch { apply(table, update) } } // After the loop: b.Loop drops the metrics reported before it starts. b.ReportMetric(bytesPerPair, "bytes/pair") } // benchAnnouncement returns the announcement of the prefix numbered prefix, a // /24 from 1.0.0.0/24 on, by peer. It is the updates-th update of the // benchmark, and its AS path holds four ASes from the peer's AS to the origin // AS. newOrigin gives it an origin AS other than the prefix's. func benchAnnouncement( rng *rand.Rand, prefix int, peer string, updates int, newOrigin bool, ) benchUpdate { origin := benchFirstASN + prefix%benchOrigins if newOrigin { origin += benchOrigins } start := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC) return benchUpdate{ prefix: netip.MustParsePrefix(fmt.Sprintf("%d.%d.%d.0/%d", 1+prefix>>16, (prefix>>8)%256, prefix%256, benchMaskLength)), origin: origin, route: routetable.Route{ Peer: peer, ASPath: []int{ benchFirstASN + rng.IntN(benchOrigins), benchFirstASN + rng.IntN(benchOrigins), benchFirstASN + rng.IntN(benchOrigins), origin, }, Time: start.Add(time.Duration(updates/benchUpdatesPerSecond) * time.Second), }, } } // apply applies update to table. func apply(table *routetable.Table, update benchUpdate) { if update.withdraw { table.Withdraw(update.prefix, update.route.Peer) return } table.Announce(update.prefix, update.origin, update.route) } // heapBytes returns the bytes of the live heap objects, after a collection. func heapBytes() uint64 { runtime.GC() var stats runtime.MemStats runtime.ReadMemStats(&stats) return stats.HeapAlloc }