Skip to content

Commit dbba57f

Browse files
authored
perf: Optimize clone (microsoft#443)
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 a8384da commit dbba57f

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,
@@ -59,7 +59,7 @@ impl Engine {
5959
/// Create an instance of [Engine].
6060
pub fn new() -> Self {
6161
Self {
62-
modules: vec![],
62+
modules: Rc::new(vec![]),
6363
interpreter: Interpreter::new(),
6464
prepared: false,
6565
rego_v1: true,
@@ -125,7 +125,7 @@ impl Engine {
125125
let source = Source::from_contents(path, rego)?;
126126
let mut parser = self.make_parser(&source)?;
127127
let module = Ref::new(parser.parse()?);
128-
self.modules.push(module.clone());
128+
Rc::make_mut(&mut self.modules).push(module.clone());
129129
// if policies change, interpreter needs to be prepared again
130130
self.prepared = false;
131131
Interpreter::get_path_string(&module.package.refr, Some("data"))
@@ -158,7 +158,7 @@ impl Engine {
158158
let source = Source::from_file(path)?;
159159
let mut parser = self.make_parser(&source)?;
160160
let module = Ref::new(parser.parse()?);
161-
self.modules.push(module.clone());
161+
Rc::make_mut(&mut self.modules).push(module.clone());
162162
// if policies change, interpreter needs to be prepared again
163163
self.prepared = false;
164164
Interpreter::get_path_string(&module.package.refr, Some("data"))
@@ -628,7 +628,7 @@ impl Engine {
628628
let schedule = analyzer.analyze(&self.modules)?;
629629

630630
self.interpreter.set_schedule(Some(schedule));
631-
self.interpreter.set_modules(&self.modules);
631+
self.interpreter.set_modules(self.modules.clone());
632632

633633
self.interpreter.clear_builtins_cache();
634634
// clean_internal_evaluation_state will set data to an efficient clont of use supplied init_data
@@ -678,13 +678,13 @@ impl Engine {
678678
}
679679

680680
self.interpreter.check_default_rules()?;
681-
for module in self.modules.clone() {
681+
for module in self.modules.clone().iter() {
682682
for rule in &module.policy {
683-
self.interpreter.eval_rule(&module, rule)?;
683+
self.interpreter.eval_rule(module, rule)?;
684684
}
685685
}
686686
// Defer the evaluation of the default rules to here
687-
for module in self.modules.clone() {
687+
for module in self.modules.clone().iter() {
688688
let prev_module = self.interpreter.set_current_module(Some(module.clone()))?;
689689
for rule in &module.policy {
690690
self.interpreter.eval_default_rule(rule)?;
@@ -693,7 +693,7 @@ impl Engine {
693693
}
694694

695695
// Ensure that all modules are created.
696-
for m in &self.modules {
696+
for m in self.modules.iter() {
697697
let path = Parser::get_path_ref_components(&m.package.refr)?;
698698
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
699699
let vref =
@@ -921,7 +921,7 @@ impl Engine {
921921
ast: &'a Module,
922922
}
923923
let mut ast = vec![];
924-
for m in &self.modules {
924+
for m in self.modules.iter() {
925925
ast.push(Policy {
926926
source: &m.package.span.source,
927927
version: 1,

0 commit comments

Comments
 (0)