Interface Performance and Debugging

The document provides a list of best practices applicable to debugging interface performance and errors.

For more information view the SAIL Performance documentation.

Debugging Interface Performance Issues

The first step to diagnosing any performance issue is identifying that there is a problem. This can be done in a myriad of ways from business users reporting slowness to performance testing results. In either case, once the performance issues have been identified, the next step is to investigate the root cause.

Appian provides three main options for delving into the cause of performance issues:

Administration Console Rule Performance Page

The Administration Console provides the historical performance of all of the rules in the system. It contains a table of the following:

  • Rule type
  • Rule name
  • Number of executions - use this column to determine the high priority items for optimization
  • Average execution time - sort by this column to determine the poorest performance rules
  • Minimum execution time
  • Maximum execution time

Note: the execution time is server execution time, and doesn’t take into account network latency, rendering, etc.

If users report generic slowness within the system without pinpointing specific interfaces, the rule performance page is the first place to look. Focus first on interfaces which have both a high number of executions and high average execution time. In the following case we will investigate WM_uiReportAssessmentTasks which is called 13 times and takes an average of 1 second to run.

Once the interface has been identified, the next step is to identify whether this is a design issue or a load issue.

Access the rule’s historical performance details by clicking on the interface rule name. These diagrams are useful for determining if the performance of the interface is consistent or if there are periods of slower executions.

If the poor performance is affecting specific interfaces use the performance view on each view to further diagnose.

Interface Designer Performance View

The performance view shows you detailed performance information for your expression. You can view live performance results of the expression in the designer or historical trends of the performance over time. The historical performance trends will provide the same view as seen from the admin console.

The performance view is broken into three main parts:

  • Parameters and Direct Children - this section displays information about the current function, rule, or parameter
  • Descendant Functions and Query Rules - this section displays each function and query rule that contributed to the overall evaluation time of the current function, rule, or parameter
  • Descendant Rules - this section displays each interface or expression rule that contributed to the overall evaluation time of the current function, rule, or parameter, but when you drill down, the grid is filtered to show only those interfaces and expression rules that contributed to the evaluation time of the current function, rule, or parameter.

Using the parameters and direct children grid, drill down into the object with the highest percentage of execution time.

In the example above, we would start by drilling down into the with, which accounts for nearly 100% of the execution time. We would continue in this fashion until locating a root cause for the performance issues.

In this case, 51% of the total execution time for this interface is due to an execution of a plugin function called getportalreportdatasubset, which has since been replaced by the out of the box feature a!queryprocessanalytics. This is a good opportunity for both de-customization (removing usage of custom plugin) and optimization (as a!queryprocessanalytics evaluates far faster).

After replacing the getportalreportdatasubset with a!queryprocessanalytics the percentage of time spent evaluating the task report fell from 537ms to 82ms with no change in functionality.

This process can be repeated by focusing on the next most expensive function and continuing the optimization.

Record Type Designer Performance View

This feature allows designers to identify performance issues for records. You can easily drill into the different record UIs (Record List, Summary View, other views defined for the record, and Related Actions) to identify performance issues. This Record Performance page provides the same information as the Interface Performance page.

On the record, switch to "Performance" and select the Record UI to analyze. For options other than the Record List, select a Record Instance to analyze.

Appian Health Check

The Appian Health Check processes various Appian performance logs to present users with an easily digestible list of poorly performing rules. Any rules with an average response duration over 2000 ms will be flagged as High Risk finding.

Some of this data can also be located from the Admin, Interface, and Record Performance consoles, however the Health Check provides further consolidations of the finding and additional information, in the case of the query rule findings. 

To specifically focus on the interface and user performance experience - dive into the User Experience category of the Health Check, as well as the part of the Design (ex:Interface context size, SAIL interface size).

Trace Logging

When enabled, SAIL Trace logs, update every time an event occurs with in-depth information about the event in question. Trace logs are disabled by default because they require a greater and less predictable amount of disk space and they contain potentially sensitive human-entered data such as usernames. When the performance analysis is complete disable the trace logs to prevent performance problems.

You will need to enable the trace logs before you can view them. If on premise open the Appian log4j file, APPIAN_HOME/ear/suite.ear/resources/appian_log4j.properties, and find and uncomment the following lines. If on Appian Cloud, open up a support case.

#log4j.logger.com.appian.perflogs.sail-trace=INFO, SAIL_TRACE
#log4j.additivity.com.appian.perflogs.sail-trace=false

After completing the performance analysis, Change INFO to OFF and leave it - don’t comment this back out again.

If on Appian Cloud, open up a support case.

Typical Performance Issues

Typical Issues Resolution
Local variables are being reevaluated unnecessarily

Ensure that variables defined in a with or a!localVariables where refreshAlways:true  is set truly need to be recalculated on every interface reevaluation. In most cases, it is sufficient for a local variable to be updated only when a referenced variable is modified or directly in the saveInto parameter of a component. 

Avoid having queries execute every time a form reevaluates.

For example, if a search form is presented to the user with various search fields, it would be more efficient to provide a search button that performs the query as a saveInto parameter rather than using with, which runs on every field update.

Looping functions are overused Ensure that the number of fields or sections being loaded into the interface via looping functions is reasonable and not in the double or triple digits. Highly nested sections and fields in looping functions and use of looping functions within other looping functions can make the evaluation of the SAIL form take longer to load.
Unlimited or very high batch size for grids The data set size and size of records shown in any grids is reasonable and not set to unlimited (-1) or a very high number. Use of pagingInfo in query rules and grids is required to keep performance fast.
High number of elements in a picker field Picker fields should returns less than 50 elements as autocomplete results. Not hundreds or thousands. Thousands of elements will significantly slow the rendering of this field.
Large number of nodes or complex logic between User Input Tasks in a chained process model Make sure that a large number of nodes performing work or complex work between User Input Tasks can make moving between forms in a process seem slow. This is not caused by the SAIL specifically but by the process model design.
Too large or complex of a form in a single interface The most common mistake in the design of SAIL interfaces is having too many objects in a single interface. Extremely large forms should be broken out into separate User Input Tasks in a process to improve, as a smaller set of data and logic is needed per interface initial load and re-evaluation.
A Query rule or query entity is running slowly or too many query rules are used

Query rules returning a large amount of data or using highly complex rules to gather the result set can slow down a form/dashboard load significantly.

Utilize the relatedRecordData() function when querying Record Types or Record Data to filter down and limit that data being returned directly instead of performing multiple queries or looping and filtering further in the Interface itself. 

Executing too many query rules on a single page will cause performance issues. Try to limit the number of queries per page to 3.

Never call queries in looping functions.

Try it out

To simulate a performance problem we will create a slowly performing rule and embed it in an interface.

The main interface uses two supporting rules, so let's create them first.

  • PERF_cdtBuilder: Creates a simple cdt to be used in the slow expression rule response.
  • PERF_slowExpressionRule: Slowly performing expression rule used to mimic a slowly performing query or complicated expression. Treat this rule as a black box.

Create an expression rule, PERF_cdtBuilder, with the following rule inputs:

  • title(Text)
  • maxEnrollment(Number(Integer))
  • endDate(Date)

Enter the following definition for the rule:

={
  title: ri!title,
  maxEnrollment: ri!maxEnrollment,
  endDate: ri!endDate
}

Create expression rule PERF_slowExpressionRule with no inputs:

Enter the following definition for the rule:

=load(
  local!numbers: enumerate(20) + 1,
  local!randomNumbers: tointeger(rand(length(local!numbers)) * 100),
  with(
    local!titles: a!forEach(local!numbers, concat("Philosophy 10", fv!item)),
      local!endDates: a!forEach(
        items:local!randomNumbers,
        expression:caladddays(
          a!forEach(
            items:local!randomNumbers,
            expression: caladddays(
              a!forEach(
                items: local!randomNumbers, 
                expression: caladddays(
                  a!forEach(
                    items: local!randomNumbers, 
                    expression: caladddays(
                      a!foreach(
                        items: local!randomNumbers + 30,
                        expression: caladddays(today(), fv!item)), 
                        fv!item)), 
                        fv!item)), 
                        fv!item)),
                        fv!item)),                 

                        apply(
                          rule!PERF_cdtBuilder(title: _, maxEnrollment: _, endDate: _),
                          merge(
                            local!titles,
                            local!randomNumbers,
                            local!endDates
                          )
                        )
  )
)

Now that we've created the two supporting rules, let's move on to the main expression.

Open an interface and create an interface with the following expression:

=load(
  local!pagingInfo: a!pagingInfo(startIndex: 1, batchSize: 15),
  local!data, local!firstName, local!lastName, local!dob, local!phone,
  with(
    local!datasubset: todatasubset(
      rule!PERF_slowExpressionRule(),
      local!pagingInfo
    ),
    {
      a!sectionLayout(
        label: "Class Schedule", 
        firstColumnContents: {
          a!gridField(
            totalCount: local!datasubset.totalCount,
            columns: {
              a!gridTextColumn(label: "Title", field: "title", data: local!datasubset.data.title),
              a!gridTextColumn(label: "Max Enrollment", field: "maxEnrollment", data: local!datasubset.data.maxEnrollment),
              a!gridTextColumn(label: "End Date", field: "endDate", data: a!forEach(items: local!datasubset.data.endDate, expression: datetext(fv!item, "dd/MM/yyyy")))
            },
            value: local!pagingInfo,
            saveInto: local!pagingInfo
          )
        }
      ),
      a!sectionLayout(
        label: "Student Details",
        firstColumnContents: {
          a!textField(label: "First Name", value: local!firstName, saveInto: local!firstName),
          a!textField(label: "Last Name", value: local!lastName, saveInto: local!lastName)
        },
        secondColumnContents: {
          a!dateField(label: "Date of Birth", value: local!dob, saveInto: local!dob),
          a!textField(label: "Phone Number", value: local!phone, saveInto: local!phone)
        }
      ),
      a!buttonLayout(
        primaryButtons: a!buttonWidgetSubmit(
          label: "Submit",
          style: "PRIMARY"
        )
      )
    }
  )
)

Now let's test it out:

  1. Click on Test to experience the initial load time.
  2. Click on Performance View to view the initial load time of approximately 1000ms.
  3. Drill down into Interface > fn!load > fn!with > local!data to see that approximately 99% of the time is spent evaluating rule!perf_slowexpressionrule.
  4. Select First Name, enter a value and then click outside of the field. Notice the Working message remains for over a second. This indicates that a slow expression is being made either in the saveInto for that particular field or in a with function elsewhere on the interface.

In this case, we can clearly see the rule!PERF_slowExpressionRule is being called in a with call. This is typically done to allow for grid sorting and paging, but can have serious impact on the refresh speed of a form. A better design would be to move the expensive expression out of the with and into both the initial load and grid's saveInto parameter so that the expression is only reevaluated when the user interacts with the grid.

This will result in the following optimized expression:

=load(
  local!pagingInfo: a!pagingInfo(startIndex: 1, batchSize: 15),
  local!firstName, local!lastName, local!dob, local!phone,
  local!datasubset: todatasubset(rule!PERF_slowExpressionRule(), local!pagingInfo),
  {
    a!sectionLayout(
      label: "Class Schedule",
      firstColumnContents: {
        a!gridField(
          totalCount: local!datasubset.totalCount,
          columns: {
            a!gridTextColumn(label: "Title", field: "title", data: local!datasubset.data.title),
            a!gridTextColumn(label: "Max Enrollment", field: "maxEnrollment", data: local!datasubset.data.maxEnrollment),
            a!gridTextColumn(label: "End Date", field: "endDate", data: a!forEach(items: local!datasubset.data.endDate, expression: datetext(fv!item, "dd/MM/yyyy")))
          },
          value: local!pagingInfo,
          saveInto: {
            local!pagingInfo,
            a!save(
              target: local!datasubset,
              value: todatasubset(rule!PERF_slowExpressionRule(), local!pagingInfo)
            )
          }
        )
      }
    ),
    a!sectionLayout(
      label: "Student Details",
      firstColumnContents: {
        a!textField(label: "First Name", value: local!firstName, saveInto: local!firstName),
        a!textField(label: "Last Name", value: local!lastName, saveInto: local!lastName)
      },
      secondColumnContents: {
        a!dateField(label: "Date of Birth", value: local!dob, saveInto: local!dob),
        a!textField(label: "Phone Number", value: local!phone, saveInto: local!phone)
      }
    ),
    a!buttonLayout(
      primaryButtons: a!buttonWidgetSubmit(
        label: "Submit",
        style: "PRIMARY"
      )
    )
  }
)

Now it's time to test it out again.

  1. Click on Test to experience the initial load time. It is still the same speed as the query is still running initially.
  2. Select First Name, enter a value and click outside of the field. Notice the Working message disappears almost instantly. This will be the same for Last Name, Date of Birth and Phone Number. The form will now feel more responsive to the user.
  3. Finally, sort the grid by the Title column. Notice the Working message again appears for approximately a second. This is because the expensive query must be run in order to requery and sort the relevant data.

Debugging Interface Errors

Interface errors fall into two main categories: syntactical and data based. Syntactical errors prevent the rendering of the interface in the interface designer and are easy to spot. Data based errors may only occur after interfaces have been released to users and are used in previously untested ways. As with performance based issues, the first step to debugging any interface is to use the Interface Designer.

Data Based Errors

The first step in solving runtime interface errors is to uncover them. Most are hopefully caught during testing, but occasionally the errors are not discovered until they hit production. Fortunately, Appian provides a new log capturing all sail errors. This log is parsed by the Appian Health Check which will return a high risk if the environment is marked as production.

The log provides the object's Design Object Name, Design Object Type, Application Name, and Error Message. Using this information the next step is to attempt to reproduce the error in a lower environment. If a staging environment exists with data matching production, this can ease in the error identification.

  1. Using the Error Message locate the relevant rule throwing the error.
  2. Use the Test button to test the interface with a representative input.

Debugging Techniques

Below is a list of techniques used to perform debugging within a SAIL interface:

  • Comment out sections to diagnose only the specific section throwing the error.
  • Pass data into interfaces and expressions using rule inputs as opposed to referencing domains like  rv!, pv!, and rf!. Look at the local variable values using the local variable viewer.
  • If needed, temporarily add additional local variables to observe the evaluations/values used in nested rules or other nested local variable evaluations.