Troubleshooting Shopify Function Execution Limits: From JavaScript to Rust
Introduction
Recently I was working on a Shopify site for a client and my team created a Shopify function which used the cartTransformAPI. The goal was to create "bundled" products, products which have a single entity in the catalog and are split into multiple components in the cart. This results in multiple line-items in the order which was the requirement from the client.
When testing the function everything was going well until the cart became "large", 10 or so bundled products. At this size the function began to fail and the products were no longer being split in the cart. The error did not present itself in the frontend, the only way that we knew someething was going wrong was that the products were not being split any longer.
Discovery
The best way to get some metrics on how a function is performing is through Shopify's partner portal. Go to Apps, find the name of the App, Insights, Functions, and click on the function name.
This report will give you a quick overview of how the function is performing. For the scope of this article I will focus on the Runs report and the Error report.
On our project, there were function calls with the status of "InstructionCountLimitExceeded". Once you find the status code and are able to copy the error message you can debug the function locally using the shopify function and input JSON from the partner portal.
Debugging
First our team found the error details in the Shopify documentation to get a better understanding of what was going wrong. That led us to the following link: https://shopify.dev/docs/apps/build/functions#limitations-and-considerations
The most pertinent limitation for this issue is: "The logic of the function must execute in a maximum of 11 million instructions, which can be tested locally using function-runner."
Now that we know what the error is, we can debug it locally using the shopify function and input JSON from the partner portal.
Shopfiy provides a utility for running functions locally and reporting their memory usage and execution count. https://github.com/shopify/function-runner
I am running this locally without installing dependencies with npx
.
npx function-runner
A Shopify function must be a "pure" function; given an input, the function must always return the same output.
Going back to the Partner Portal, you can copy the exact JSON input that is causing the error and save it locally.
The function-runner
takes the WASM artifact, the input JSON, and the method to run as arguments. It will execute the function and return the results.
Building your function locally will result in the WASM being outputted to dist/function.wasm
.
npm run build
Putting everything together, you can run the following command:
npx function-runner -f 'dist/function.wasm' -e run -i 'input.json'
Using the JSON from the partner portal we can inspect the details of the function call including the memory usage, and execution count.
The fucntion-runner
output clearly shows that the execution count has exceeded the 11 million limit.
Analysis
Now that we know what the error is and a way to debug it locally, we can study the function and figure out what parts are using the most execution cycles.
My approach was to comment out sections of the function, build, run the function, and then inspect the output. Using this method I was able to identify which parts of the function are using the most execution cycles.
Our function's ultimate goal is to use the cartTransformAPI
to split "bundled" products. There are three main parts of the function:
- Parse a string from the input into an array of JSON objects. These are the "rules" that will be used to split the products.
- Loop over the
cart.lines
and find the products that match the rules. - For each matching product, generate an ExpandOperation to split the product using the
cartTransformAPI
.
Step | Description | Instructions | Delta |
---|---|---|---|
1 | Parse rules only | 2.92857M | 0 |
2 | Loop over cart | 2.981207M | 0.052637 |
3 | Expand products (basic) | 4.428727M | 1.44752 |
4 | Expand products (medium) | 4.934058M | 0.505331 |
5 | Expand products (full) | 5.603001M | 0.668943 |
6 | Output JSON | 11.539462M | 5.936461 |
Looking at the delta column of the table the biggest jumps are coming from Expanding the products and outputting the JSON.
Refactor to Rust
There is not too much optimization that we can do to the the internals of the function. There are certain actions that have to take place and they can only be optimized so much using JavaScript. The next optimization step is to refactor the function to Rust.
Once again, looking at the run details of the function in the partner portal you can copy the JSON that was output by the function and use that to compare that the two implementations, one in JavaScript and one in rust, are ouputting the same JSON. Once the output has been verified to be the same we can look at the "Resource Limits" section of the run details and compare the "Instruction Count" of the JavaScript implementation to the rust implementation.
JavaScript Function Implementation
Rust Function Implementation
Comparing the resource usage of the JavaScript function versus the Rust function you can see that the Rust function uses 7x less instruction count to output the same JSON object. That is a pretty big efficiency gain and results in the Rust function being able to handle many more products in the cart.