The document provides a list of best practices applicable to debugging interface performance and errors.
For more information view the SAIL Performance documentation.
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:
The Administration Console provides the historical performance of all of the rules in the system. It contains a table of the following:
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.
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:
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.
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.
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).
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.
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.
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.
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.
Create an expression rule, PERF_cdtBuilder, with the following rule inputs:
Enter the following definition for the rule:
={ title: ri!title, maxEnrollment: ri!maxEnrollment, endDate: ri!endDate }
Create expression rule PERF_slowExpressionRule with no inputs:
=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:
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.
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.
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.
Below is a list of techniques used to perform debugging within a SAIL interface: