Skip to content

Commit ac0caf8

Browse files
committed
perf: Optimize clone
Introduce the notion of CompiledPolicy to hold stuff that remains immutable during evaluation - e.g. rules, function, schedules etc Cloning takes about 60 nano seconds for an engine loaded with ACI policies. Earlier it used to take 40 microseconds. Thus there is easily more than 100x speedup. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
1 parent 90b4ec6 commit ac0caf8

3 files changed

Lines changed: 210 additions & 93 deletions

File tree

benches/regorus_benchmark.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::hint::black_box;
22

3-
use regorus::Engine;
3+
use regorus::{Engine, Value};
44

55
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
66
use serde_json::json;
@@ -93,9 +93,59 @@ fn allow_with_simple_membership(c: &mut Criterion) {
9393
group.finish();
9494
}
9595

96+
fn clone(c: &mut Criterion) {
97+
// Use Arc<BtreeMap> as a reference. Clone will only increment
98+
// the reference count.
99+
let mut m = std::collections::BTreeMap::default();
100+
m.insert(1, 2);
101+
let m = std::sync::Arc::new(m);
102+
103+
c.bench_function("clone: Arc<BTreeMap>", |b| {
104+
b.iter(|| {
105+
let _ = m.clone();
106+
})
107+
});
108+
109+
let mut engine = Engine::new();
110+
engine.set_rego_v0(true);
111+
engine
112+
.add_policy_from_file("tests/aci/framework.rego")
113+
.unwrap();
114+
engine.add_policy_from_file("tests/aci/api.rego").unwrap();
115+
engine
116+
.add_policy_from_file("tests/aci/policy.rego")
117+
.unwrap();
118+
engine
119+
.add_data(Value::from_json_file("tests/aci/data.json").expect("failed to load data.json"))
120+
.expect("failed to add data");
121+
engine.set_input(
122+
Value::from_json_file("tests/aci/input.json").expect("failed to load input.json"),
123+
);
124+
125+
// An engine without preparation will not have processed fields populated.
126+
c.bench_function("clone: engine with aci policies", |b| {
127+
b.iter(|| {
128+
let _ = engine.clone();
129+
})
130+
});
131+
132+
// Trigger engine preparation.
133+
let _ = engine.eval_query("data.framework.mount_overlay".to_string(), false);
134+
135+
// Prepared engine will have many more fields populated. But the fields are
136+
// immutable after preparation and will be shared between clones.
137+
c.bench_function("clone: prepared engine with aci policies", |b| {
138+
b.iter(|| {
139+
let _ = engine.clone();
140+
})
141+
});
142+
}
143+
96144
criterion_group!(
97145
benches,
98146
allow_with_simple_equality,
99-
allow_with_simple_membership
147+
allow_with_simple_membership,
148+
clone
100149
);
150+
101151
criterion_main!(benches);

src/engine.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use anyhow::{bail, Result};
1717
///
1818
#[derive(Debug, Clone)]
1919
pub struct Engine {
20-
modules: Vec<Ref<Module>>,
20+
modules: Rc<Vec<Ref<Module>>>,
2121
interpreter: Interpreter,
2222
prepared: bool,
2323
rego_v1: bool,
@@ -34,7 +34,7 @@ impl Engine {
3434
/// Create an instance of [Engine].
3535
pub fn new() -> Self {
3636
Self {
37-
modules: vec![],
37+
modules: Rc::new(vec![]),
3838
interpreter: Interpreter::new(),
3939
prepared: false,
4040
rego_v1: true,
@@ -100,7 +100,7 @@ impl Engine {
100100
let source = Source::from_contents(path, rego)?;
101101
let mut parser = self.make_parser(&source)?;
102102
let module = Ref::new(parser.parse()?);
103-
self.modules.push(module.clone());
103+
Rc::make_mut(&mut self.modules).push(module.clone());
104104
// if policies change, interpreter needs to be prepared again
105105
self.prepared = false;
106106
Interpreter::get_path_string(&module.package.refr, Some("data"))
@@ -133,7 +133,7 @@ impl Engine {
133133
let source = Source::from_file(path)?;
134134
let mut parser = self.make_parser(&source)?;
135135
let module = Ref::new(parser.parse()?);
136-
self.modules.push(module.clone());
136+
Rc::make_mut(&mut self.modules).push(module.clone());
137137
// if policies change, interpreter needs to be prepared again
138138
self.prepared = false;
139139
Interpreter::get_path_string(&module.package.refr, Some("data"))
@@ -603,7 +603,7 @@ impl Engine {
603603
let schedule = analyzer.analyze(&self.modules)?;
604604

605605
self.interpreter.set_schedule(Some(schedule));
606-
self.interpreter.set_modules(&self.modules);
606+
self.interpreter.set_modules(self.modules.clone());
607607

608608
self.interpreter.clear_builtins_cache();
609609
// clean_internal_evaluation_state will set data to an efficient clont of use supplied init_data
@@ -653,13 +653,13 @@ impl Engine {
653653
}
654654

655655
self.interpreter.check_default_rules()?;
656-
for module in self.modules.clone() {
656+
for module in self.modules.clone().iter() {
657657
for rule in &module.policy {
658-
self.interpreter.eval_rule(&module, rule)?;
658+
self.interpreter.eval_rule(module, rule)?;
659659
}
660660
}
661661
// Defer the evaluation of the default rules to here
662-
for module in self.modules.clone() {
662+
for module in self.modules.clone().iter() {
663663
let prev_module = self.interpreter.set_current_module(Some(module.clone()))?;
664664
for rule in &module.policy {
665665
self.interpreter.eval_default_rule(rule)?;
@@ -668,7 +668,7 @@ impl Engine {
668668
}
669669

670670
// Ensure that all modules are created.
671-
for m in &self.modules {
671+
for m in self.modules.iter() {
672672
let path = Parser::get_path_ref_components(&m.package.refr)?;
673673
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
674674
let vref =
@@ -896,7 +896,7 @@ impl Engine {
896896
ast: &'a Module,
897897
}
898898
let mut ast = vec![];
899-
for m in &self.modules {
899+
for m in self.modules.iter() {
900900
ast.push(Policy {
901901
source: &m.package.span.source,
902902
version: 1,

0 commit comments

Comments
 (0)