From e9b46dc242be90d9557738910a95b059512227be Mon Sep 17 00:00:00 2001 From: Adam Petro Date: Thu, 5 Feb 2026 16:45:11 -0500 Subject: [PATCH] Explore removing string interning --- example_with_targets/cart.graphql | 7 +++ example_with_targets/schema.graphql | 27 ++++++++++ example_with_targets/src/main.rs | 17 +++++++ integration_tests/src/lib.rs | 33 ++++++++++++- integration_tests/tests/integration_test.rs | 55 ++++++++++++++++++--- shopify_function_macro/src/lib.rs | 5 +- 6 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 example_with_targets/cart.graphql diff --git a/example_with_targets/cart.graphql b/example_with_targets/cart.graphql new file mode 100644 index 0000000..e22f2ae --- /dev/null +++ b/example_with_targets/cart.graphql @@ -0,0 +1,7 @@ +query Input { + cartLines { + id + quantity + title + } +} diff --git a/example_with_targets/schema.graphql b/example_with_targets/schema.graphql index d2b6ce1..3ce0104 100644 --- a/example_with_targets/schema.graphql +++ b/example_with_targets/schema.graphql @@ -61,6 +61,15 @@ A void type that can be used to return a null value from a mutation. """ scalar Void +""" +A cart line representing an item in the cart. +""" +type CartLine { + id: ID! + quantity: Int! + title: String! +} + """ The input object for the function. """ @@ -77,6 +86,7 @@ type Input { optionalArray: [String!] optionalArrayOfArrays: [[String!]!] optionalArrayOfOptionalArrays: [[String!]] + cartLines: [CartLine!] @restrictTarget(only: ["test.target-cart"]) } """ @@ -102,6 +112,16 @@ type MutationRoot { """ result: FunctionTargetBResult! ): Void! + + """ + The function for API target Cart. + """ + targetCart( + """ + The result of calling the function for API target Cart. + """ + result: FunctionTargetCartResult! + ): Void! } """ @@ -119,6 +139,13 @@ input FunctionTargetBResult { operations: [Operation!]! } +""" +The result of API target Cart. +""" +input FunctionTargetCartResult { + totalQuantity: Int! +} + input Operation @oneOf { doThis: This doThat: That diff --git a/example_with_targets/src/main.rs b/example_with_targets/src/main.rs index f8e4aa4..f917271 100644 --- a/example_with_targets/src/main.rs +++ b/example_with_targets/src/main.rs @@ -15,6 +15,9 @@ mod schema { #[query("./b.graphql")] pub mod target_b {} + + #[query("./cart.graphql")] + pub mod target_cart {} } #[shopify_function] @@ -45,6 +48,20 @@ fn target_panic(_input: schema::target_a::Input) -> Result Result { + // Iterate over cart lines and sum quantities - this accesses the `quantity` property + // multiple times, which should benefit from interned string caching + let total_quantity: i32 = input + .cart_lines() + .unwrap_or(&[]) + .iter() + .map(|line| *line.quantity()) + .sum(); + + Ok(schema::FunctionTargetCartResult { total_quantity }) +} + fn main() { log!("Invoke a named export"); process::abort() diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index d3e09a8..ccc49f4 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -6,6 +6,19 @@ use std::{ sync::LazyLock, }; +/// Result from running a Shopify Function +#[derive(Debug)] +pub struct RunResult { + /// The JSON output from the function + pub output: serde_json::Value, + /// The logs from the function + pub logs: String, + /// The number of instructions executed + pub instructions: u64, + /// The memory usage in bytes + pub memory_usage: u64, +} + const FUNCTION_RUNNER_VERSION: &str = "9.1.0"; const TRAMPOLINE_VERSION: &str = "2.0.0"; @@ -152,7 +165,7 @@ pub fn run_example( path: PathBuf, export: &str, input: serde_json::Value, -) -> Result<(serde_json::Value, String)> { +) -> Result { let function_runner_path = FUNCTION_RUNNER_PATH .as_ref() .map_err(|e| anyhow::anyhow!("Failed to download function runner: {}", e))?; @@ -198,6 +211,16 @@ pub fn run_example( .ok_or_else(|| anyhow::anyhow!("Logs are not a string"))? .to_string(); + let instructions = output + .get("instructions") + .and_then(|v| v.as_u64()) + .ok_or_else(|| anyhow::anyhow!("No instructions count"))?; + + let memory_usage = output + .get("memory_usage") + .and_then(|v| v.as_u64()) + .ok_or_else(|| anyhow::anyhow!("No memory_usage"))?; + if !status.success() { anyhow::bail!( "Function runner returned non-zero exit code: {}, logs: {}", @@ -210,5 +233,11 @@ pub fn run_example( .get_mut("output") .ok_or_else(|| anyhow::anyhow!("No output"))? .take(); - Ok((output_json, logs)) + + Ok(RunResult { + output: output_json, + logs, + instructions, + memory_usage, + }) } diff --git a/integration_tests/tests/integration_test.rs b/integration_tests/tests/integration_test.rs index 875d542..f9379d9 100644 --- a/integration_tests/tests/integration_test.rs +++ b/integration_tests/tests/integration_test.rs @@ -16,14 +16,18 @@ fn test_example_with_targets_target_a() -> Result<()> { "name": "test", "country": "CA" }); - let (output, logs) = run_example(path.clone(), "target_a", input)?; + let result = run_example(path.clone(), "target_a", input)?; assert_eq!( - output, + result.output, serde_json::json!({ "status": 200 }) ); - assert_eq!(logs, "In target_a\nWith var: 42\nWith var: 42\n"); + assert_eq!(result.logs, "In target_a\nWith var: 42\nWith var: 42\n"); + eprintln!( + "target_a: instructions={}, memory_usage={}", + result.instructions, result.memory_usage + ); Ok(()) } @@ -36,9 +40,9 @@ fn test_example_with_targets_target_b() -> Result<()> { "id": "gid://shopify/Order/1234567890", "targetAResult": 200 }); - let (output, logs) = run_example(path.clone(), "target_b", input)?; + let result = run_example(path.clone(), "target_b", input)?; assert_eq!( - output, + result.output, serde_json::json!({ "name": "new name: \"gid://shopify/Order/1234567890\"", "operations": [ @@ -55,7 +59,11 @@ fn test_example_with_targets_target_b() -> Result<()> { ] }) ); - assert_eq!(logs, "In target_b\n"); + assert_eq!(result.logs, "In target_b\n"); + eprintln!( + "target_b: instructions={}, memory_usage={}", + result.instructions, result.memory_usage + ); Ok(()) } @@ -72,10 +80,43 @@ fn test_example_with_panic() -> Result<()> { .unwrap_err() .to_string(); let expected_err = - "Function runner returned non-zero exit code: exit status: 1, logs: panicked at example_with_targets/src/main.rs:45:5:\nSomething went wrong\nerror while executing at wasm backtrace:"; + "Function runner returned non-zero exit code: exit status: 1, logs: panicked at example_with_targets/src/main.rs:48:5:\nSomething went wrong\nerror while executing at wasm backtrace:"; assert!( err.contains(expected_err), "Expected error message to contain:\n`{expected_err}`\nbut was:\n`{err}`" ); Ok(()) } + +#[test] +fn test_example_with_targets_target_cart() -> Result<()> { + let path = EXAMPLE_WITH_TARGETS_RESULT + .as_ref() + .map_err(|e| anyhow::anyhow!("Failed to prepare example: {}", e))?; + // Create input with 100 cart lines to demonstrate interned string benefits + let cart_lines: Vec<_> = (0..100) + .map(|i| { + serde_json::json!({ + "id": format!("gid://shopify/CartLine/{i}"), + "quantity": i + 1, + "title": format!("Product {i}") + }) + }) + .collect(); + let input = serde_json::json!({ + "cartLines": cart_lines + }); + let result = run_example(path.clone(), "target_cart", input)?; + // Sum of 1 + 2 + ... + 100 = 5050 + assert_eq!( + result.output, + serde_json::json!({ + "totalQuantity": 5050 + }) + ); + eprintln!( + "target_cart (100 lines): instructions={}, memory_usage={}", + result.instructions, result.memory_usage + ); + Ok(()) +} diff --git a/shopify_function_macro/src/lib.rs b/shopify_function_macro/src/lib.rs index e8e440f..d959fa4 100644 --- a/shopify_function_macro/src/lib.rs +++ b/shopify_function_macro/src/lib.rs @@ -349,11 +349,8 @@ impl CodeGenerator for ShopifyFunctionCodeGenerator { parse_quote! { #description pub fn #field_name_ident(&self) -> #field_type { - static INTERNED_FIELD_NAME: shopify_function::wasm_api::CachedInternedStringId = shopify_function::wasm_api::CachedInternedStringId::new(#field_name_lit_str, ); - let interned_string_id = INTERNED_FIELD_NAME.load(); - let value = self.#field_name_ident.get_or_init(|| { - let value = self.__wasm_value.get_interned_obj_prop(interned_string_id); + let value = self.__wasm_value.get_obj_prop(#field_name_lit_str); shopify_function::wasm_api::Deserialize::deserialize(&value).unwrap() }); let value_ref = &value;