diff --git a/proxy/internal/proxy/proxy_bench_test.go b/proxy/internal/proxy/proxy_bench_test.go new file mode 100644 index 000000000..b7526e26b --- /dev/null +++ b/proxy/internal/proxy/proxy_bench_test.go @@ -0,0 +1,130 @@ +package proxy_test + +import ( + "crypto/rand" + "fmt" + "math/big" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/netbirdio/netbird/proxy/internal/proxy" + "github.com/netbirdio/netbird/proxy/internal/types" +) + +type nopTransport struct{} + +func (nopTransport) RoundTrip(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: http.NoBody, + }, nil +} + +func BenchmarkServeHTTP(b *testing.B) { + rp := proxy.NewReverseProxy(nopTransport{}, "http", nil, nil) + rp.AddMapping(proxy.Mapping{ + ID: rand.Text(), + AccountID: types.AccountID(rand.Text()), + Host: "app.example.com", + Paths: map[string]*url.URL{ + "/": { + Scheme: "http", + Host: "10.0.0.1:8080", + }, + }, + }) + + req := httptest.NewRequest(http.MethodGet, "http://app.example.com", nil) + req.Host = "app.example.com" + req.RemoteAddr = "203.0.113.50:12345" + + for b.Loop() { + rp.ServeHTTP(httptest.NewRecorder(), req) + } +} + +func BenchmarkServeHTTPHostCount(b *testing.B) { + hostCounts := []int{1, 10, 100, 1_000, 10_000} + + for _, hostCount := range hostCounts { + b.Run(fmt.Sprintf("hosts=%d", hostCount), func(b *testing.B) { + rp := proxy.NewReverseProxy(nopTransport{}, "http", nil, nil) + + var target string + targetIndex, err := rand.Int(rand.Reader, big.NewInt(int64(hostCount))) + if err != nil { + b.Fatal(err) + } + for i := range hostCount { + id := rand.Text() + host := fmt.Sprintf("%s.example.com", id) + if int64(i) == targetIndex.Int64() { + target = id + } + rp.AddMapping(proxy.Mapping{ + ID: id, + AccountID: types.AccountID(rand.Text()), + Host: host, + Paths: map[string]*url.URL{ + "/": { + Scheme: "http", + Host: "10.0.0.1:8080", + }, + }, + }) + } + + req := httptest.NewRequest(http.MethodGet, "http://"+target+"/", nil) + req.Host = target + req.RemoteAddr = "203.0.113.50:12345" + + for b.Loop() { + rp.ServeHTTP(httptest.NewRecorder(), req) + } + }) + } +} + +func BenchmarkServeHTTPPathCount(b *testing.B) { + pathCounts := []int{1, 5, 10, 25, 50} + + for _, pathCount := range pathCounts { + b.Run(fmt.Sprintf("paths=%d", pathCount), func(b *testing.B) { + rp := proxy.NewReverseProxy(nopTransport{}, "http", nil, nil) + + var target string + targetIndex, err := rand.Int(rand.Reader, big.NewInt(int64(pathCount))) + if err != nil { + b.Fatal(err) + } + + paths := make(map[string]*url.URL, pathCount) + for i := range pathCount { + path := "/" + rand.Text() + if int64(i) == targetIndex.Int64() { + target = path + } + paths[path] = &url.URL{ + Scheme: "http", + Host: "10.0.0.1:" + fmt.Sprintf("%d", 8080+i), + } + } + rp.AddMapping(proxy.Mapping{ + ID: rand.Text(), + AccountID: types.AccountID(rand.Text()), + Host: "app.example.com", + Paths: paths, + }) + + req := httptest.NewRequest(http.MethodGet, "http://app.example.com"+target, nil) + req.Host = "app.example.com" + req.RemoteAddr = "203.0.113.50:12345" + + for b.Loop() { + rp.ServeHTTP(httptest.NewRecorder(), req) + } + }) + } +} diff --git a/proxy/internal/roundtrip/netbird_bench_test.go b/proxy/internal/roundtrip/netbird_bench_test.go new file mode 100644 index 000000000..070638f1c --- /dev/null +++ b/proxy/internal/roundtrip/netbird_bench_test.go @@ -0,0 +1,107 @@ +package roundtrip + +import ( + "crypto/rand" + "math/big" + "sync" + "testing" + "time" + + "github.com/netbirdio/netbird/proxy/internal/types" + "github.com/netbirdio/netbird/shared/management/domain" +) + +// Simple benchmark for comparison with AddPeer contention. +func BenchmarkHasClient(b *testing.B) { + // Knobs for dialling in: + initialClientCount := 100 // Size of initial peer map to generate. + + nb := mockNetBird() + + var target types.AccountID + targetIndex, err := rand.Int(rand.Reader, big.NewInt(int64(initialClientCount))) + if err != nil { + b.Fatal(err) + } + for i := range initialClientCount { + id := types.AccountID(rand.Text()) + if int64(i) == targetIndex.Int64() { + target = id + } + nb.clients[id] = &clientEntry{ + domains: map[domain.Domain]domainInfo{ + domain.Domain(rand.Text()): { + reverseProxyID: rand.Text(), + }, + }, + createdAt: time.Now(), + started: true, + } + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + nb.HasClient(target) + } + }) + b.StopTimer() +} + +func BenchmarkHasClientDuringAddPeer(b *testing.B) { + // Knobs for dialling in: + initialClientCount := 100 // Size of initial peer map to generate. + addPeerWorkers := 5 // Number of workers to concurrently call AddPeer. + + nb := mockNetBird() + + // Add random client entries to the netbird instance. + // We're trying to test map lock contention, so starting with + // a populated map should help with this. + // Pick a random one to target for retrieval later. + var target types.AccountID + targetIndex, err := rand.Int(rand.Reader, big.NewInt(int64(initialClientCount))) + if err != nil { + b.Fatal(err) + } + for i := range initialClientCount { + id := types.AccountID(rand.Text()) + if int64(i) == targetIndex.Int64() { + target = id + } + nb.clients[id] = &clientEntry{ + domains: map[domain.Domain]domainInfo{ + domain.Domain(rand.Text()): { + reverseProxyID: rand.Text(), + }, + }, + createdAt: time.Now(), + started: true, + } + } + + // Launch workers that continuously call AddPeer with new random accountIDs. + var wg sync.WaitGroup + for range addPeerWorkers { + wg.Go(func() { + for { + if err := nb.AddPeer(b.Context(), + types.AccountID(rand.Text()), + domain.Domain(rand.Text()), + rand.Text(), + rand.Text()); err != nil { + b.Log(err) + } + } + }) + } + + // Benchmark calling HasClient during AddPeer contention. + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + nb.HasClient(target) + } + }) + b.StopTimer() +}