From a251833107a4df5cae7c990663c7f50e186806a2 Mon Sep 17 00:00:00 2001 From: "LamTrinh.Dev" Date: Thu, 30 Apr 2026 21:30:12 +0700 Subject: [PATCH 1/4] perf(proxy): optimize subdomain checking by building candidates incrementally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace O(n²) strings.Join in loop with O(n) incremental string building. For domains with many subdomains (e.g., a.b.c.d.e.com), this reduces string operations from n*(n+1)/2 to n. Before: strings.Join(parts[i:], ".") in loop creates n+(n-1)+...+1 operations After: Build strings incrementally by prepending parts: n operations --- proxy/filter/blocklists.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/proxy/filter/blocklists.go b/proxy/filter/blocklists.go index b6a97fed..4df68664 100644 --- a/proxy/filter/blocklists.go +++ b/proxy/filter/blocklists.go @@ -50,8 +50,20 @@ func (f *DomainFilter) filterBlocklists(reqCtx *requestcontext.RequestContext, d if reqCtx.PrivacySettings[SUBDOMAINS_RULE] == RULE_BLOCK { // iterate over all subdomains parts := strings.Split(fqdn, ".") - for i := range len(parts) - 1 { - candidate := strings.Join(parts[i:], ".") + var candidate string + for i := len(parts) - 1; i >= 0; i-- { + // Build candidate incrementally by prepending current part + if i == len(parts)-1 { + candidate = parts[i] + } else { + candidate = parts[i] + "." + candidate + } + + // Skip the full domain as it was already checked above + if i == 0 { + continue + } + // now, check if candidate domain is part of any blocklist entry blocklisted, err = f.Cache.GetBlocklistEntry(context.Background(), blocklistId, candidate) if err != nil { From 0edb6c1765fd52ef69765e6af83af1fe3411e207 Mon Sep 17 00:00:00 2001 From: "LamTrinh.Dev" Date: Fri, 29 May 2026 22:17:39 +0700 Subject: [PATCH 2/4] Enhance after Copilot feedback. --- proxy/filter/blocklists.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/proxy/filter/blocklists.go b/proxy/filter/blocklists.go index 4df68664..59410852 100644 --- a/proxy/filter/blocklists.go +++ b/proxy/filter/blocklists.go @@ -48,22 +48,17 @@ func (f *DomainFilter) filterBlocklists(reqCtx *requestcontext.RequestContext, d } if reqCtx.PrivacySettings[SUBDOMAINS_RULE] == RULE_BLOCK { - // iterate over all subdomains + // iterate over all subdomains (excluding TLD and full FQDN) parts := strings.Split(fqdn, ".") var candidate string - for i := len(parts) - 1; i >= 0; i-- { + for i := len(parts) - 2; i >= 0; i-- { // Build candidate incrementally by prepending current part - if i == len(parts)-1 { - candidate = parts[i] + if i == len(parts)-2 { + candidate = parts[i] + "." + parts[i+1] } else { candidate = parts[i] + "." + candidate } - // Skip the full domain as it was already checked above - if i == 0 { - continue - } - // now, check if candidate domain is part of any blocklist entry blocklisted, err = f.Cache.GetBlocklistEntry(context.Background(), blocklistId, candidate) if err != nil { From 36cbcf2b94abbeb1a74c5d6ea2009d68a5c684bf Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 21 Jul 2026 09:05:43 +0200 Subject: [PATCH 3/4] perf(proxy): Skip redundant full-FQDN re-check in subdomain loop Signed-off-by: Maciek --- proxy/filter/blocklists.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/proxy/filter/blocklists.go b/proxy/filter/blocklists.go index 59410852..16702c40 100644 --- a/proxy/filter/blocklists.go +++ b/proxy/filter/blocklists.go @@ -48,10 +48,11 @@ func (f *DomainFilter) filterBlocklists(reqCtx *requestcontext.RequestContext, d } if reqCtx.PrivacySettings[SUBDOMAINS_RULE] == RULE_BLOCK { - // iterate over all subdomains (excluding TLD and full FQDN) + // iterate over all parent domains, excluding the TLD and the full + // FQDN (already covered by the exact-match check above) parts := strings.Split(fqdn, ".") var candidate string - for i := len(parts) - 2; i >= 0; i-- { + for i := len(parts) - 2; i >= 1; i-- { // Build candidate incrementally by prepending current part if i == len(parts)-2 { candidate = parts[i] + "." + parts[i+1] From 57880de36852c3d3d5895fb1a7d5c6304ec6251d Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 21 Jul 2026 09:06:52 +0200 Subject: [PATCH 4/4] test(proxy): Add benchmarks for subdomain candidate building Signed-off-by: Maciek --- proxy/filter/blocklists_benchmark_test.go | 90 +++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 proxy/filter/blocklists_benchmark_test.go diff --git a/proxy/filter/blocklists_benchmark_test.go b/proxy/filter/blocklists_benchmark_test.go new file mode 100644 index 00000000..c78f22df --- /dev/null +++ b/proxy/filter/blocklists_benchmark_test.go @@ -0,0 +1,90 @@ +package filter + +import ( + "strings" + "testing" +) + +// Benchmarks for the subdomain candidate-building strategies used by +// filterBlocklists. "Join" is the previous implementation (strings.Join per +// suffix), "Prepend" is the current one (incremental prepending). Both emit +// the same candidate set: every parent domain excluding the TLD and the full +// FQDN. In production each candidate is followed by a blocklist cache lookup, +// which dominates the cost of this loop; these benchmarks isolate the string +// construction itself. + +var subdomainBenchDomains = []struct { + name string + fqdn string +}{ + {"4_Labels", "a.b.c.com"}, + {"7_Labels", "a.b.c.d.e.f.com"}, + {"11_Labels", "a.b.c.d.e.f.g.h.i.j.com"}, +} + +var benchCandidateSink string + +func joinCandidates(fqdn string, visit func(string)) { + parts := strings.Split(fqdn, ".") + for i := 1; i < len(parts)-1; i++ { + visit(strings.Join(parts[i:], ".")) + } +} + +func prependCandidates(fqdn string, visit func(string)) { + parts := strings.Split(fqdn, ".") + var candidate string + for i := len(parts) - 2; i >= 1; i-- { + if i == len(parts)-2 { + candidate = parts[i] + "." + parts[i+1] + } else { + candidate = parts[i] + "." + candidate + } + visit(candidate) + } +} + +func BenchmarkSubdomainCandidatesJoin(b *testing.B) { + for _, tc := range subdomainBenchDomains { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + joinCandidates(tc.fqdn, func(c string) { benchCandidateSink = c }) + } + }) + } +} + +func BenchmarkSubdomainCandidatesPrepend(b *testing.B) { + for _, tc := range subdomainBenchDomains { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + prependCandidates(tc.fqdn, func(c string) { benchCandidateSink = c }) + } + }) + } +} + +// TestSubdomainCandidatesEquivalence guards the refactoring: both strategies +// must produce the identical candidate set, in reverse order of each other. +func TestSubdomainCandidatesEquivalence(t *testing.T) { + fqdns := []string{"com", "b.com", "a.b.com", "a.b.c.com", "a.b.c.d.e.f.g.h.i.j.com"} + for _, fqdn := range fqdns { + var joined, prepended []string + joinCandidates(fqdn, func(c string) { joined = append(joined, c) }) + prependCandidates(fqdn, func(c string) { prepended = append(prepended, c) }) + + for i, j := 0, len(prepended)-1; i < len(prepended)/2; i, j = i+1, j-1 { + prepended[i], prepended[j] = prepended[j], prepended[i] + } + if len(joined) != len(prepended) { + t.Fatalf("%s: candidate count mismatch: %v vs %v", fqdn, joined, prepended) + } + for i := range joined { + if joined[i] != prepended[i] { + t.Fatalf("%s: candidate mismatch at %d: %q vs %q", fqdn, i, joined[i], prepended[i]) + } + } + } +}