use std::time::Instant;
use serde_json::json;
use super::{eval::tests::TestRequest, model::Rule, Rules};
fn request_line_rules(count: usize) -> Vec<Rule> {
(0..count)
.map(|i| {
let condition = match i % 4 {
0 => json!({ "and": {
"method": { "eq": "POST" },
"path": { "prefix": format!("/api/v{i}/") },
} }),
1 => json!({ "and": {
"host": { "eq": format!("app-{i}.example.com") },
"not": { "path": { "eq": "/health" } },
} }),
2 => json!({ "and": { "path": { "wildcard": format!("/admin/{i}/*") } } }),
_ => json!({ "and": {
"client": { "in": [format!("10.{}.0.0/16", i % 256)] },
"method": { "neq": "GET" },
} }),
};
named(condition, i)
})
.collect()
}
fn named(condition: serde_json::Value, i: usize) -> Rule {
let mut rule = condition;
rule["name"] = json!(format!("rule {i}"));
rule["then"] = json!({ "respond": { "status": 403 } });
Rule(rule)
}
fn rules(count: usize) -> Vec<Rule> {
(0..count)
.map(|i| {
let condition = match i % 5 {
0 => json!({ "and": {
"method": { "eq": "POST" },
"path": { "prefix": format!("/api/v{i}/") },
} }),
1 => json!({ "and": {
"host": { "eq": format!("app-{i}.example.com") },
"not": { "path": { "eq": "/health" } },
} }),
2 => json!({ "or": {
"header": { "x-client": { "eq": format!("legacy-{i}") } },
"cookie": { "beta": { "eq": format!("{i}") } },
} }),
3 => json!({ "and": {
"client": { "in": [format!("10.{}.0.0/16", i % 256)] },
"path": { "wildcard": format!("/admin/{i}/*") },
} }),
_ => json!({ "and": {
"header": { "user-agent": { "matches": format!("(?i)bot-{i}") } },
} }),
};
named(condition, i)
})
.collect()
}
fn cost(rules: &[Rule]) -> f64 {
let count = rules.len();
let rules = Rules::compile(rules).unwrap().unwrap();
let request = TestRequest::new("GET", "www.example.com", "/products/42?ref=home")
.header(
"user-agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) Safari/605.1.15",
)
.header("cookie", "session=abc; theme=dark")
.header("accept", "text/html");
let iterations = 2_000_000 / count.max(1) + 20_000;
let mut matched = 0;
for _ in 0..iterations {
matched += rules.matching(&request);
}
let start = Instant::now();
for _ in 0..iterations {
matched += rules.matching(&request);
}
assert_eq!(matched, 0);
start.elapsed().as_nanos() as f64 / iterations as f64
}
#[test]
#[ignore = "a benchmark: run with --release"]
fn bench_rules() {
let mixes: [(&str, fn(usize) -> Vec<Rule>, [f64; 3]); 2] = [
(
"method, host, path, client",
request_line_rules,
[150.0, 300.0, 1500.0],
),
("also headers, cookie, regex", rules, [250.0, 600.0, 4000.0]),
];
for (mix, make, budgets) in mixes {
for (count, budget) in [10, 100, 1000].into_iter().zip(budgets) {
let nanos = cost(&make(count));
println!("{mix:>28}, {count:>4} rules: {nanos:>7.1}ns (budget {budget}ns)");
if !cfg!(debug_assertions) {
assert!(
nanos <= budget,
"{mix}, {count} rules: {nanos:.1}ns, over {budget}ns"
);
}
}
}
}