mirror of
https://github.com/wweir/sower.git
synced 2026-10-19 16:14:33 +00:00
perf: batch byte accounting and bound gateway memory
- countingConn batches per-domain attribution (32KiB/500ms) so the relay hot path pays two atomics per I/O instead of a global mutex; Close drains - Precompute OTel byte attributes to drop per-I/O allocations - suffixtree: indexMap only for nodes with >=8 children; Match stays read-only for concurrent queries - Soft Go memory limit (128MiB default, GOMEMLIMIT/SOWER_MEMORY_LIMIT_MB override) cuts resident memory from ~240MB to ~130MB on the default ad/china/gfw rule sets - Split stats.go into snapshot/eviction/counting by responsibility
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BenchmarkDNSPrepareUpstreamState measures the per-query upstream state
|
||||
// snapshot cost (mutex + address-slice copy).
|
||||
func BenchmarkDNSPrepareUpstreamState(b *testing.B) {
|
||||
r, err := NewRouter(nil, "", "223.5.5.5", "", nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "8.8.8.8:53", "223.5.5.5:53"}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
r.prepareUpstreamState(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDNSRuleMatchPath measures the rule-classification cost of one
|
||||
// DNS query: domain-list construction plus up to three RuleSet.Match calls.
|
||||
func BenchmarkDNSRuleMatchPath(b *testing.B) {
|
||||
r, err := NewRouter(nil, "", "223.5.5.5", "", nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
rules := make([]string, 0, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
rules = append(rules, fmt.Sprintf("host%d.example.com", i))
|
||||
}
|
||||
r.BlockRule.Replace(rules...)
|
||||
r.DirectRule.Replace("direct.example.com")
|
||||
r.ProxyRule.Replace("proxy.example.com")
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
domains := dnsRouteDomains("www.example.com", 1)
|
||||
_ = dnsRuleMatch(r.BlockRule, domains)
|
||||
if !dnsRuleMatch(r.DirectRule, domains) {
|
||||
_ = dnsRuleMatch(r.ProxyRule, domains)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-1
@@ -10,6 +10,8 @@ import (
|
||||
// RuleSet is a thread-safe rule container. It retains the raw rule list so
|
||||
// rules can be listed and removed at runtime; the suffixtree is derived
|
||||
// state and is rebuilt from the retained list whenever a rule is removed.
|
||||
// Membership for Add/Remove uses the raw rule strings (set), so case and
|
||||
// trailing-dot variants stay distinct entries in the configuration.
|
||||
type RuleSet struct {
|
||||
mu sync.RWMutex
|
||||
rules []string
|
||||
@@ -25,7 +27,8 @@ func NewRuleSet(rules ...string) *RuleSet {
|
||||
return rs
|
||||
}
|
||||
|
||||
// Add appends rules that are not already present. Duplicates are ignored.
|
||||
// Add appends rules that are not already present, using the raw rule string
|
||||
// as the membership key. Duplicates are ignored.
|
||||
func (rs *RuleSet) Add(rules ...string) {
|
||||
if len(rules) == 0 {
|
||||
return
|
||||
@@ -46,6 +49,32 @@ func (rs *RuleSet) Add(rules ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Replace swaps the entire rule list atomically, rebuilding the suffix
|
||||
// tree. Literal duplicates in the input are dropped. It is used to rebuild
|
||||
// a rule set from the boot baseline after a delta reset.
|
||||
func (rs *RuleSet) Replace(rules ...string) {
|
||||
// Construct the full immutable candidate before taking the match lock.
|
||||
// The field swap below is the operation's linearization point.
|
||||
nextRules := make([]string, 0, len(rules))
|
||||
nextSet := make(map[string]struct{}, len(rules))
|
||||
nextTree := suffixtree.NewNodeFromRules()
|
||||
for _, rule := range rules {
|
||||
if _, ok := nextSet[rule]; ok {
|
||||
continue
|
||||
}
|
||||
nextSet[rule] = struct{}{}
|
||||
nextRules = append(nextRules, rule)
|
||||
nextTree.Add(rule)
|
||||
}
|
||||
nextTree.GC()
|
||||
|
||||
rs.mu.Lock()
|
||||
rs.rules = nextRules
|
||||
rs.set = nextSet
|
||||
rs.tree = nextTree
|
||||
rs.mu.Unlock()
|
||||
}
|
||||
|
||||
// Compact runs garbage collection on the underlying tree. Call it after a
|
||||
// batch of adds to avoid per-add compaction overhead on large rule files.
|
||||
func (rs *RuleSet) Compact() {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkRuleSetMatch(b *testing.B) {
|
||||
rules := make([]string, 0, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
rules = append(rules, fmt.Sprintf("host%d.example.com", i))
|
||||
}
|
||||
rs := NewRuleSet(rules...)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
rs.Match("www.example.com")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRuleSetMatchHit(b *testing.B) {
|
||||
rules := make([]string, 0, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
rules = append(rules, fmt.Sprintf("host%d.example.com", i))
|
||||
}
|
||||
rs := NewRuleSet(rules...)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
rs.Match("host5000.example.com")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// keepGlobal pins the trees across GC; without it the compiler may treat
|
||||
// them as dead after the Match checks.
|
||||
var keepGlobal struct {
|
||||
block *RuleSet
|
||||
direct *RuleSet
|
||||
proxy *RuleSet
|
||||
}
|
||||
|
||||
// TestRulesetMemoryFootprint builds rule shapes matching the production
|
||||
// ad/china/gfw lists (synthetic, no external files) and reports the retained
|
||||
// heap. Skipped under -short.
|
||||
func TestRulesetMemoryFootprint(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("memory footprint test; skipped in -short mode")
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
keepGlobal = struct {
|
||||
block *RuleSet
|
||||
direct *RuleSet
|
||||
proxy *RuleSet
|
||||
}{}
|
||||
})
|
||||
|
||||
// adlist-like: many exact domains under a few second-level domains.
|
||||
block := make([]string, 0, 160000)
|
||||
for i := 0; i < 160000; i++ {
|
||||
block = append(block, fmt.Sprintf("ads%d.doubleclick.net", i))
|
||||
}
|
||||
// chinalist-like: many exact domains under many second-level domains.
|
||||
direct := make([]string, 0, 110000)
|
||||
for i := 0; i < 110000; i++ {
|
||||
direct = append(direct, fmt.Sprintf("host%d.example%d.com", i, i%2000))
|
||||
}
|
||||
// gfwlist-like: wildcard-heavy.
|
||||
proxy := make([]string, 0, 5000)
|
||||
for i := 0; i < 5000; i++ {
|
||||
proxy = append(proxy, fmt.Sprintf("**.site%d.com", i%100))
|
||||
}
|
||||
t.Logf("rules: block=%d direct=%d proxy=%d", len(block), len(direct), len(proxy))
|
||||
|
||||
keepGlobal.block = NewRuleSet(block...)
|
||||
keepGlobal.direct = NewRuleSet(direct...)
|
||||
keepGlobal.proxy = NewRuleSet(proxy...)
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
before := m.HeapAlloc
|
||||
|
||||
// Keep references live across GC.
|
||||
if !keepGlobal.block.Match("ads1.doubleclick.net") || !keepGlobal.direct.Match("host1.example1.com") || !keepGlobal.proxy.Match("a.b.site1.com") {
|
||||
t.Fatal("unexpected match result")
|
||||
}
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&m)
|
||||
t.Logf("retained after GC: %d KiB (%.1f MB)", m.HeapAlloc/1024, float64(m.HeapAlloc)/1024/1024)
|
||||
t.Logf("heap before/after: %d / %d", before, m.HeapAlloc)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package router
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -149,6 +150,32 @@ func TestRuleSetConcurrentAccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleSetReplaceConcurrentWithMatch(t *testing.T) {
|
||||
rs := NewRuleSet("seed.example.com")
|
||||
var stop atomic.Bool
|
||||
var wg sync.WaitGroup
|
||||
for range 8 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for !stop.Load() {
|
||||
_ = rs.Match("seed.example.com")
|
||||
_ = rs.Match("replacement.example.com")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
rs.Replace(fmt.Sprintf("replacement%d.example.com", i), "replacement.example.com")
|
||||
}
|
||||
stop.Store(true)
|
||||
wg.Wait()
|
||||
|
||||
if !rs.Match("replacement.example.com") || rs.Match("seed.example.com") {
|
||||
t.Fatalf("unexpected final rules: %v", rs.List())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListFilteredPaginationAndSearch(t *testing.T) {
|
||||
rs := NewRuleSet("example.com", "**.cn", "GitHub.com", "mail.google.com", "a.cn")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user