Skip to content

Commit 61d339e

Browse files
authored
Merge pull request #417 from brave/setup-basic-perf-ci
Setup basic perf CI
2 parents 990817c + 199d257 commit 61d339e

File tree

16 files changed

+283717
-156243
lines changed

16 files changed

+283717
-156243
lines changed

Diff for: .github/workflows/perf-ci.yml

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# CI for performance benchmarking
2+
# Domains of interest:
3+
# * startup speed (== to parse and load all rules)
4+
# * network filter matching (== the avg time to check a request)
5+
# * first request matching delay (== time to check the first request)
6+
# * memory usage after loading rules and after a few requests
7+
name: Performance CI
8+
9+
on:
10+
push:
11+
branches: [ master ]
12+
pull_request:
13+
14+
permissions:
15+
contents: write
16+
pages: write
17+
pull-requests: write
18+
19+
jobs:
20+
benchmark:
21+
name: Performance benchmarking
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
27+
28+
- name: Bench network filter matching
29+
run: cargo bench --bench bench_matching rule-match-browserlike/brave-list -- --output-format bencher | tee -a output.txt
30+
31+
- name: Bench first request matching delay
32+
run: cargo bench --bench bench_matching rule-match-first-request -- --output-format bencher | tee -a output.txt
33+
34+
- name: Bench startup speed
35+
run: cargo bench --bench bench_rules blocker_new/brave-list -- --output-format bencher | tee -a output.txt
36+
37+
- name: Bench memory usage
38+
run: cargo bench --bench bench_memory -- --output-format bencher | tee -a output.txt
39+
40+
- name: Store benchmark result
41+
uses: benchmark-action/github-action-benchmark@d48d326b4ca9ba73ca0cd0d59f108f9e02a381c7 # v1.20.4
42+
with:
43+
name: Rust Benchmark
44+
tool: 'cargo'
45+
output-file-path: output.txt
46+
github-token: ${{ secrets.GITHUB_TOKEN }}
47+
alert-threshold: '130%' # fails on +30% regression
48+
comment-on-alert: true
49+
fail-on-alert: true
50+
comment-always: true

Diff for: Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ harness = false
7575
name = "bench_redirect_performance"
7676
harness = false
7777

78+
79+
[[bench]]
80+
name = "bench_memory"
81+
harness = false
82+
7883
# Currently disabled, as cosmetic filter internals
7984
# are no longer part of the crate's public API
8085
#[[bench]]

Diff for: benches/bench_matching.rs

+50-10
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ fn bench_matching_only(blocker: &Blocker, resources: &ResourceStorage, requests:
7676
(matches, passes)
7777
}
7878

79+
type ParsedRequest = (String, String, String, String, bool);
80+
7981
fn bench_rule_matching_browserlike(
8082
blocker: &Engine,
81-
requests: &Vec<(String, String, String, String, bool)>,
83+
requests: &Vec<ParsedRequest>,
8284
) -> (u32, u32) {
8385
let mut matches = 0;
8486
let mut passes = 0;
@@ -331,27 +333,64 @@ fn rule_match_browserlike_comparable(c: &mut Criterion) {
331333
.collect::<Vec<_>>()
332334
}
333335

334-
let elep_req = requests_parsed(&requests);
335-
let el_req = elep_req.clone();
336-
let slim = elep_req.clone();
336+
let requests = requests_parsed(&requests);
337337

338-
group.bench_function("el+ep", move |b| {
338+
group.bench_function("el+ep", |b| {
339339
let rules = rules_from_lists(&[
340340
"data/easylist.to/easylist/easylist.txt",
341341
"data/easylist.to/easylist/easyprivacy.txt",
342342
]);
343343
let engine = Engine::from_rules_parametrised(rules, Default::default(), false, true);
344-
b.iter(|| bench_rule_matching_browserlike(&engine, &elep_req))
344+
b.iter(|| bench_rule_matching_browserlike(&engine, &requests))
345345
});
346-
group.bench_function("el", move |b| {
346+
group.bench_function("el", |b| {
347347
let rules = rules_from_lists(&["data/easylist.to/easylist/easylist.txt"]);
348348
let engine = Engine::from_rules_parametrised(rules, Default::default(), false, true);
349-
b.iter(|| bench_rule_matching_browserlike(&engine, &el_req))
349+
b.iter(|| bench_rule_matching_browserlike(&engine, &requests))
350350
});
351-
group.bench_function("slimlist", move |b| {
351+
group.bench_function("slimlist", |b| {
352352
let rules = rules_from_lists(&["data/slim-list.txt"]);
353353
let engine = Engine::from_rules_parametrised(rules, Default::default(), false, true);
354-
b.iter(|| bench_rule_matching_browserlike(&engine, &slim))
354+
b.iter(|| bench_rule_matching_browserlike(&engine, &requests))
355+
});
356+
group.bench_function("brave-list", |b| {
357+
let rules = rules_from_lists(&["data/brave/brave-main-list.txt"]);
358+
let engine = Engine::from_rules_parametrised(rules, Default::default(), false, true);
359+
b.iter(|| bench_rule_matching_browserlike(&engine, &requests))
360+
});
361+
362+
group.finish();
363+
}
364+
365+
fn rule_match_first_request(c: &mut Criterion) {
366+
let mut group = c.benchmark_group("rule-match-first-request");
367+
368+
group.sample_size(10);
369+
370+
let requests: Vec<ParsedRequest> = vec![(
371+
"https://example.com".to_string(),
372+
"example.com".to_string(),
373+
"example.com".to_string(),
374+
"document".to_string(),
375+
false,
376+
)];
377+
378+
group.bench_function("brave-list", |b| {
379+
b.iter_custom(
380+
|iters| {
381+
let mut total_time = std::time::Duration::ZERO;
382+
for _ in 0..iters {
383+
let rules = rules_from_lists(&["data/brave/brave-main-list.txt"]);
384+
let engine = Engine::from_rules_parametrised(rules, Default::default(), false, true);
385+
386+
// Measure only the matching time, skip setup and destruction
387+
let start_time = std::time::Instant::now();
388+
bench_rule_matching_browserlike(&engine, &requests);
389+
total_time += start_time.elapsed();
390+
}
391+
total_time
392+
}
393+
)
355394
});
356395

357396
group.finish();
@@ -363,6 +402,7 @@ criterion_group!(
363402
rule_match_parsed_el,
364403
rule_match_parsed_elep_slimlist,
365404
rule_match_browserlike_comparable,
405+
rule_match_first_request,
366406
serialization,
367407
deserialization
368408
);

Diff for: benches/bench_memory.rs

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
4+
* You can obtain one at https://mozilla.org/MPL/2.0/. */
5+
6+
use criterion::*;
7+
use std::alloc::{GlobalAlloc, Layout, System};
8+
use std::sync::atomic::{AtomicUsize, Ordering};
9+
use serde::{Deserialize, Serialize};
10+
11+
use adblock::Engine;
12+
use adblock::request::Request;
13+
14+
#[path = "../tests/test_utils.rs"]
15+
mod test_utils;
16+
use test_utils::rules_from_lists;
17+
18+
// Custom allocator to track memory usage
19+
#[global_allocator]
20+
static ALLOCATOR: MemoryTracker = MemoryTracker::new();
21+
22+
struct MemoryTracker {
23+
allocated: AtomicUsize,
24+
internal: System,
25+
}
26+
27+
impl MemoryTracker {
28+
const fn new() -> Self {
29+
Self {
30+
allocated: AtomicUsize::new(0),
31+
internal: System,
32+
}
33+
}
34+
35+
fn current_usage(&self) -> usize {
36+
self.allocated.load(Ordering::SeqCst)
37+
}
38+
39+
fn reset(&self) {
40+
self.allocated.store(0, Ordering::SeqCst);
41+
}
42+
}
43+
44+
unsafe impl GlobalAlloc for MemoryTracker {
45+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
46+
let ret = self.internal.alloc(layout);
47+
if !ret.is_null() {
48+
self.allocated.fetch_add(layout.size(), Ordering::SeqCst);
49+
}
50+
ret
51+
}
52+
53+
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
54+
self.internal.dealloc(ptr, layout);
55+
self.allocated.fetch_sub(layout.size(), Ordering::SeqCst);
56+
}
57+
58+
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
59+
let ret = self.internal.realloc(ptr, layout, new_size);
60+
if !ret.is_null() {
61+
self.allocated.fetch_sub(layout.size(), Ordering::SeqCst);
62+
self.allocated.fetch_add(new_size, Ordering::SeqCst);
63+
}
64+
ret
65+
}
66+
67+
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
68+
let ret = self.internal.alloc_zeroed(layout);
69+
if !ret.is_null() {
70+
self.allocated.fetch_add(layout.size(), Ordering::SeqCst);
71+
}
72+
ret
73+
}
74+
}
75+
76+
#[allow(non_snake_case)]
77+
#[derive(Serialize, Deserialize, Clone)]
78+
struct TestRequest {
79+
frameUrl: String,
80+
url: String,
81+
cpt: String,
82+
}
83+
84+
impl From<&TestRequest> for Request {
85+
fn from(v: &TestRequest) -> Self {
86+
Request::new(&v.url, &v.frameUrl, &v.cpt).unwrap()
87+
}
88+
}
89+
90+
fn load_requests() -> Vec<TestRequest> {
91+
let requests_str = rules_from_lists(&["data/requests.json"]);
92+
let reqs: Vec<TestRequest> = requests_str
93+
.into_iter()
94+
.map(|r| serde_json::from_str(&r))
95+
.filter_map(Result::ok)
96+
.collect();
97+
reqs
98+
}
99+
100+
fn bench_memory_usage(c: &mut Criterion) {
101+
let mut group = c.benchmark_group("memory-usage");
102+
group.sample_size(10);
103+
group.measurement_time(std::time::Duration::from_secs(1));
104+
105+
let mut noise = 0;
106+
let all_requests = load_requests();
107+
let first_1000_requests: Vec<_> = all_requests.iter().take(1000).collect();
108+
109+
group.bench_function("brave-list-initial", |b| {
110+
let mut result = 0;
111+
b.iter_custom(|iters| {
112+
for _ in 0..iters {
113+
ALLOCATOR.reset();
114+
let rules = rules_from_lists(&["data/brave/brave-main-list.txt"]);
115+
let engine = Engine::from_rules(rules, Default::default());
116+
117+
noise += 1; // add some noise to make criterion happy
118+
result += ALLOCATOR.current_usage() + noise;
119+
120+
// Prevent engine from being optimized
121+
criterion::black_box(&engine);
122+
}
123+
124+
// Return the memory usage as a Duration
125+
std::time::Duration::from_nanos(result as u64)
126+
});
127+
});
128+
129+
group.bench_function("brave-list-after-1000-requests", |b| {
130+
b.iter_custom(|iters| {
131+
let mut result = 0;
132+
for _ in 0..iters {
133+
ALLOCATOR.reset();
134+
let rules = rules_from_lists(&["data/brave/brave-main-list.txt"]);
135+
let engine = Engine::from_rules(rules, Default::default());
136+
137+
for request in first_1000_requests.clone() {
138+
criterion::black_box(engine.check_network_request(&request.into()));
139+
}
140+
141+
noise += 1; // add some noise to make criterion happy
142+
result += ALLOCATOR.current_usage() + noise;
143+
144+
// Prevent engine from being optimized
145+
criterion::black_box(&engine);
146+
}
147+
148+
// Return the memory usage as a Duration
149+
std::time::Duration::from_nanos(result as u64)
150+
})
151+
});
152+
153+
group.finish();
154+
}
155+
156+
criterion_group!(benches, bench_memory_usage);
157+
criterion_main!(benches);

Diff for: benches/bench_rules.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@ fn list_parse(c: &mut Criterion) {
8484
fn get_blocker(rules: impl IntoIterator<Item=impl AsRef<str>>) -> Blocker {
8585
let (network_filters, _) = adblock::lists::parse_filters(rules, false, Default::default());
8686

87-
println!("Got {} network filters", network_filters.len());
88-
8987
let blocker_options = BlockerOptions {
9088
enable_optimizations: true,
9189
};
@@ -99,12 +97,16 @@ fn blocker_new(c: &mut Criterion) {
9997
group.throughput(Throughput::Elements(1));
10098
group.sample_size(10);
10199

102-
let rules: Vec<_> = rules_from_lists(&[
100+
let easylist_rules: Vec<_> = rules_from_lists(&[
103101
"data/easylist.to/easylist/easylist.txt",
104102
"data/easylist.to/easylist/easyprivacy.txt",
105103
]).collect();
104+
let brave_list_rules: Vec<_> = rules_from_lists(&[
105+
"data/brave/brave-main-list.txt",
106+
]).collect();
106107

107-
group.bench_function("el+ep", move |b| b.iter(|| get_blocker(&rules)));
108+
group.bench_function("el+ep", move |b| b.iter(|| get_blocker(&easylist_rules)));
109+
group.bench_function("brave-list", move |b| b.iter(|| get_blocker(&brave_list_rules)));
108110

109111
group.finish();
110112
}

0 commit comments

Comments
 (0)