Building Performant Workflows

Every technology has best practices. In this guide, we’ll address some common issues that impact workflow performance, as well as some tips that can make your workflows more efficient.

Before we begin, a few disclaimers:

  • You’ll see many of the examples in this guide reference the workflow debug timing tab, one of the primary tools for measuring workflow performance. We encourage you to review this feature if you aren’t familiar.
  • Additionally, many of our examples utilize the Virtual Button Trigger and the Debug Node, both of which are very helpful for testing.
  • As always, test diligently before committing any of the recommendations shown here to production workflows. We highly recommend the use of workflow versioning.

Function Node Usage

The Function Node allows the execution of arbitrary, user-defined JavaScript code. WEGnology users well-versed in JavaScript should be very comfortable using this node in a wide variety of cases. It can be a powerful tool, but it should be used sparingly.

This is largely due to the containerized nature of the Function Node. For security purposes, the user’s code runs in a discrete sandbox and is validated before being allowed to enter the workflow. This imparts a fixed time cost every time a Function Node is used, even though the code itself is executed quickly.

In addition, there is a limit to how many Function Nodes can run simultaneously on a single application. Overuse of these nodes in one or more workflows can cause application-wide performance issues, although this is a rare occurrence.

For these reasons, minimizing the frequency of Function Nodes increases efficiency, and if you have multiple Function Nodes in a workflow, you should attempt to combine them. Similarly, Function Nodes should not be used in loops, whenever possible.

We’ve constructed a series of tests to demonstrate the performance advantages and disadvantages associated with the Function Node. In each of the three examples shown below, we’re executing the following tasks:

  • Run 1,000 iterations. For each iteration:

    • Generate three random numbers between (index * -10) and (index * 10).
    • Push all three numbers to an in-iteration array.
    • Return that array for the result of the iteration.
  • Flatten the resulting array of arrays into one flat array.
  • Sort the values in ascending order.

Function Node Within a Loop

In this first demonstration, we’ve placed a Function Node within a loop to accomplish the test scenario described above. This means the Function Node container is being constructed and deconstructed 1000 times.

const getRandomInteger = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);

payload.randomX = getRandomInteger(payload.currentItem.index * -10, payload.currentItem.index * 10);
payload.randomY = getRandomInteger(payload.currentItem.index * -10, payload.currentItem.index * 10);
payload.randomZ = getRandomInteger(payload.currentItem.index * -10, payload.currentItem.index * 10);

payload.myArray = [];
payload.myArray.push(payload.randomX);
payload.myArray.push(payload.randomY);
payload.myArray.push(payload.randomZ);

Function In Loop

12.204s is the completion time.

Single Function Node

For the next test, we’re going to complete the same tasks listed above, but rather than looping through a Function Node 1000 times, we’re going to execute everything in just one Function Node.

const getRandomInteger = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);

payload.result = [];

for (let i = 0; i < 1000; i++) {

    const randomX = getRandomInteger(i * -10, i * 10);
    const randomY = getRandomInteger(i * -10, i * 10);
    const randomZ = getRandomInteger(i * -10, i * 10);

    const myArray = [];
    myArray.push(randomX);
    myArray.push(randomY);
    myArray.push(randomZ);

    payload.result.push(myArray);
}

payload.result = payload.result.flat();

payload.result.sort((a, b) => a - b);

Single Function Node

46ms is the completion time, which is significantly less than when the Function Node was placed in a loop in the first example. This is because the construction and deconstruction of the Function Node’s container only occurred once - not 1000 times.

Single Native Node vs. Single Function Node

In the above examples, we determined for that particular set of tasks, a single Function Node provided the best performance. However, let’s take a look at another task:

  • Fetch 1,000 devices.
  • Group the array by the value of the given tag key, comparing the Array Node’s “Group By” method to using a Function Node.

Since the native node doesn’t have to construct and deconstruct a container to operate within, we will see that native nodes oftentimes outperform Function Nodes when given an equivalent task.

Function vs Native

The important takeaway from the example above is the difference between the Function Node execution time (353ms) and the Array: Group By Node (8ms).

Loop Nodes: Serial vs. Parallel

The Loop Node allows for iterating over values on your payload, or for performing a certain operation a set number of times. When configuring a Loop Node, you must specify whether the loop should run in Serial Mode or Parallel Mode.

Serial loops run one iteration of a loop at a time, while parallel loops run up to five iterations of a loop at a time. This means that parallel loops are almost always the most efficient option, executing up to five times faster than serial loops. This is especially true when the loop is making calls to external services (via the HTTP Node, WEGnology API Node, etc.), since multiple iterations can execute concurrently while waiting for a response.

Here’s a diagram that visualizes the difference:

Serial vs Parallel Loops

There are some cases in which it’s necessary to use a serial loop. If you need to mutate the original payload from one iteration to the next, or if you need the ability to prematurely end the loop, a seriallLoop might be your best option. However, we encourage you to adjust your workflow as needed to facilitate parallel loops where possible.

WEGnology API Node vs. Native Nodes

WEGnology is powered by a full REST API. The WEGnology API Node allows a workflow to utilize this API to access any resource under an application.

Many of the features available in the WEGnology API are also available as discrete native nodes. One example is the Device: Get request, which can be performed through the API or through the native Device: Get Node.

Since the WEGnology API Node has to make a full API request that leaves the private WEGnology infrastructure, this introduces an additional time cost. Therefore, native nodes will almost always outperform equivalent requests made through the WEGnology API Node.

Utilizing a loop to fetch 1000 devices, with WEGnology API Node in the loop compared to the native Device: Get Node in the loop, we have a massive improvement; the native node is more than 10x faster!

Caching with Workflow Storage

You can cache responses from third-party services using Workflow Storage. If a value needs to be referenced multiple times across a workflow, this will help to reduce the number of API calls. It’s generally best practice to use Workflow Storage to cache API responses when relevant to your workflow.

Device: Get Node Queries

The Device: Get Node has a number of different query methods available. Smart usage of these options can result in improved workflow efficiency.

Device Count

If you only need to know the total device count of a given query, returning an object with device count metadata is more efficient than returning an array containing all matching devices.

First, select a query method that can return multiple results (all options other than Device ID). This reveals the Results Configuration section. Then, check Return multiple devices?:

Device Get Count

In the above configuration, we’ve set the Page Number and Results Per Page to 0, and selected Return an object containing metadata along with results. As we can see in the Debug tab (under devices), we have a totalCount value of 900. No other device details are returned. We can also see this request processed in 10ms.

Alternatively, if we were to request an array containing the details of all devices matching our query:

Device Get Array

In this case, we’ve set the Page Number to 0, the Results Per Page to 1000, and selected Return just an array of results (will be an empty array if no items are found). Our Debug tab now shows an array containing 12 keys for each of the 900 matching devices. As expected, the processing time has also increased to 693ms.

Composite State

The Device: Get Node has an option to include the most recently reported device attributes in the result path. This is referred to as Composite State and is configured in the Result section. As we’ll demonstrate below, it’s more efficient to not include Composite State when it’s not needed.

The default option is Include no attributes, which will obviously not include any device attribute data:

Device Get Without Composite

If you want to return Composite State in your result path, you can select Include all attributes or Include the following attributes:

Device Get With Composite

As we can see in our example, processing time increased from 2,118ms to 3,201ms when including Composite State.

Conclusion

WEGnology is constantly working to improve the usability and performance of our platform. We look forward to sharing these new features with you as they’re released. To wrap-up for now, a few key points:

Was this page helpful?


Still looking for help? You can also search the WEGnology Forums or submit your question there.