From 70dea3d93c31d11599184a7ec716e6d1c80c6a93 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Tue, 19 May 2026 10:31:28 +1000 Subject: [PATCH 1/3] chore(pact_matching): Add criterion benchmarks and flamegraph profiling target for V2 engine - Replace iai stub in benches/v2_engine.rs with criterion benchmarks covering four scenarios (text body, JSON equality, JSON with matching rules, headers+query), each split into build / execute / build+execute phases - Add examples/profile_engine.rs as a standalone flamegraph target - Add [profile.profiling] to workspace Cargo.toml (release speed + debug symbols) Co-Authored-By: Claude Sonnet 4.6 --- rust/Cargo.lock | 183 ++++++++++++- rust/Cargo.toml | 6 + rust/pact_matching/Cargo.toml | 5 + rust/pact_matching/benches/v2_engine.rs | 258 ++++++++++++++++-- rust/pact_matching/examples/profile_engine.rs | 165 +++++++++++ 5 files changed, 585 insertions(+), 32 deletions(-) create mode 100644 rust/pact_matching/examples/profile_engine.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5c8fa54e..0e789bb4 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -57,6 +57,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "ansi_term" version = "0.12.1" @@ -470,6 +476,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.61" @@ -529,6 +541,33 @@ dependencies = [ "phf", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -706,6 +745,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -731,6 +806,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -1269,6 +1350,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hamcrest2" version = "0.3.0" @@ -1318,6 +1410,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1669,7 +1767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f85dac6c239acc85fd61934c572292d93adfd2de459d9c032aa22b553506e915" dependencies = [ "either", - "itertools", + "itertools 0.14.0", "proc-macro2 1.0.106", "quote 1.0.45", "strum", @@ -1736,6 +1834,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1751,6 +1860,15 @@ dependencies = [ "regex", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2551,6 +2669,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -2598,7 +2722,7 @@ dependencies = [ "futures-util", "home", "indicatif", - "itertools", + "itertools 0.14.0", "lazy_static", "log", "maplit", @@ -2634,7 +2758,7 @@ dependencies = [ "bytes", "env_logger", "expectest", - "itertools", + "itertools 0.14.0", "maplit", "pact-plugin-driver", "pact_matching", @@ -2670,7 +2794,7 @@ dependencies = [ "expectest", "futures", "hamcrest2", - "itertools", + "itertools 0.14.0", "lazy_static", "libc", "log", @@ -2717,6 +2841,7 @@ dependencies = [ "base64", "bytes", "chrono", + "criterion", "difference", "env_logger", "expectest", @@ -2726,7 +2851,7 @@ dependencies = [ "hex", "http", "infer", - "itertools", + "itertools 0.14.0", "kiss_xml", "lazy_static", "lenient_semver", @@ -2772,7 +2897,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "itertools", + "itertools 0.14.0", "lazy_static", "maplit", "pact-plugin-driver", @@ -2809,7 +2934,7 @@ dependencies = [ "hashers", "hex", "indextree", - "itertools", + "itertools 0.14.0", "kiss_xml", "lazy_static", "lenient_semver", @@ -2857,7 +2982,7 @@ dependencies = [ "http", "humantime", "insta", - "itertools", + "itertools 0.14.0", "lazy_static", "maplit", "mime", @@ -3083,6 +3208,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -3214,7 +3367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -3234,7 +3387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2 1.0.106", "quote 1.0.45", "syn 2.0.117", @@ -4588,6 +4741,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 464076dc..a3b3538c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -17,3 +17,9 @@ pact_matching = { version = "~2.0.3", path = "./pact_matching" } strip = true opt-level = "z" codegen-units = 1 + +# Release-speed optimisations with debug symbols retained — for flamegraph/perf. +[profile.profiling] +inherits = "release" +strip = false +debug = true diff --git a/rust/pact_matching/Cargo.toml b/rust/pact_matching/Cargo.toml index 73dba744..62768f17 100644 --- a/rust/pact_matching/Cargo.toml +++ b/rust/pact_matching/Cargo.toml @@ -75,3 +75,8 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter", "tracing-lo ntest = "0.9.5" pretty_assertions = "1.4.1" rstest = "0.26.1" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "v2_engine" +harness = false diff --git a/rust/pact_matching/benches/v2_engine.rs b/rust/pact_matching/benches/v2_engine.rs index 60acb284..b110a1e0 100644 --- a/rust/pact_matching/benches/v2_engine.rs +++ b/rust/pact_matching/benches/v2_engine.rs @@ -1,28 +1,242 @@ -use iai::{black_box, main}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; use pact_matching::engine::{build_request_plan, execute_request_plan}; use pact_matching::engine::context::PlanMatchingContext; use pact_models::bodies::OptionalBody; use pact_models::content_types::TEXT; +use pact_models::matchingrules; +use pact_models::matchingrules::MatchingRule; use pact_models::v4::http_parts::HttpRequest; +use pact_models::v4::interaction::V4Interaction; +use pact_models::v4::synch_http::SynchronousHttp; +use serde_json::json; -fn iai_benchmark_simple() { - let request = HttpRequest { - method: "put".to_string(), - path: "/test".to_string(), - body: OptionalBody::Present("Some nice bit of text".into(), Some(TEXT.clone()), None), - .. Default::default() - }; - let expected_request = HttpRequest { - method: "POST".to_string(), - path: "/test".to_string(), - query: None, - headers: None, - body: OptionalBody::Present("Some nice bit of text".into(), Some(TEXT.clone()), None), - .. Default::default() - }; - let mut context = PlanMatchingContext::default(); - let plan = build_request_plan(&expected_request, &context).unwrap(); - let _executed_plan = execute_request_plan(&plan, &request, &mut context).unwrap(); -} - -iai::main!(iai_benchmark_simple); +// --- Scenario fixtures --- + +fn text_requests() -> (HttpRequest, HttpRequest) { + let expected = HttpRequest { + method: "POST".to_string(), + path: "/api/things".to_string(), + body: OptionalBody::Present("hello world".into(), Some(TEXT.clone()), None), + ..Default::default() + }; + let actual = expected.clone(); + (expected, actual) +} + +fn json_equality_requests() -> (HttpRequest, HttpRequest) { + let body = json!({ + "id": 42, + "name": "widget", + "price": 9.99, + "tags": ["sale", "new"], + "address": { + "street": "1 Main St", + "city": "Springfield", + "zip": "12345" + } + }); + let expected = HttpRequest { + method: "POST".to_string(), + path: "/api/orders".to_string(), + body: OptionalBody::from(&body), + ..Default::default() + }; + let actual = expected.clone(); + (expected, actual) +} + +fn json_rules_requests() -> (HttpRequest, HttpRequest, PlanMatchingContext) { + let matching_rules = matchingrules! { + "body" => { + "$.id" => [ MatchingRule::Integer ], + "$.name" => [ MatchingRule::Type ], + "$.price" => [ MatchingRule::Decimal ], + "$.zip" => [ MatchingRule::Regex("[0-9]{5}".to_string()) ] + } + }; + let expected = HttpRequest { + method: "POST".to_string(), + path: "/api/orders".to_string(), + body: OptionalBody::from(&json!({ + "id": 1, + "name": "example", + "price": 1.0, + "zip": "00000" + })), + matching_rules: matching_rules.clone(), + ..Default::default() + }; + let actual = HttpRequest { + method: "POST".to_string(), + path: "/api/orders".to_string(), + body: OptionalBody::from(&json!({ + "id": 99, + "name": "sprocket", + "price": 14.50, + "zip": "90210" + })), + ..Default::default() + }; + let context = PlanMatchingContext { + interaction: SynchronousHttp { + request: expected.clone(), + ..SynchronousHttp::default() + }.boxed_v4(), + ..PlanMatchingContext::default() + }; + (expected, actual, context) +} + +fn headers_query_requests() -> (HttpRequest, HttpRequest) { + let expected = HttpRequest { + method: "GET".to_string(), + path: "/api/search".to_string(), + query: Some(vec![ + ("q".to_string(), vec![Some("rust".to_string())]), + ("page".to_string(), vec![Some("1".to_string())]), + ("limit".to_string(), vec![Some("20".to_string())]), + ].into_iter().collect()), + headers: Some(vec![ + ("Accept".to_string(), vec!["application/json".to_string()]), + ("Authorization".to_string(), vec!["Bearer token123".to_string()]), + ("X-Request-Id".to_string(), vec!["abc-123".to_string()]), + ].into_iter().collect()), + ..Default::default() + }; + let actual = expected.clone(); + (expected, actual) +} + +// --- Benchmarks --- + +fn bench_text_body(c: &mut Criterion) { + let (expected, actual) = text_requests(); + let mut group = c.benchmark_group("text_body"); + + group.bench_function("build", |b| { + b.iter(|| { + let context = PlanMatchingContext::default(); + black_box(build_request_plan(black_box(&expected), &context).unwrap()) + }) + }); + + group.bench_function("execute", |b| { + let context = PlanMatchingContext::default(); + let plan = build_request_plan(&expected, &context).unwrap(); + b.iter(|| { + let mut ctx = context.clone(); + black_box(execute_request_plan(black_box(&plan), black_box(&actual), &mut ctx).unwrap()) + }) + }); + + group.bench_function("build+execute", |b| { + b.iter(|| { + let mut context = PlanMatchingContext::default(); + let plan = build_request_plan(black_box(&expected), &context).unwrap(); + black_box(execute_request_plan(&plan, black_box(&actual), &mut context).unwrap()) + }) + }); + + group.finish(); +} + +fn bench_json_equality(c: &mut Criterion) { + let (expected, actual) = json_equality_requests(); + let mut group = c.benchmark_group("json_equality"); + + group.bench_function("build", |b| { + b.iter(|| { + let context = PlanMatchingContext::default(); + black_box(build_request_plan(black_box(&expected), &context).unwrap()) + }) + }); + + group.bench_function("execute", |b| { + let context = PlanMatchingContext::default(); + let plan = build_request_plan(&expected, &context).unwrap(); + b.iter(|| { + let mut ctx = context.clone(); + black_box(execute_request_plan(black_box(&plan), black_box(&actual), &mut ctx).unwrap()) + }) + }); + + group.bench_function("build+execute", |b| { + b.iter(|| { + let mut context = PlanMatchingContext::default(); + let plan = build_request_plan(black_box(&expected), &context).unwrap(); + black_box(execute_request_plan(&plan, black_box(&actual), &mut context).unwrap()) + }) + }); + + group.finish(); +} + +fn bench_json_with_rules(c: &mut Criterion) { + let (expected, actual, base_context) = json_rules_requests(); + let mut group = c.benchmark_group("json_with_rules"); + + group.bench_function("build", |b| { + b.iter(|| { + let context = base_context.clone(); + black_box(build_request_plan(black_box(&expected), &context).unwrap()) + }) + }); + + group.bench_function("execute", |b| { + let plan = build_request_plan(&expected, &base_context).unwrap(); + b.iter(|| { + let mut ctx = base_context.clone(); + black_box(execute_request_plan(black_box(&plan), black_box(&actual), &mut ctx).unwrap()) + }) + }); + + group.bench_function("build+execute", |b| { + b.iter(|| { + let mut context = base_context.clone(); + let plan = build_request_plan(black_box(&expected), &context).unwrap(); + black_box(execute_request_plan(&plan, black_box(&actual), &mut context).unwrap()) + }) + }); + + group.finish(); +} + +fn bench_headers_and_query(c: &mut Criterion) { + let (expected, actual) = headers_query_requests(); + let mut group = c.benchmark_group("headers_and_query"); + + group.bench_function("build", |b| { + b.iter(|| { + let context = PlanMatchingContext::default(); + black_box(build_request_plan(black_box(&expected), &context).unwrap()) + }) + }); + + group.bench_function("execute", |b| { + let context = PlanMatchingContext::default(); + let plan = build_request_plan(&expected, &context).unwrap(); + b.iter(|| { + let mut ctx = context.clone(); + black_box(execute_request_plan(black_box(&plan), black_box(&actual), &mut ctx).unwrap()) + }) + }); + + group.bench_function("build+execute", |b| { + b.iter(|| { + let mut context = PlanMatchingContext::default(); + let plan = build_request_plan(black_box(&expected), &context).unwrap(); + black_box(execute_request_plan(&plan, black_box(&actual), &mut context).unwrap()) + }) + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_text_body, + bench_json_equality, + bench_json_with_rules, + bench_headers_and_query, +); +criterion_main!(benches); diff --git a/rust/pact_matching/examples/profile_engine.rs b/rust/pact_matching/examples/profile_engine.rs new file mode 100644 index 00000000..6734128d --- /dev/null +++ b/rust/pact_matching/examples/profile_engine.rs @@ -0,0 +1,165 @@ +// Profiling target for the V2 matching engine. +// Run with: cargo flamegraph --package pact_matching --example profile_engine +// +// Exercises four representative scenarios in a tight loop so flamegraph gets +// enough samples to show where time is actually spent: +// +// 1. Simple text body request (baseline — minimal plan) +// 2. JSON body request with equality matching +// 3. JSON body request with type + regex matching rules +// 4. Request with headers and query parameters + +use pact_matching::engine::{build_request_plan, execute_request_plan}; +use pact_matching::engine::context::PlanMatchingContext; +use pact_models::bodies::OptionalBody; +use pact_models::content_types::TEXT; +use pact_models::matchingrules; +use pact_models::v4::http_parts::HttpRequest; +use pact_models::v4::interaction::V4Interaction; +use pact_models::v4::synch_http::SynchronousHttp; +use pact_models::matchingrules::MatchingRule; +use serde_json::json; + +const ITERATIONS: usize = 10_000; + +fn scenario_text_body(expected: &HttpRequest, actual: &HttpRequest) { + let mut context = PlanMatchingContext::default(); + let plan = build_request_plan(expected, &context).unwrap(); + let _ = execute_request_plan(&plan, actual, &mut context).unwrap(); +} + +fn scenario_json_equality(expected: &HttpRequest, actual: &HttpRequest) { + let mut context = PlanMatchingContext::default(); + let plan = build_request_plan(expected, &context).unwrap(); + let _ = execute_request_plan(&plan, actual, &mut context).unwrap(); +} + +fn scenario_json_with_rules(expected: &HttpRequest, actual: &HttpRequest, context: &mut PlanMatchingContext) { + let plan = build_request_plan(expected, context).unwrap(); + let _ = execute_request_plan(&plan, actual, context).unwrap(); +} + +fn scenario_headers_and_query(expected: &HttpRequest, actual: &HttpRequest) { + let mut context = PlanMatchingContext::default(); + let plan = build_request_plan(expected, &context).unwrap(); + let _ = execute_request_plan(&plan, actual, &mut context).unwrap(); +} + +fn main() { + // --- Scenario 1: simple text body --- + let text_expected = HttpRequest { + method: "POST".to_string(), + path: "/api/things".to_string(), + body: OptionalBody::Present("hello world".into(), Some(TEXT.clone()), None), + ..Default::default() + }; + let text_actual = HttpRequest { + method: "POST".to_string(), + path: "/api/things".to_string(), + body: OptionalBody::Present("hello world".into(), Some(TEXT.clone()), None), + ..Default::default() + }; + + // --- Scenario 2: JSON equality --- + let json_expected = HttpRequest { + method: "POST".to_string(), + path: "/api/orders".to_string(), + body: OptionalBody::from(&json!({ + "id": 42, + "name": "widget", + "price": 9.99, + "tags": ["sale", "new"], + "address": { + "street": "1 Main St", + "city": "Springfield", + "zip": "12345" + } + })), + ..Default::default() + }; + let json_actual = HttpRequest { + method: "POST".to_string(), + path: "/api/orders".to_string(), + body: OptionalBody::from(&json!({ + "id": 42, + "name": "widget", + "price": 9.99, + "tags": ["sale", "new"], + "address": { + "street": "1 Main St", + "city": "Springfield", + "zip": "12345" + } + })), + ..Default::default() + }; + + // --- Scenario 3: JSON with type + regex matching rules --- + let matching_rules = matchingrules! { + "body" => { + "$.id" => [ MatchingRule::Integer ], + "$.name" => [ MatchingRule::Type ], + "$.price" => [ MatchingRule::Decimal ], + "$.zip" => [ MatchingRule::Regex("[0-9]{5}".to_string()) ] + } + }; + let json_rules_expected = HttpRequest { + method: "POST".to_string(), + path: "/api/orders".to_string(), + body: OptionalBody::from(&json!({ + "id": 1, + "name": "example", + "price": 1.0, + "zip": "00000" + })), + matching_rules: matching_rules.clone(), + ..Default::default() + }; + let json_rules_actual = HttpRequest { + method: "POST".to_string(), + path: "/api/orders".to_string(), + body: OptionalBody::from(&json!({ + "id": 99, + "name": "sprocket", + "price": 14.50, + "zip": "90210" + })), + ..Default::default() + }; + let interaction = SynchronousHttp { + request: json_rules_expected.clone(), + ..SynchronousHttp::default() + }; + let mut rules_context = PlanMatchingContext { + interaction: interaction.boxed_v4(), + ..PlanMatchingContext::default() + }; + + // --- Scenario 4: headers + query parameters --- + let headers_expected = HttpRequest { + method: "GET".to_string(), + path: "/api/search".to_string(), + query: Some(vec![ + ("q".to_string(), vec![Some("rust".to_string())]), + ("page".to_string(), vec![Some("1".to_string())]), + ("limit".to_string(), vec![Some("20".to_string())]), + ].into_iter().collect()), + headers: Some(vec![ + ("Accept".to_string(), vec!["application/json".to_string()]), + ("Authorization".to_string(), vec!["Bearer token123".to_string()]), + ("X-Request-Id".to_string(), vec!["abc-123".to_string()]), + ].into_iter().collect()), + ..Default::default() + }; + let headers_actual = headers_expected.clone(); + + // Run all scenarios in a tight loop so perf/flamegraph gets enough samples. + for _ in 0..ITERATIONS { + scenario_text_body(&text_expected, &text_actual); + scenario_json_equality(&json_expected, &json_actual); + scenario_json_with_rules(&json_rules_expected, &json_rules_actual, &mut rules_context); + scenario_headers_and_query(&headers_expected, &headers_actual); + } + + println!("Done: {} iterations x 4 scenarios", ITERATIONS); +} From 5d721171bb2f4cdbbad6b96030b99a2f16722ce9 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Tue, 19 May 2026 11:13:09 +1000 Subject: [PATCH 2/3] perf(pact_matching): Eliminate redundant tree-cloning in V2 engine interpreter Change walk_tree and all execute_* methods to take ExecutionPlanNode by value instead of by reference. The interpreter now moves/transforms the plan tree into the executed tree rather than cloning it. Key changes: - walk_tree: &ExecutionPlanNode -> ExecutionPlanNode; CONTAINER and PIPELINE branches use children.into_iter() instead of VecDeque::from(children.clone()), and push(child_result) instead of push(child_result.clone()) - execute_action and all ~25 execute_* methods: owned node, destructure at top - validate_one/two/three_args and validate_args: take Vec directly, use into_iter() to move children to walk_tree - evaluate_children: takes children: Vec by value - execute_plan: clones plan_root once at entry (public API unchanged) Benchmark results (execute phase): text body: -30% JSON equality: -35% JSON with rules: -33% Co-Authored-By: Claude Sonnet 4.6 --- rust/pact_matching/src/engine/interpreter.rs | 796 ++++++++++-------- .../src/engine/tests/walk_tree_tests.rs | 44 +- 2 files changed, 471 insertions(+), 369 deletions(-) diff --git a/rust/pact_matching/src/engine/interpreter.rs b/rust/pact_matching/src/engine/interpreter.rs index b5810c39..4c47c2ba 100644 --- a/rust/pact_matching/src/engine/interpreter.rs +++ b/rust/pact_matching/src/engine/interpreter.rs @@ -59,7 +59,7 @@ impl ExecutionPlanInterpreter { ) -> anyhow::Result { let path = vec![]; let start = Instant::now(); - let executed_tree = self.walk_tree(&path, &plan.plan_root, value_resolver)?; + let executed_tree = self.walk_tree(&path, plan.plan_root.clone(), value_resolver)?; Ok(ExecutionPlan { plan_root: executed_tree, execution_time: Some(start.elapsed()) @@ -70,51 +70,55 @@ impl ExecutionPlanInterpreter { pub fn walk_tree( &mut self, path: &[String], - node: &ExecutionPlanNode, + node: ExecutionPlanNode, value_resolver: &dyn ValueResolver ) -> anyhow::Result { - match &node.node_type { + let ExecutionPlanNode { node_type, children, .. } = node; + match node_type { PlanNodeType::EMPTY => { trace!(?path, "walk_tree ==> Empty node"); - Ok(node.clone()) + Ok(ExecutionPlanNode { node_type: PlanNodeType::EMPTY, result: None, children }) }, - PlanNodeType::CONTAINER(label) => { - trace!(?path, %label, "walk_tree ==> Container node"); + PlanNodeType::CONTAINER(ref label) => { + trace!(?path, label, "walk_tree ==> Container node"); - let mut result = vec![]; + let mut result_children = vec![]; let mut child_path = path.to_vec(); child_path.push(label.clone()); let mut status = NodeResult::OK; - let mut loop_items = VecDeque::from(node.children.clone()); - - while !loop_items.is_empty() { - let child = loop_items.pop_front().unwrap(); - let child_result = self.walk_tree(&child_path, &child, value_resolver)?; - status = status.and(&child_result.result.clone().unwrap_or_default()); - result.push(child_result.clone()); - if child_result.is_splat() { - for item in child_result.children.iter().rev() { + let mut loop_items: VecDeque = children.into_iter().collect(); + + while let Some(child) = loop_items.pop_front() { + let child_result = self.walk_tree(&child_path, child, value_resolver)?; + status = status.and(child_result.result.as_ref().unwrap_or(&NodeResult::OK)); + let is_splat = child_result.is_splat(); + result_children.push(child_result); + if is_splat { + let last = result_children.last().unwrap(); + for item in last.children.iter().rev() { loop_items.push_front(item.clone()); } } } Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(status.truthy()), - children: result + children: result_children }) } - PlanNodeType::ACTION(action) => { - trace!(?path, %action, "walk_tree ==> Action node"); - Ok(self.execute_action(action.as_str(), value_resolver, node, path)) + PlanNodeType::ACTION(ref action) => { + trace!(?path, action, "walk_tree ==> Action node"); + let action_str = action.clone(); + let node = ExecutionPlanNode { node_type, children, result: None }; + Ok(self.execute_action(&action_str, value_resolver, node, path)) } - PlanNodeType::VALUE(val) => { + PlanNodeType::VALUE(ref val) => { trace!(?path, ?val, "walk_tree ==> Value node"); let value = match val { NodeValue::NAMESPACED(namespace, value) => match namespace.as_str() { "json" => serde_json::from_str(value.as_str()) - .map(|v| NodeValue::JSON(v)) + .map(NodeValue::JSON) .map_err(|err| anyhow!(err)), #[cfg(feature = "xml")] "xml" => kiss_xml::parse_str(value) @@ -125,25 +129,25 @@ impl ExecutionPlanInterpreter { _ => Ok(val.clone()) }?; Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(value)), children: vec![] }) } - PlanNodeType::RESOLVE(resolve_path) => { + PlanNodeType::RESOLVE(ref resolve_path) => { trace!(?path, %resolve_path, "walk_tree ==> Resolve node"); match value_resolver.resolve(resolve_path, &self.context) { Ok(val) => { Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), - result: Some(NodeResult::VALUE(val.clone())), + node_type, + result: Some(NodeResult::VALUE(val)), children: vec![] }) } Err(err) => { trace!(?path, %resolve_path, %err, "Resolve node failed"); Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![] }) @@ -156,54 +160,50 @@ impl ExecutionPlanInterpreter { let child_path = path.to_vec(); self.push_result(None); let mut child_results = vec![]; - let mut loop_items = VecDeque::from(node.children.clone()); + let mut loop_items: VecDeque = children.into_iter().collect(); // TODO: Need a short circuit here if any child results in an error - while !loop_items.is_empty() { - let child = loop_items.pop_front().unwrap(); - let child_result = self.walk_tree(&child_path, &child, value_resolver)?; + while let Some(child) = loop_items.pop_front() { + let child_result = self.walk_tree(&child_path, child, value_resolver)?; self.update_result(child_result.result.clone()); - child_results.push(child_result.clone()); - if child_result.is_splat() { - for item in child_result.children.iter().rev() { + let is_splat = child_result.is_splat(); + child_results.push(child_result); + if is_splat { + let last = child_results.last().unwrap(); + for item in last.children.iter().rev() { loop_items.push_front(item.clone()); } } } let result = self.pop_result(); - match result { - Some(value) => { - Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), - result: Some(value), - children: child_results - }) - } + let result = match result { + Some(value) => value, None => { trace!(?path, "Value from stack is empty"); - Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), - result: Some(NodeResult::ERROR("Value from stack is empty".to_string())), - children: child_results - }) + NodeResult::ERROR("Value from stack is empty".to_string()) } - } + }; + Ok(ExecutionPlanNode { + node_type, + result: Some(result), + children: child_results + }) } - PlanNodeType::RESOLVE_CURRENT(expression) => { + PlanNodeType::RESOLVE_CURRENT(ref expression) => { trace!(?path, %expression, "walk_tree ==> Resolve current node"); match self.resolve_stack_value(expression) { Ok(val) => { Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), - result: Some(NodeResult::VALUE(val.clone())), + node_type, + result: Some(NodeResult::VALUE(val)), children: vec![] }) } Err(err) => { debug!(?path, %expression, %err, "Resolve node failed"); Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![] }) @@ -217,37 +217,39 @@ impl ExecutionPlanInterpreter { let mut child_results = vec![]; // TODO: Need a short circuit here if any child results in an error - for child in &node.children { + for child in children { let child_result = self.walk_tree(&child_path, child, value_resolver)?; match &child_result.result { - None => child_results.push(child_result.clone()), + None => child_results.push(child_result), Some(result) => match result { - NodeResult::OK => child_results.push(child_result.clone()), + NodeResult::OK => child_results.push(child_result), NodeResult::VALUE(value) => match value { NodeValue::MMAP(map) => { - for (key, value) in map { + for (key, value) in map.iter() { child_results.push(child_result.clone_with_value(NodeValue::ENTRY(key.clone(), Box::new(NodeValue::SLIST(value.clone()))))); } } NodeValue::SLIST(list) => { - for item in list { + for item in list.iter() { child_results.push(child_result.clone_with_value(NodeValue::STRING(item.clone()))); } } - _ => child_results.push(child_result.clone()) + _ => child_results.push(child_result) } - NodeResult::ERROR(_) => child_results.push(child_result.clone()) + NodeResult::ERROR(_) => child_results.push(child_result) } } } Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::OK), children: child_results }) } - PlanNodeType::ANNOTATION(_) => Ok(node.clone()) + PlanNodeType::ANNOTATION(_) => { + Ok(ExecutionPlanNode { node_type, result: None, children }) + } } } @@ -257,7 +259,7 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, path: &[String] ) -> ExecutionPlanNode { trace!(%action, "Executing action"); @@ -274,10 +276,11 @@ impl ExecutionPlanInterpreter { } else if action.starts_with("match:") { match action.strip_prefix("match:") { None => { + let ExecutionPlanNode { node_type, children, .. } = node; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("'{}' is not a valid action", action))), - children: node.children.clone() + children } } Some(matcher) => self.execute_match(action, matcher, value_resolver, node, &action_path) @@ -321,10 +324,11 @@ impl ExecutionPlanInterpreter { #[cfg(feature = "multipart")] "multipart:parse" => self.execute_multipart_parse(action, value_resolver, node, &action_path), _ => { + let ExecutionPlanNode { node_type, children, .. } = node; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("'{}' is not a valid action", action))), - children: node.children.clone() + children } } } @@ -335,16 +339,17 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_three_args(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_three_args(children, action, value_resolver, &action_path) { Ok((first_node, second_node, third_node)) => { let result1 = first_node.value().unwrap_or_default(); let expected_json_type = match result1.as_string() { None => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("'{}' is not a valid JSON type", result1))), children: vec![first_node, second_node, third_node] } @@ -355,7 +360,7 @@ impl ExecutionPlanInterpreter { let expected_keys = match result2.as_slist() { None => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("'{}' is not a list of Strings", result2))), children: vec![first_node, second_node, third_node] } @@ -368,7 +373,7 @@ impl ExecutionPlanInterpreter { let value = match result3.as_value() { None => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("Was expecting a JSON value, but got {}", result3))), children: vec![first_node, second_node, third_node] } @@ -379,7 +384,7 @@ impl ExecutionPlanInterpreter { NodeValue::JSON(json) => json, _ => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("Was expecting a JSON value, but got {:?}", value))), children: vec![first_node, second_node, third_node] } @@ -387,7 +392,7 @@ impl ExecutionPlanInterpreter { }; if let Err(err) = json_check_type(expected_json_type, json_value) { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![first_node, second_node, third_node] } @@ -401,13 +406,13 @@ impl ExecutionPlanInterpreter { let diff = &expected_keys - &actual_keys; if diff.is_empty() { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(true))), children: vec![first_node, second_node, third_node] } } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some( NodeResult::ERROR( format!("The following expected entries were missing from the actual Object: {}", @@ -420,7 +425,7 @@ impl ExecutionPlanInterpreter { } _ => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("Was expecting a JSON Object, but got {:?}", json_value))), children: vec![first_node, second_node, third_node] } @@ -429,9 +434,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -441,16 +446,17 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_three_args(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_three_args(children, action, value_resolver, &action_path) { Ok((first_node, second_node, third_node)) => { let result1 = first_node.value().unwrap_or_default(); let expected_json_type = match result1.as_string() { None => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("'{}' is not a valid JSON type", result1))), children: vec![first_node, second_node, third_node] } @@ -461,7 +467,7 @@ impl ExecutionPlanInterpreter { let expected_length = match result2.as_number() { None => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("'{}' is not a valid number", result2))), children: vec![first_node, second_node, third_node] } @@ -472,7 +478,7 @@ impl ExecutionPlanInterpreter { let value = match result3.as_value() { None => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("Was expecting a JSON value, but got {}", result3))), children: vec![first_node, second_node, third_node] } @@ -483,7 +489,7 @@ impl ExecutionPlanInterpreter { NodeValue::JSON(json) => json, _ => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("Was expecting a JSON value, but got {:?}", value))), children: vec![first_node, second_node, third_node] } @@ -491,29 +497,29 @@ impl ExecutionPlanInterpreter { }; if let Err(err) = json_check_type(expected_json_type, &json_value) { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![first_node, second_node, third_node] } } if let Err(err) = json_check_length(expected_length as usize, &json_value) { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![first_node, second_node, third_node] } } ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(true))), children: vec![first_node, second_node, third_node] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -523,17 +529,18 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec, is_empty: bool ) -> ExecutionPlanNode { - match self.validate_two_args(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_two_args(children, action, value_resolver, &action_path) { Ok((first_node, second_node)) => { let result1 = first_node.value().unwrap_or_default(); let expected_json_type = match result1.as_string() { None => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("'{}' is not a valid JSON type", result1))), children: vec![first_node, second_node] } @@ -544,7 +551,7 @@ impl ExecutionPlanInterpreter { let value = match result2.as_value() { None => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("Was expecting a JSON value, but got {}", result2))), children: vec![first_node, second_node] } @@ -556,7 +563,7 @@ impl ExecutionPlanInterpreter { NodeValue::JSON(json) => json, _ => { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(format!("Was expecting a JSON value, but got {:?}", value))), children: vec![first_node, second_node] } @@ -565,7 +572,7 @@ impl ExecutionPlanInterpreter { if let Err(err) = json_check_type(expected_json_type, &json_value) { return ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![first_node, second_node] } @@ -616,14 +623,14 @@ impl ExecutionPlanInterpreter { match result { Ok(result) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![first_node, second_node] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![first_node, second_node] } @@ -632,9 +639,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -644,10 +651,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let arg_value = value.value().unwrap_or_default().as_value(); let result = if let Some(value) = &arg_value { @@ -667,14 +675,14 @@ impl ExecutionPlanInterpreter { match result { Ok(result) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![value] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![value] } @@ -683,9 +691,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -695,10 +703,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let arg_value = value.value().unwrap_or_default().as_value(); let result = if let Some(value) = &arg_value { @@ -734,14 +743,14 @@ impl ExecutionPlanInterpreter { match result { Ok(result) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![value] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![value] } @@ -750,9 +759,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -763,10 +772,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_two_args(node, action, value_resolver, action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_two_args(children, action, value_resolver, action_path) { Ok((body_node, ct_node)) => { let body_value = body_node.value().unwrap_or_default().as_value().unwrap_or_default(); let ct_value = ct_node.value().unwrap_or_default().as_value().unwrap_or_default(); @@ -794,21 +804,21 @@ impl ExecutionPlanInterpreter { }; match result { Ok(result) => ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![body_node, ct_node] }, Err(err) => ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![body_node, ct_node] } } } Err(err) => ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -818,10 +828,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let arg_value = value.value().unwrap_or_default().as_value(); let result = if let Some(value) = &arg_value { @@ -845,21 +856,25 @@ impl ExecutionPlanInterpreter { match result { Ok(result) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![value] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![value] } } } } - Err(err) => node.clone_with_result(NodeResult::ERROR(err.to_string())) + Err(err) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + } } } @@ -868,10 +883,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let arg_value = value.value().unwrap_or_default().as_value(); let result = if let Some(value) = &arg_value { @@ -888,21 +904,25 @@ impl ExecutionPlanInterpreter { match result { Ok(result) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![value] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![value] } } } } - Err(err) => node.clone_with_result(NodeResult::ERROR(err.to_string())) + Err(err) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + } } } @@ -911,10 +931,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let arg_value = value.value().unwrap_or_default().as_value(); let result = if let Some(value) = &arg_value { @@ -937,36 +958,41 @@ impl ExecutionPlanInterpreter { match result { Ok(result) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![value] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![value] } } } } - Err(err) => node.clone_with_result(NodeResult::ERROR(err.to_string())) + Err(err) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + } } } - fn execute_apply(&mut self, node: &ExecutionPlanNode) -> ExecutionPlanNode { + fn execute_apply(&mut self, node: ExecutionPlanNode) -> ExecutionPlanNode { + let ExecutionPlanNode { node_type, children, .. } = node; if let Some(value) = self.value_stack.last() { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: value.clone(), - children: node.children.clone() + children } } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR("No value to apply (stack is empty)".to_string())), - children: node.children.clone() + children } } } @@ -974,89 +1000,87 @@ impl ExecutionPlanInterpreter { fn execute_if( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - if let Some(first_node) = node.children.first() { - match self.walk_tree(action_path.as_slice(), first_node, value_resolver) { + let ExecutionPlanNode { node_type, children, .. } = node; + if let Some(first_node) = children.first() { + match self.walk_tree(action_path.as_slice(), first_node.clone(), value_resolver) { Ok(first) => { let node_result = first.value().unwrap_or_default(); - let mut children = node.children.clone(); - children[0] = first.clone(); + let mut result_children = children.clone(); + result_children[0] = first.clone(); if !node_result.is_truthy() { - if node.children.len() > 2 { - match self.walk_tree(action_path.as_slice(), &node.children[2], value_resolver) { + if children.len() > 2 { + match self.walk_tree(action_path.as_slice(), children[2].clone(), value_resolver) { Ok(else_node) => { - children[2] = else_node.clone(); + result_children[2] = else_node.clone(); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: else_node.result.clone().map(|r| r.truthy()), - children + children: result_children } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children + children: result_children } } } } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(false))), - children + children: result_children } } - } else if let Some(second_node) = node.children.get(1) { - match self.walk_tree(action_path.as_slice(), second_node, value_resolver) { + } else if let Some(second_node) = children.get(1) { + match self.walk_tree(action_path.as_slice(), second_node.clone(), value_resolver) { Ok(second) => { let second_result = second.value().unwrap_or_default(); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(second_result.truthy()), - children: vec![first, second].iter() - .chain(node.children.iter().dropping(2)) - .cloned() + children: vec![first, second].into_iter() + .chain(children.into_iter().skip(2)) .collect() } } Err(err) => { error!("Failed to evaluate the second child - {}", err); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(false))), - children: vec![first, second_node.clone()].iter() - .chain(node.children.iter().dropping(2)) - .cloned() + children: vec![first, second_node.clone()].into_iter() + .chain(children.into_iter().skip(2)) .collect() } } } } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(node_result), - children: vec![first].iter().chain(node.children.iter().dropping(1)) - .cloned() + children: vec![first].into_iter().chain(children.into_iter().skip(1)) .collect() } } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children } } } } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR("'if' action requires at least one argument".to_string())), - children: node.children.clone() + children } } } @@ -1064,25 +1088,26 @@ impl ExecutionPlanInterpreter { fn execute_tee( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - if let Some(first_node) = node.children.first() { - match self.walk_tree(action_path.as_slice(), first_node, value_resolver) { + let ExecutionPlanNode { node_type, children, .. } = node; + if let Some(first_node) = children.first() { + match self.walk_tree(action_path.as_slice(), first_node.clone(), value_resolver) { Ok(first) => { let first_result = first.value().unwrap_or_default(); if first_result.is_err() { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(first_result.clone()), - children: once(first).chain(node.children.iter().dropping(1).cloned()).collect() + children: once(first).chain(children.into_iter().skip(1)).collect() } } else { let mut result = NodeResult::OK; self.push_result(first.result.clone()); let mut child_results = vec![first.clone()]; - for child in node.children.iter().dropping(1) { - match self.walk_tree(&action_path, &child, value_resolver) { + for child in children.iter().skip(1) { + match self.walk_tree(&action_path, child.clone(), value_resolver) { Ok(value) => { result = result.and(&value.result.clone().unwrap_or_default()); child_results.push(value.clone()); @@ -1097,16 +1122,24 @@ impl ExecutionPlanInterpreter { self.pop_result(); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result.truthy()), children: child_results } } } - Err(err) => node.clone_with_result(NodeResult::ERROR(err.to_string())) + Err(err) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children + } } } else { - node.clone_with_result(NodeResult::OK) + ExecutionPlanNode { + node_type, + result: Some(NodeResult::OK), + children + } } } @@ -1114,10 +1147,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let arg_value = value.value().unwrap_or_default().as_value(); let result = if let Some(value) = &arg_value { @@ -1133,14 +1167,14 @@ impl ExecutionPlanInterpreter { match result { Ok(result) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![value] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: vec![value] } @@ -1149,9 +1183,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -1161,15 +1195,16 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_args(1, 1, node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_args(1, 1, children, action, value_resolver, &action_path) { Ok((values, optional)) => { let first = values.first().unwrap().value().unwrap_or_default(); if let NodeResult::ERROR(err) = first { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: values.iter().chain(optional.iter()).cloned().collect() } @@ -1260,7 +1295,7 @@ impl ExecutionPlanInterpreter { match result { Ok(result) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: values.iter().chain(optional.iter()).cloned().collect() } @@ -1268,10 +1303,10 @@ impl ExecutionPlanInterpreter { Err(err) => { debug!("expect:empty failed with an error: {}", err); if optional.len() > 0 { - if let Ok(value) = self.walk_tree(action_path.as_slice(), &optional[0], value_resolver) { + if let Ok(value) = self.walk_tree(action_path.as_slice(), optional[0].clone(), value_resolver) { let message = value.value().unwrap_or_default().as_string().unwrap_or_default(); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(message)), children: values.iter().chain(once(&value)).cloned().collect() } @@ -1279,14 +1314,14 @@ impl ExecutionPlanInterpreter { // There was an error generating the optional message, so just return the // original error ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: values.iter().chain(optional.iter()).cloned().collect() } } } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: values.iter().chain(optional.iter()).cloned().collect() } @@ -1297,9 +1332,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -1310,10 +1345,11 @@ impl ExecutionPlanInterpreter { action: &str, matcher: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> Result { - match self.validate_args(4, 1, node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_args(4, 1, children, action, value_resolver, &action_path) { Ok((args, optional)) => { let first_node = &args[0]; let second_node = &args[1]; @@ -1325,7 +1361,7 @@ impl ExecutionPlanInterpreter { .value_or_error() .map_err(|err| { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type: node_type.clone(), result: Some(NodeResult::ERROR(err.to_string())), children: args.iter() .chain(optional.iter()) @@ -1339,7 +1375,7 @@ impl ExecutionPlanInterpreter { .value_or_error() .map_err(|err| { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type: node_type.clone(), result: Some(NodeResult::ERROR(err.to_string())), children: args.iter() .chain(optional.iter()) @@ -1353,7 +1389,7 @@ impl ExecutionPlanInterpreter { .value_or_error() .map_err(|err| { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type: node_type.clone(), result: Some(NodeResult::ERROR(err.to_string())), children: args.iter() .chain(optional.iter()) @@ -1369,7 +1405,7 @@ impl ExecutionPlanInterpreter { .value_or_error() .map_err(|err| { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type: node_type.clone(), result: Some(NodeResult::ERROR(err.to_string())), children: args.iter() .chain(optional.iter()) @@ -1385,7 +1421,7 @@ impl ExecutionPlanInterpreter { match rule.match_value(&exepected_value, &actual_value, false, show_types) { Ok(_) => { Ok(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(true))), children: args.iter() .chain(optional.iter()) @@ -1396,14 +1432,14 @@ impl ExecutionPlanInterpreter { Err(err) => { if let Some(error_node) = optional.first() { self.push_result(Some(NodeResult::VALUE(NodeValue::STRING(err.to_string())))); - match self.walk_tree(action_path.as_slice(), error_node, value_resolver) { + match self.walk_tree(action_path.as_slice(), error_node.clone(), value_resolver) { Ok(error_node) => { let message = match error_node.value().unwrap_or_default() { NodeResult::ERROR(e) => e, other => other.as_string().unwrap_or_default() }; Err(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(message)), children: args.iter().chain(vec![error_node].iter()).cloned().collect() }) @@ -1413,7 +1449,7 @@ impl ExecutionPlanInterpreter { // There was an error generating the optional error node, so just return the // original error Err(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: args.iter() .chain(optional.iter()) @@ -1424,7 +1460,7 @@ impl ExecutionPlanInterpreter { } } else { Err(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: args.iter() .chain(optional.iter()) @@ -1435,20 +1471,29 @@ impl ExecutionPlanInterpreter { } } } - Err(err) => Err(node.clone_with_result(NodeResult::ERROR(err.to_string()))) + Err(err) => Err(ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + }) } } - Err(err) => Err(node.clone_with_result(NodeResult::ERROR(err.to_string()))) + Err(err) => Err(ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + }) } } fn execute_match_values( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_args(4, 0, node, "match:values", value_resolver, action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_args(4, 0, children, "match:values", value_resolver, action_path) { Ok((args, _)) => { let expected = args[0].value().unwrap_or_default().as_value(); let actual = args[1].value().unwrap_or_default().as_value(); @@ -1460,7 +1505,7 @@ impl ExecutionPlanInterpreter { _ => false }; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(if ok { NodeResult::VALUE(NodeValue::BOOL(true)) } else { @@ -1469,17 +1514,22 @@ impl ExecutionPlanInterpreter { children: args } } - Err(err) => node.clone_with_result(NodeResult::ERROR(err.to_string())) + Err(err) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + } } } fn execute_match_each_key( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_args(4, 0, node, "match:each-key", value_resolver, action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_args(4, 0, children, "match:each-key", value_resolver, action_path) { Ok((args, _)) => { let actual_result = args[1].value().unwrap_or_default(); let matcher_params = args[2].value().unwrap_or_default() @@ -1513,29 +1563,41 @@ impl ExecutionPlanInterpreter { } } ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(!has_error))), children: child_results } } - _ => node.clone_with_result(NodeResult::VALUE(NodeValue::BOOL(true))) + _ => ExecutionPlanNode { + node_type, + result: Some(NodeResult::VALUE(NodeValue::BOOL(true))), + children: args + } } } - Ok(_) | Err(_) => node.clone_with_result(NodeResult::ERROR( - "match:each-key requires an each-key matching rule".to_string())) + Ok(_) | Err(_) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR("match:each-key requires an each-key matching rule".to_string())), + children: args + } } } - Err(err) => node.clone_with_result(NodeResult::ERROR(err.to_string())) + Err(err) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + } } } fn execute_match_each_value( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_args(4, 0, node, "match:each-value", value_resolver, action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_args(4, 0, children, "match:each-value", value_resolver, action_path) { Ok((args, _)) => { let actual_result = args[1].value().unwrap_or_default(); let matcher_params = args[2].value().unwrap_or_default() @@ -1569,7 +1631,7 @@ impl ExecutionPlanInterpreter { } } ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(!has_error))), children: child_results } @@ -1597,7 +1659,7 @@ impl ExecutionPlanInterpreter { } } ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(!has_error))), children: child_results } @@ -1625,19 +1687,30 @@ impl ExecutionPlanInterpreter { } } ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::BOOL(!has_error))), children: child_results } } - _ => node.clone_with_result(NodeResult::VALUE(NodeValue::BOOL(true))) + _ => ExecutionPlanNode { + node_type, + result: Some(NodeResult::VALUE(NodeValue::BOOL(true))), + children: args + } } } - Ok(_) | Err(_) => node.clone_with_result(NodeResult::ERROR( - "match:each-value requires an each-value matching rule".to_string())) + Ok(_) | Err(_) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR("match:each-value requires an each-value matching rule".to_string())), + children: args + } } } - Err(err) => node.clone_with_result(NodeResult::ERROR(err.to_string())) + Err(err) => ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + } } } @@ -1645,11 +1718,12 @@ impl ExecutionPlanInterpreter { &mut self, _action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec, upper_case: bool ) -> ExecutionPlanNode { - let (children, values) = match self.evaluate_children(value_resolver, node, action_path, true) { + let ExecutionPlanNode { node_type, children, .. } = node; + let (result_children, values) = match self.evaluate_children(value_resolver, &node_type, children, action_path, true) { Ok(value) => value, Err(value) => return value }; @@ -1686,9 +1760,9 @@ impl ExecutionPlanInterpreter { NodeValue::LIST(results) }; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(result)), - children + children: result_children } } @@ -1696,10 +1770,11 @@ impl ExecutionPlanInterpreter { &mut self, _action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - let (children, values) = match self.evaluate_children(value_resolver, node, action_path, true) { + let ExecutionPlanNode { node_type, children, .. } = node; + let (result_children, values) = match self.evaluate_children(value_resolver, &node_type, children, action_path, true) { Ok(value) => value, Err(value) => return value }; @@ -1731,9 +1806,9 @@ impl ExecutionPlanInterpreter { NodeValue::LIST(results) }; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(result)), - children + children: result_children } } @@ -1741,10 +1816,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let result = value.value() .unwrap_or_default() @@ -1772,16 +1848,16 @@ impl ExecutionPlanInterpreter { _ => NodeResult::ERROR(format!("'length' can't be used with a {:?} node", value)) }; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![ value.clone() ] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -1791,10 +1867,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let result = if let NodeResult::VALUE(value) = value.value().unwrap_or_default() { match value { @@ -1805,16 +1882,16 @@ impl ExecutionPlanInterpreter { NodeResult::VALUE(NodeValue::BOOL(false)) }; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result), children: vec![value] } } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -1846,50 +1923,55 @@ impl ExecutionPlanInterpreter { fn validate_one_arg( &mut self, - node: &ExecutionPlanNode, + children: Vec, action: &str, value_resolver: &dyn ValueResolver, path: &Vec ) -> anyhow::Result { - if node.children.len() > 1 { - Err(anyhow!("{} takes only one argument, got {}", action, node.children.len())) - } else if let Some(argument) = node.children.first() { - self.walk_tree(path.as_slice(), argument, value_resolver) + if children.len() > 1 { + Err(anyhow!("{} takes only one argument, got {}", action, children.len())) } else { - Err(anyhow!("{} requires one argument, got none", action)) + let mut iter = children.into_iter(); + if let Some(argument) = iter.next() { + self.walk_tree(path.as_slice(), argument, value_resolver) + } else { + Err(anyhow!("{} requires one argument, got none", action)) + } } } fn validate_two_args( &mut self, - node: &ExecutionPlanNode, + children: Vec, action: &str, value_resolver: &dyn ValueResolver, path: &Vec ) -> anyhow::Result<(ExecutionPlanNode, ExecutionPlanNode)> { - if node.children.len() == 2 { - let first = self.walk_tree(path.as_slice(), &node.children[0], value_resolver)?; - let second = self.walk_tree(path.as_slice(), &node.children[1], value_resolver)?; + if children.len() == 2 { + let mut iter = children.into_iter(); + let first = self.walk_tree(path.as_slice(), iter.next().unwrap(), value_resolver)?; + let second = self.walk_tree(path.as_slice(), iter.next().unwrap(), value_resolver)?; Ok((first, second)) } else { - Err(anyhow!("Action '{}' requires two arguments, got {}", action, node.children.len())) + Err(anyhow!("Action '{}' requires two arguments, got {}", action, children.len())) } } fn validate_three_args( &mut self, - node: &ExecutionPlanNode, + children: Vec, action: &str, value_resolver: &dyn ValueResolver, path: &Vec ) -> anyhow::Result<(ExecutionPlanNode, ExecutionPlanNode, ExecutionPlanNode)> { - if node.children.len() == 3 { - let first = self.walk_tree(path.as_slice(), &node.children[0], value_resolver)?; - let second = self.walk_tree(path.as_slice(), &node.children[1], value_resolver)?; - let third = self.walk_tree(path.as_slice(), &node.children[2], value_resolver)?; + if children.len() == 3 { + let mut iter = children.into_iter(); + let first = self.walk_tree(path.as_slice(), iter.next().unwrap(), value_resolver)?; + let second = self.walk_tree(path.as_slice(), iter.next().unwrap(), value_resolver)?; + let third = self.walk_tree(path.as_slice(), iter.next().unwrap(), value_resolver)?; Ok((first, second, third)) } else { - Err(anyhow!("Action '{}' requires three arguments, got {}", action, node.children.len())) + Err(anyhow!("Action '{}' requires three arguments, got {}", action, children.len())) } } @@ -1897,22 +1979,24 @@ impl ExecutionPlanInterpreter { &mut self, required: usize, optional: usize, - node: &ExecutionPlanNode, + children: Vec, action: &str, value_resolver: &dyn ValueResolver, path: &Vec ) -> anyhow::Result<(Vec, Vec)> { - if node.children.len() < required { - Err(anyhow!("{} requires {} arguments, got {}", action, required, node.children.len())) - } else if node.children.len() > required + optional { - Err(anyhow!("{} supports at most {} arguments, got {}", action, required + optional, node.children.len())) + let count = children.len(); + if count < required { + Err(anyhow!("{} requires {} arguments, got {}", action, required, count)) + } else if count > required + optional { + Err(anyhow!("{} supports at most {} arguments, got {}", action, required + optional, count)) } else { + let mut iter = children.into_iter(); let mut required_args = vec![]; - for child in node.children.iter().take(required) { + for child in iter.by_ref().take(required) { let value = self.walk_tree(path.as_slice(), child, value_resolver)?; required_args.push(value); } - Ok((required_args, node.children.iter().dropping(required).cloned().collect())) + Ok((required_args, iter.collect())) } } @@ -1920,10 +2004,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, path: &Vec ) -> ExecutionPlanNode { - let (children, str_values) = match self.evaluate_children(value_resolver, node, path, true) { + let ExecutionPlanNode { node_type, children, .. } = node; + let (result_children, str_values) = match self.evaluate_children(value_resolver, &node_type, children, path, true) { Ok((children, values)) => { (children, values.iter().flat_map(|v| { let v = v.as_value().unwrap_or_default(); @@ -1951,9 +2036,9 @@ impl ExecutionPlanInterpreter { }; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::STRING(result))), - children + children: result_children } } @@ -1961,10 +2046,11 @@ impl ExecutionPlanInterpreter { &mut self, _action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, path: &Vec ) -> ExecutionPlanNode { - let (children, str_values) = match self.evaluate_children(value_resolver, node, path, true) { + let ExecutionPlanNode { node_type, children, .. } = node; + let (result_children, str_values) = match self.evaluate_children(value_resolver, &node_type, children, path, true) { Ok((children, values)) => { (children, values.iter().flat_map(|v| { let v = v.as_value().unwrap_or_default(); @@ -1986,54 +2072,59 @@ impl ExecutionPlanInterpreter { let result = str_values.iter().join(""); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(result)), - children + children: result_children } } fn evaluate_children( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node_type: &PlanNodeType, + children: Vec, path: &Vec, short_circuit: bool ) -> Result<(Vec, Vec), ExecutionPlanNode> { - let mut children = vec![]; + let mut result_children = vec![]; let mut values = vec![]; - let mut loop_items = VecDeque::from(node.children.clone()); + let mut loop_items: VecDeque = children.into_iter().collect(); - while !loop_items.is_empty() { - let child = loop_items.pop_front().unwrap(); + while let Some(child) = loop_items.pop_front() { let value = if let Some(child_value) = child.value() { child_value } else { - match &self.walk_tree(path.as_slice(), &child, value_resolver) { + match self.walk_tree(path.as_slice(), child, value_resolver) { Ok(value) => { - if let Some(NodeResult::ERROR(_)) = &value.result && short_circuit { - children.push(value.clone()); - children.extend(loop_items); - return Err(ExecutionPlanNode { - node_type: node.node_type.clone(), - result: value.result.clone(), - children: children.clone() - }) - } else if value.is_splat() { - children.push(value.clone()); + if let Some(NodeResult::ERROR(_)) = &value.result { + if short_circuit { + result_children.push(value.clone()); + result_children.extend(loop_items); + return Err(ExecutionPlanNode { + node_type: node_type.clone(), + result: value.result.clone(), + children: result_children.clone() + }); + } + } + if value.is_splat() { for splat_child in value.children.iter().rev() { loop_items.push_front(splat_child.clone()); } - NodeResult::OK + let v = value.value().unwrap_or_default(); + result_children.push(value); + v } else { - children.push(value.clone()); - value.value().unwrap_or_default() + let v = value.value().unwrap_or_default(); + result_children.push(value); + v } }, Err(err) => { return Err(ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type: node_type.clone(), result: Some(NodeResult::ERROR(err.to_string())), - children: children.clone() + children: result_children.clone() }) } } @@ -2041,17 +2132,18 @@ impl ExecutionPlanInterpreter { values.push(value); } - Ok((children, values)) + Ok((result_children, values)) } fn execute_check_entries( &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_args(2, 1, node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_args(2, 1, children, action, value_resolver, &action_path) { Ok((values, optional)) => { let first = values[0].value() .unwrap_or_default() @@ -2120,7 +2212,7 @@ impl ExecutionPlanInterpreter { match result { Ok(_) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::OK), children: values.iter().chain(optional.iter()).cloned().collect() } @@ -2130,10 +2222,10 @@ impl ExecutionPlanInterpreter { if optional.len() > 0 { if let Some(diff) = diff { self.push_result(Some(NodeResult::VALUE(NodeValue::SLIST(diff.iter().cloned().collect())))); - let result = if let Ok(value) = self.walk_tree(action_path.as_slice(), &optional[0], value_resolver) { + let result = if let Ok(value) = self.walk_tree(action_path.as_slice(), optional[0].clone(), value_resolver) { let message = value.value().unwrap_or_default().as_string().unwrap_or_default(); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(message)), children: values.iter().chain(once(&value)).cloned().collect() } @@ -2141,7 +2233,7 @@ impl ExecutionPlanInterpreter { // There was an error generating the optional message, so just return the // original error ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: values.iter().chain(optional.iter()).cloned().collect() } @@ -2150,14 +2242,14 @@ impl ExecutionPlanInterpreter { result } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: values.iter().chain(optional.iter()).cloned().collect() } } } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: values.iter().chain(optional.iter()).cloned().collect() } @@ -2167,9 +2259,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -2179,10 +2271,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_args(2, 1, node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_args(2, 1, children, action, value_resolver, &action_path) { Ok((values, optional)) => { let expected_length = values[0].value() .unwrap_or_default() @@ -2257,7 +2350,7 @@ impl ExecutionPlanInterpreter { match result { Ok(_) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::OK), children: values.iter().chain(optional.iter()).cloned().collect() } @@ -2265,10 +2358,10 @@ impl ExecutionPlanInterpreter { Err(err) => { debug!("expect:count failed with an error: {}", err); if optional.len() > 0 { - if let Ok(value) = self.walk_tree(action_path.as_slice(), &optional[0], value_resolver) { + if let Ok(value) = self.walk_tree(action_path.as_slice(), optional[0].clone(), value_resolver) { let message = value.value().unwrap_or_default().as_string().unwrap_or_default(); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(message)), children: values.iter().chain(once(&value)).cloned().collect() } @@ -2276,14 +2369,14 @@ impl ExecutionPlanInterpreter { // There was an error generating the optional message, so just return the // original error ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: values.iter().chain(optional.iter()).cloned().collect() } } } else { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), children: values.iter().chain(optional.iter()).cloned().collect() } @@ -2293,9 +2386,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -2304,17 +2397,18 @@ impl ExecutionPlanInterpreter { fn execute_and( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, path: &Vec ) -> ExecutionPlanNode { - match self.evaluate_children(value_resolver, node, path, true) { - Ok((children, values)) => { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.evaluate_children(value_resolver, &node_type, children, path, true) { + Ok((result_children, values)) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(values.iter().fold(NodeResult::OK, |result, value| { result.and(value) })), - children + children: result_children } } Err(err) => err @@ -2324,17 +2418,18 @@ impl ExecutionPlanInterpreter { fn execute_or( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, path: &Vec ) -> ExecutionPlanNode { - match self.evaluate_children(value_resolver, node, path, false) { - Ok((children, values)) => { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.evaluate_children(value_resolver, &node_type, children, path, false) { + Ok((result_children, values)) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(values.iter().fold(NodeResult::OK, |result, value| { result.or(value) })), - children + children: result_children } } Err(err) => err @@ -2373,10 +2468,11 @@ impl ExecutionPlanInterpreter { &mut self, action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_one_arg(node, action, value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_one_arg(children, action, value_resolver, &action_path) { Ok(value) => { let arg_value = value.value() .unwrap_or_default() @@ -2389,7 +2485,7 @@ impl ExecutionPlanInterpreter { let parameter_map = parse_charset_parameters(header_params); ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(NodeValue::JSON(json!({ "value": header_value, "parameters": parameter_map @@ -2399,9 +2495,9 @@ impl ExecutionPlanInterpreter { } Err(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err.to_string())), - children: node.children.clone() + children: vec![] } } } @@ -2411,10 +2507,11 @@ impl ExecutionPlanInterpreter { &mut self, _action: &str, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - let (children, values) = match self.evaluate_children(value_resolver, node, action_path, true) { + let ExecutionPlanNode { node_type, children, .. } = node; + let (result_children, values) = match self.evaluate_children(value_resolver, &node_type, children, action_path, true) { Ok(value) => value, Err(value) => return value }; @@ -2441,19 +2538,20 @@ impl ExecutionPlanInterpreter { }; ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::VALUE(result)), - children + children: result_children } } fn execute_for_each( &mut self, value_resolver: &dyn ValueResolver, - node: &ExecutionPlanNode, + node: ExecutionPlanNode, action_path: &Vec ) -> ExecutionPlanNode { - match self.validate_args(2, 1, node, "for-each", value_resolver, &action_path) { + let ExecutionPlanNode { node_type, children, .. } = node; + match self.validate_args(2, 1, children, "for-each", value_resolver, &action_path) { Ok((values, optional)) => { let marker_result = &values[0]; let loop_items = &values[1]; @@ -2464,11 +2562,11 @@ impl ExecutionPlanInterpreter { match loop_items.value().unwrap_or_default() { NodeResult::ERROR(err) => { ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(NodeResult::ERROR(err)), children: [marker_result.clone(), loop_items.clone()] .iter() - .chain(node.children.iter().dropping(2)) + .chain(optional.iter()) .cloned() .collect() } @@ -2478,15 +2576,15 @@ impl ExecutionPlanInterpreter { let mut child_results = vec![marker_result.clone(), loop_items.clone()]; if let Some(template) = optional.first() { - let loop_items = loop_items + let loop_items_list = loop_items .value() .unwrap_or_default() .as_value() .unwrap_or_default() .to_list(); - for (index, _) in loop_items.iter().enumerate() { + for (index, _) in loop_items_list.iter().enumerate() { let updated_child = inject_index(template, marker.as_str(), index); - match self.walk_tree(&action_path, &updated_child, value_resolver) { + match self.walk_tree(&action_path, updated_child.clone(), value_resolver) { Ok(value) => { result = result.and(&value.result.clone().unwrap_or_default()); child_results.push(value.clone()); @@ -2501,7 +2599,7 @@ impl ExecutionPlanInterpreter { } ExecutionPlanNode { - node_type: node.node_type.clone(), + node_type, result: Some(result.truthy()), children: child_results } @@ -2509,7 +2607,11 @@ impl ExecutionPlanInterpreter { } } Err(err) => { - node.clone_with_result(NodeResult::ERROR(err.to_string())) + ExecutionPlanNode { + node_type, + result: Some(NodeResult::ERROR(err.to_string())), + children: vec![] + } } } } diff --git a/rust/pact_matching/src/engine/tests/walk_tree_tests.rs b/rust/pact_matching/src/engine/tests/walk_tree_tests.rs index bdf0126f..e0d21609 100644 --- a/rust/pact_matching/src/engine/tests/walk_tree_tests.rs +++ b/rust/pact_matching/src/engine/tests/walk_tree_tests.rs @@ -35,7 +35,7 @@ fn json_with_null() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -57,7 +57,7 @@ fn json_with_null() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -79,7 +79,7 @@ fn json_with_null() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -109,7 +109,7 @@ fn json_with_boolean() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -131,7 +131,7 @@ fn json_with_boolean() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -161,7 +161,7 @@ fn json_with_empty_array() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -181,7 +181,7 @@ fn json_with_empty_array() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -201,7 +201,7 @@ fn json_with_empty_array() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -229,7 +229,7 @@ fn json_with_array() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -298,7 +298,7 @@ fn json_with_array() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -367,7 +367,7 @@ fn json_with_array() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -436,7 +436,7 @@ fn json_with_array() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -513,7 +513,7 @@ fn very_simple_xml() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -552,7 +552,7 @@ fn very_simple_xml() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -591,7 +591,7 @@ fn very_simple_xml() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(" %tee ( @@ -646,7 +646,7 @@ fn simple_xml() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(r#" %tee ( @@ -914,7 +914,7 @@ fn simple_xml() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(r#" %tee ( @@ -1183,7 +1183,7 @@ fn missing_xml_value() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(r#" %tee ( @@ -1276,7 +1276,7 @@ fn missing_xml_value() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(r#" %tee ( @@ -1369,7 +1369,7 @@ fn missing_xml_value() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(r#" %tee ( @@ -1471,7 +1471,7 @@ fn invalid_xml() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(r#" %tee ( @@ -1573,7 +1573,7 @@ fn unexpected_xml_value() { bytes: content.to_vec() }; let mut interpreter = ExecutionPlanInterpreter::new_with_context(&context); - let result = interpreter.walk_tree(&path, &node, &resolver).unwrap(); + let result = interpreter.walk_tree(&path, node.clone(), &resolver).unwrap(); let mut buffer = String::new(); result.pretty_form(&mut buffer, 2); assert_eq!(r#" %tee ( From fb640a1e3f6ab0f9fbfd3c24465ae9525307bac1 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Tue, 19 May 2026 11:58:19 +1000 Subject: [PATCH 3/3] docs(pact_matching): Add performance optimisation guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the flamegraph → benchmark → fix → repeat workflow used to optimise the V2 engine, including tool setup, how to read flamegraphs, criterion baseline management, and when to stop optimising. Co-Authored-By: Claude Sonnet 4.6 --- rust/pact_matching/PERFORMANCE.md | 162 ++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 rust/pact_matching/PERFORMANCE.md diff --git a/rust/pact_matching/PERFORMANCE.md b/rust/pact_matching/PERFORMANCE.md new file mode 100644 index 00000000..3dbbb07a --- /dev/null +++ b/rust/pact_matching/PERFORMANCE.md @@ -0,0 +1,162 @@ +# Performance Optimisation Guide + +This document describes the process used to profile and optimise the V2 matching +engine, and can be followed for future performance work. + +## Tools + +| Tool | Purpose | +|---|---| +| `cargo flamegraph` | CPU profiling — identifies *where* time is spent | +| `criterion` | Statistical benchmarking — measures *how fast* and tracks regressions | +| `heaptrack` | Allocation profiling — identifies *what* is being allocated and from where | + +Install flamegraph if not already present: + +```bash +cargo install flamegraph +``` + +Flamegraph uses `perf` on Linux. If it complains about permissions: + +```bash +echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid +``` + +## The Workflow + +The process follows a repeated cycle: + +``` +flamegraph → identify hotspot → benchmark baseline → fix → benchmark → flamegraph → ... +``` + +**Always benchmark before making a change.** You need a "before" number to compare +against. Without it you know the code is different but not whether it's better. + +## Step 1 — Generate a Flamegraph + +The profiling build profile in `rust/Cargo.toml` gives release-speed optimisations +with debug symbols retained (required for readable stack frames): + +```toml +[profile.profiling] +inherits = "release" +strip = false +debug = true +``` + +The profiling target is `pact_matching/examples/profile_engine.rs`. It runs four +representative matching scenarios (text body, JSON equality, JSON with matching +rules, headers + query parameters) in a tight loop so `perf` collects enough samples. + +Generate the flamegraph from the `rust/` directory: + +```bash +cargo flamegraph \ + --package pact_matching \ + --example profile_engine \ + --profile profiling \ + --output flamegraph.svg +``` + +Open `flamegraph.svg` in a browser. The SVG is interactive — click any frame to +zoom in. The widest bars are the hotspots. + +### Reading the flamegraph + +- **X-axis** — time (width = percentage of CPU samples) +- **Y-axis** — call stack depth (bottom = caller, top = callee/leaf) +- **`[unknown]`** — symbol could not be resolved; usually allocator internals + (glibc/ptmalloc) or Rust stdlib code compiled without debug symbols. Look at + the frames *above* it (callees) and *below* it (callers) to understand context. +- When `[unknown]` sits below a `clone` frame, the flamegraph has hit its symbol + resolution limit. Switch to `heaptrack` for allocation-specific analysis. + +## Step 2 — Establish a Benchmark Baseline + +Benchmarks live in `pact_matching/benches/v2_engine.rs` and use +[criterion](https://github.com/bheisler/criterion.rs). + +Clear any prior criterion data first (stale baselines cause misleading comparisons): + +```bash +rm -rf rust/target/criterion +``` + +Run the full benchmark suite from the `rust/` directory: + +```bash +cargo bench --package pact_matching --bench v2_engine +``` + +Criterion saves results under `target/criterion/`. The next run after a code change +will automatically compare against this baseline and print a change percentage with +statistical confidence. + +For a quick smoke-run during development (faster, less accurate): + +```bash +cargo bench --package pact_matching --bench v2_engine \ + -- --warm-up-time 1 --measurement-time 1 --sample-size 10 +``` + +### Adding new benchmarks + +Each benchmark group covers one scenario split into three phases: + +- **`build`** — plan construction only +- **`execute`** — plan execution only (pre-built plan, measures interpreter cost) +- **`build+execute`** — end-to-end (what callers actually pay) + +Separating build and execute makes it easy to see which phase a change affects. + +## Step 3 — Identify and Fix the Hotspot + +With a flamegraph showing where time goes and a criterion baseline to measure +against, make the change and re-run the benchmarks: + +```bash +cargo bench --package pact_matching --bench v2_engine +``` + +Criterion will print lines like: + +``` +json_equality/execute time: [153.21 µs 153.63 µs 154.10 µs] + change: [-35.539% -35.374% -35.215%] (p = 0.00 < 0.05) + Performance has improved. +``` + +If the change is worthwhile, generate a new flamegraph to find the next hotspot. +Repeat the cycle. + +## Step 4 — Know When to Stop + +Benchmark numbers in isolation are not enough — consider the real-world call rate. + +The matching engine is invoked once per Pact interaction: perhaps a few dozen times +in a typical test run, up to ~100–200 for large Pact files. At that scale, even a +200 µs execution time contributes only ~40 ms total, which is negligible compared +to network I/O or mock server startup. Further micro-optimisation below that +threshold has no practical impact on test suite runtime. + +Stop when: +- The remaining hotspots are in allocator internals with no clear code-level fix +- The absolute time per call × realistic call rate is already imperceptible +- The complexity of the next fix outweighs the gain + +## History + +### 2026-05 — Eliminate redundant tree-cloning in the interpreter + +**Finding:** `cargo flamegraph` showed `malloc` and `clone` as the dominant +hotspots. The interpreter built a complete shadow tree on every execution by +cloning every node of the plan tree. + +**Fix:** Changed `walk_tree` and all `execute_*` methods to take `ExecutionPlanNode` +by value instead of by reference. Nodes are now moved/transformed into the executed +tree rather than cloned. The public API (`execute_request_plan` etc.) was preserved +by cloning `plan_root` once at the entry point. + +**Result:** 30–35% reduction in execution time across all scenarios.