From 610c1daa4dfd1ccff642e066d0740231a25e3079 Mon Sep 17 00:00:00 2001 From: DM Date: Thu, 26 Feb 2026 08:39:04 +0300 Subject: [PATCH] types: avoid NodeView clone in CanAccess NodeView.CanAccess called node2.AsStruct() on every check. In peer-map construction we run CanAccess in O(n^2) pair scans (often twice per pair), so that per-call clone multiplied into large heap churn --- hscontrol/types/node.go | 4 +- hscontrol/types/node_benchmark_test.go | 73 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 hscontrol/types/node_benchmark_test.go diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 05b27c19..91feb7d4 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -800,11 +800,11 @@ func (nv NodeView) InIPSet(set *netipx.IPSet) bool { } func (nv NodeView) CanAccess(matchers []matcher.Match, node2 NodeView) bool { - if !nv.Valid() { + if !nv.Valid() || !node2.Valid() { return false } - return nv.ж.CanAccess(matchers, node2.AsStruct()) + return nv.ж.CanAccess(matchers, node2.ж) } func (nv NodeView) CanAccessRoute(matchers []matcher.Match, route netip.Prefix) bool { diff --git a/hscontrol/types/node_benchmark_test.go b/hscontrol/types/node_benchmark_test.go new file mode 100644 index 00000000..e7320379 --- /dev/null +++ b/hscontrol/types/node_benchmark_test.go @@ -0,0 +1,73 @@ +package types + +import ( + "fmt" + "net/netip" + "testing" + + "github.com/juanfont/headscale/hscontrol/policy/matcher" + "tailscale.com/tailcfg" +) + +func BenchmarkNodeViewCanAccess(b *testing.B) { + addr := func(ip string) *netip.Addr { + parsed := netip.MustParseAddr(ip) + return &parsed + } + + rules := []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1/32"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "100.64.0.2/32", + Ports: tailcfg.PortRangeAny, + }, + }, + }, + } + matchers := matcher.MatchesFromFilterRules(rules) + + derpLatency := make(map[string]float64, 256) + for i := range 128 { + derpLatency[fmt.Sprintf("%d-v4", i)] = float64(i) / 10 + derpLatency[fmt.Sprintf("%d-v6", i)] = float64(i) / 10 + } + + src := Node{ + IPv4: addr("100.64.0.1"), + } + dst := Node{ + IPv4: addr("100.64.0.2"), + Hostinfo: &tailcfg.Hostinfo{ + NetInfo: &tailcfg.NetInfo{ + DERPLatency: derpLatency, + }, + }, + } + + srcView := src.View() + dstView := dst.View() + + if !srcView.CanAccess(matchers, dstView) { + b.Fatal("benchmark setup error: expected source to access destination") + } + + b.Run("pointer", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for b.Loop() { + srcView.CanAccess(matchers, dstView) + } + }) + + b.Run("struct clone", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for b.Loop() { + src.CanAccess(matchers, dstView.AsStruct()) + } + }) +}