Skip to content

AdBlocker: request engine - #3245

Open
nikneym wants to merge 13 commits into
mainfrom
nikneym/adblock-request-engine
Open

AdBlocker: request engine#3245
nikneym wants to merge 13 commits into
mainfrom
nikneym/adblock-request-engine

Conversation

@nikneym

@nikneym nikneym commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

On-going effort; here's the recent benchmark made against EasyList + EasyPrivacy on this branch:

Adblock engine: EasyList + EasyPrivacy corpus report

Lists downloaded fresh from easylist.to the same day (135,908 lines total). Measured with
the per-request engine (tries + token-bucket indexes) after the adversarial-review fixes.

Coverage: what loads

EasyList EasyPrivacy Combined
File lines 79,233 56,675 135,908
Comments / blank 13,920 771 14,691
Rules 65,313 55,904 121,217
Cosmetic rules (not network) 10,852 32 10,884
Network rules 54,461 55,872 110,333
Loaded 54,266 (99.6%) 55,858 (99.97%) 110,124 (99.8%)
→ in hostname tries 45,264 42,955 88,219
→ engine-indexed 9,002 12,903 21,905

The 209 network rules we don't apply, by reason

  • 31 /regex/ filters (24 EL + 7 EP) — the only rules uBO enforces as
    blocks that we genuinely can't: there's still no regex engine. ~0.03% of the
    corpus.
  • 168 cosmetic-realm exceptions ($generichide/$elemhide/
    $specifichide) — they modify cosmetic filtering, which we don't have yet,
    so there's nothing for them to except.
  • 6 modifier-only rules ($removeparam-class) — uBO applies these as URL
    rewrites, never blocks; dropping them is the conservative, correct choice.
  • 3 malformed rules (EasyPrivacy) — uBO rejects these too.
  • 1 unsupported option (the @@…$redirect-rule google-analytics line —
    correctly treated as excepting nothing, so google-analytics.com stays
    blocked).

The ~10,884 "cosmetic" drops are element-hiding rules (##, #@# — including
944 with spaces in their CSS selectors that take the hosts-file parsing path);
no network engine applies those, so they're out of scope rather than failures.
Notably, zero hostnames land in the suppressed trie across the whole
corpus — every exception either evaluates per-request or provably unblocks
nothing.

Correctness on the loaded rules

  • A 26-assertion doubleclick battery against uBO ground truth passes with the
    review fixes in: gpt.js blocked except on the listed carve-out sites, the
    /ssai/ exception honored, wrong-type/wrong-site/wrong-path variants all
    correct, and the apex doubleclick.net undecided rather than suppressed.
  • End-to-end: lightpanda fetch on a page loading the real ad stack blocks
    gpt.js, td./googleads./fls.doubleclick.net and Google Analytics
    before they reach the network.

Bottom line

99.8% of the network rules in EasyList+EasyPrivacy load and enforce; the
only real gap is 31 regex filters.

@nikneym
nikneym force-pushed the nikneym/adblock-request-engine branch from a458be7 to 07af4c3 Compare August 25, 2026 11:02
@nikneym
nikneym marked this pull request as ready for review August 25, 2026 11:13
@nikneym
nikneym marked this pull request as draft August 25, 2026 11:51
@nikneym
nikneym marked this pull request as ready for review August 26, 2026 14:05
@nikneym
nikneym force-pushed the nikneym/adblock-request-engine branch from 686b310 to 931240e Compare August 26, 2026 14:12
@krichprollsch

Copy link
Copy Markdown
Member

It seems ||coinimp.com ||coinimp.com^ and ||coinimp.com| are all the same in ublock: hostname is coinimp.com or a subdomain

@krichprollsch

Copy link
Copy Markdown
Member

@@…$important seems to be invalid in ublock. we should return error.InvalidOption in this case.

Comment thread src/network/adblock/Engine.zig
Comment thread src/network/adblock/Engine.zig
Comment thread src/network/HttpClient.zig Outdated
@nikneym
nikneym marked this pull request as draft September 2, 2026 08:38
@nikneym
nikneym force-pushed the nikneym/adblock-request-engine branch from 931240e to 42abfa4 Compare September 2, 2026 08:39
@nikneym
nikneym marked this pull request as ready for review September 2, 2026 12:56
Comment thread src/browser/Frame.zig Outdated
Comment thread src/network/adblock/AdBlocker.zig
Comment thread src/network/adblock/AdBlocker.zig Outdated
Comment thread src/network/adblock/Engine.zig Outdated
Comment thread src/network/HttpClient.zig Outdated
Comment thread src/network/adblock/AdBlocker.zig Outdated
Comment thread src/network/adblock/Engine.zig Outdated
};
var it: Tokens = .{ .text = url };
while (it.next()) |token| {
if (request.tokens_len == request.tokens_buf.len) break;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, AdBlocker.match does:

    if (self.trie.matches(self.suppressed, hostname) != null) return .none;

    if (self.trie.matches(self.blocked_important, hostname) != null) return .blocked;
    if (self.blocking_important.match(request) != null) return .blocked;

    if (self.exceptions.match(request) != null) return .allowed;
    if (self.trie.matches(self.allowed, hostname) != null) return .allowed;

    if (self.trie.matches(self.blocked, hostname) != null) return .blocked;
    if (self.blocking.match(request) != null) return .blocked;

There are cases where you won't even need the tokens that you've already split / hashed. AND, you can rewrite that to be even more efficient:

  if (self.trie.matches(self.suppressed, hostname) != null) return .none;

  if (self.trie.matches(self.blocked_important, hostname) != null) return .blocked;
  if (self.blocking_important.match(request) != null) return .blocked;
  
  if (self.trie.matches(self.blocked, hostname) != null or self.blocking.match(request) != null) {
      if (self.trie.matches(self.allowed, hostname) != null) return .allowed;
      if (self.exceptions.match(request) != null) return .allowed;
      return .blocked;
  }
  return .none;

There's no need to check exceptions unless something is blocked (.allowed vs .none is just a metrics difference) AND there are fewer cases where you need to tokenize/hash anything.

With that in mind, don't pre-tokenize. Tokenize on demand. Whether or not it's worth caching in the tokenizer, I don't know. But it isn't worth limiting it to 128 tokens. Since the tokenizer becomes streaming, you can cache the first 128 values, and then stream until the end of the URL.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the tokenizer becomes streaming, you can cache the first 128 values, and then stream until the end of the URL.

I actually thought the same, Claude also recommended, but unsure how many cases would benefit from it. Currently overflow results to loss of tokens; comparing it with stream-the-rest approach.

@nikneym
nikneym marked this pull request as draft September 4, 2026 08:35
`Parser`: don't drop cosmetic lines

introduce url pattern matcher

yet-another-url-parser
* Engine.Request.fromHttp(req, source_url, buffers) now builds the adblock request straight from HttpClient.Request.
* The URL is tokenized once per request (hashed into the Request, shared by all engines); capped at 128 tokens (same as adblock-rust).
* Document hostname longer than 253 bytes now skips adblocking.
* `@@…$important` -> `error.InvalidOption`
* `||host`, `||host^`, `||host|` (and bare `host|`/`|host` lines) all read as "hostname or subdomain" and land in the trie when option-free.
* Wildcard trimming now follows uBO's pointless-wildcard rules.
* remove unnecessary `pub` marking
* metrics tracking cosmetic filters separately
Eager tokenization is ~7% of a ~2.1 µs match. A lazy fill saved nothing because every request reaches the first engine. Streaming from scratch per engine cost +10% capped and +16% uncapped, and stayed +10% even with exception gating. The 128 cap changed 1 verdict in 242,908. So we went with hybrid approach: no token is lost, and 97% of URLs still pay one tokenization.
@nikneym
nikneym force-pushed the nikneym/adblock-request-engine branch from e993082 to 2c5603a Compare September 4, 2026 09:08
@nikneym
nikneym marked this pull request as ready for review September 4, 2026 09:09
Solely to pass failing metrics test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants