mirror of
https://github.com/gorules/zen.git
synced 2026-10-04 00:02:18 +00:00
205 lines
7.6 KiB
Rust
205 lines
7.6 KiB
Rust
use std::time::Instant;
|
|
|
|
use serde_json::{json, Map, Value};
|
|
use zen_engine::model::DecisionContent;
|
|
use zen_engine::policy::{DiagnosticCode, Workspace};
|
|
|
|
fn graph(table: Value) -> DecisionContent {
|
|
let schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"amount": { "type": "number" },
|
|
"country": { "type": "string" },
|
|
"segment": { "type": "string" },
|
|
"vip": { "type": "boolean" },
|
|
"age": { "type": "number" },
|
|
"httpCode": { "type": "number" },
|
|
"appCode": { "type": "string" }
|
|
},
|
|
"required": ["amount", "country", "segment", "vip", "age", "httpCode", "appCode"]
|
|
});
|
|
serde_json::from_value(json!({
|
|
"nodes": [
|
|
{ "id": "in", "name": "request", "type": "inputNode", "content": { "schema": schema.to_string() } },
|
|
table,
|
|
{ "id": "out", "name": "out", "type": "outputNode", "content": {} }
|
|
],
|
|
"edges": [
|
|
{ "id": "e1", "sourceId": "in", "targetId": "dt" },
|
|
{ "id": "e2", "sourceId": "dt", "targetId": "out" }
|
|
]
|
|
}))
|
|
.expect("graph")
|
|
}
|
|
|
|
fn table(inputs: Value, outputs: Value, rules: Vec<Value>) -> Value {
|
|
json!({ "id": "dt", "name": "bench", "type": "decisionTableNode", "content": {
|
|
"hitPolicy": "first", "inputs": inputs, "outputs": outputs, "rules": rules, "passThrough": true
|
|
} })
|
|
}
|
|
|
|
const COUNTRIES: [&str; 6] = [
|
|
"\"US\", \"CA\"",
|
|
"\"MX\"",
|
|
"\"GB\", \"IE\"",
|
|
"\"DE\", \"AT\", \"CH\"",
|
|
"\"FR\"",
|
|
"\"JP\"",
|
|
];
|
|
const SEGMENTS: [&str; 4] = ["\"retail\"", "\"sme\"", "\"corporate\"", "\"public\""];
|
|
|
|
fn grid(rows: usize, compressible: bool) -> Value {
|
|
let inputs = json!([
|
|
{ "id": "a", "name": "Amount", "field": "amount" },
|
|
{ "id": "c", "name": "Country", "field": "country" },
|
|
{ "id": "s", "name": "Segment", "field": "segment" },
|
|
{ "id": "v", "name": "VIP", "field": "vip" },
|
|
{ "id": "g", "name": "Age", "field": "age" }
|
|
]);
|
|
let outputs = json!([ { "id": "r", "name": "Rate", "field": "rate" } ]);
|
|
let mut rules = Vec::with_capacity(rows);
|
|
let ages = [(18, 25), (25, 35), (35, 50), (50, 65), (65, 120)];
|
|
'outer: for band in 0.. {
|
|
for (ci, country) in COUNTRIES.iter().enumerate() {
|
|
for (si, segment) in SEGMENTS.iter().enumerate() {
|
|
for vip in [true, false] {
|
|
for (gi, (lo, hi)) in ages.iter().enumerate() {
|
|
if rules.len() >= rows {
|
|
break 'outer;
|
|
}
|
|
let rate = if compressible {
|
|
format!("{}", band as f64 * 0.001)
|
|
} else {
|
|
format!(
|
|
"{}",
|
|
(band * 997 + ci * 131 + si * 31 + gi * 7 + vip as usize) % 1000
|
|
)
|
|
};
|
|
let mut rule = Map::new();
|
|
rule.insert("_id".into(), json!(format!("r{}", rules.len())));
|
|
rule.insert(
|
|
"a".into(),
|
|
json!(format!(">= {} and < {}", band * 1000, (band + 1) * 1000)),
|
|
);
|
|
rule.insert("c".into(), json!(country));
|
|
rule.insert("s".into(), json!(segment));
|
|
rule.insert("v".into(), json!(vip.to_string()));
|
|
rule.insert("g".into(), json!(format!("[{lo}..{hi})")));
|
|
rule.insert("r".into(), json!(rate));
|
|
rules.push(Value::Object(rule));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
table(inputs, outputs, rules)
|
|
}
|
|
|
|
fn lookup(rows: usize) -> Value {
|
|
let inputs = json!([
|
|
{ "id": "h", "name": "HttpCode", "field": "httpCode" },
|
|
{ "id": "p", "name": "AppCode", "field": "appCode" }
|
|
]);
|
|
let outputs = json!([
|
|
{ "id": "o", "name": "Reason", "field": "reasonCode" },
|
|
{ "id": "x", "name": "Serviceable", "field": "serviceable" }
|
|
]);
|
|
let rules = (0..rows)
|
|
.map(|i| {
|
|
let code = ["200", "400", "500"][i % 3];
|
|
json!({
|
|
"_id": format!("r{i}"),
|
|
"h": code,
|
|
"p": format!("'SERV_{i:05}'"),
|
|
"o": format!("'R_{i:05}'"),
|
|
"x": if i % 5 == 0 { "true" } else { "false" }
|
|
})
|
|
})
|
|
.collect();
|
|
table(inputs, outputs, rules)
|
|
}
|
|
|
|
fn measure(label: &str, table: Value) {
|
|
let content = graph(table);
|
|
let mut ws = Workspace::new();
|
|
ws.set_document("g", content);
|
|
let start = Instant::now();
|
|
let diagnostics = ws.diagnostics("g");
|
|
let elapsed = start.elapsed();
|
|
let count = |code: DiagnosticCode| diagnostics.iter().filter(|d| d.code == code).count();
|
|
println!(
|
|
"{label:<28} live {:>9.1} ms missing={} compress={} unreachable={} duplicate={} incomplete={} total={}",
|
|
elapsed.as_secs_f64() * 1000.0,
|
|
count(DiagnosticCode::MissingCases),
|
|
count(DiagnosticCode::CompressibleTable),
|
|
count(DiagnosticCode::UnreachableRule),
|
|
count(DiagnosticCode::DuplicateRule),
|
|
count(DiagnosticCode::TableChecksIncomplete),
|
|
diagnostics.len()
|
|
);
|
|
}
|
|
|
|
fn chained(tables: usize, rows: usize) -> DecisionContent {
|
|
let schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"amount": { "type": "number" }, "country": { "type": "string" },
|
|
"segment": { "type": "string" }, "vip": { "type": "boolean" }, "age": { "type": "number" }
|
|
},
|
|
"required": ["amount", "country", "segment", "vip", "age"]
|
|
});
|
|
let mut nodes = vec![
|
|
json!({ "id": "in", "name": "request", "type": "inputNode", "content": { "schema": schema.to_string() } }),
|
|
];
|
|
let mut edges = Vec::new();
|
|
let mut previous = "in".to_string();
|
|
for t in 0..tables {
|
|
let mut node = grid(rows, t % 2 == 0);
|
|
let id = format!("dt{t}");
|
|
node["id"] = json!(id);
|
|
node["name"] = json!(format!("table{t}"));
|
|
node["content"]["outputs"] =
|
|
json!([ { "id": "r", "name": "Rate", "field": format!("rate{t}") } ]);
|
|
nodes.push(node);
|
|
edges.push(json!({ "id": format!("e{t}"), "sourceId": previous, "targetId": id }));
|
|
previous = id;
|
|
}
|
|
nodes.push(json!({ "id": "out", "name": "out", "type": "outputNode", "content": {} }));
|
|
edges.push(json!({ "id": "eout", "sourceId": previous, "targetId": "out" }));
|
|
serde_json::from_value(json!({ "nodes": nodes, "edges": edges })).expect("graph")
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn project_benchmark() {
|
|
let (files, tables, rows) = (300, 10, 50);
|
|
let mut ws = Workspace::new();
|
|
for f in 0..files {
|
|
ws.set_document(format!("g{f}"), chained(tables, rows));
|
|
}
|
|
let start = Instant::now();
|
|
let all = ws.all_diagnostics();
|
|
println!(
|
|
"project {files} files x {tables} tables x {rows} rows {:>9.1} ms diagnostics={}",
|
|
start.elapsed().as_secs_f64() * 1000.0,
|
|
all.len()
|
|
);
|
|
ws.set_document("g0", chained(tables, rows));
|
|
let start = Instant::now();
|
|
let _ = ws.all_diagnostics();
|
|
println!(
|
|
"project after one file edit {:>9.1} ms",
|
|
start.elapsed().as_secs_f64() * 1000.0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn table_benchmark() {
|
|
for rows in [200, 1_000, 2_000, 5_000, 10_000] {
|
|
measure(&format!("grid {rows}"), grid(rows, false));
|
|
measure(&format!("grid compressible {rows}"), grid(rows, true));
|
|
measure(&format!("lookup {rows}"), lookup(rows));
|
|
}
|
|
}
|