Interface Evaluation Lifecycle

This page provides detailed technical information about what occurs during the interface evaluation life-cycle. Understanding how each phase in the evaluation works may help you improve interface performance.

Optimizing Interfaces

Interfaces can vary immensely in terms of what they do and how they do it. To optimize a particular interface, you must understand how its specific characteristics impact each phase of an interface evaluation. Each interface goes through the following phases:

Phase Application Server Network Client User
1 Acquire Context
2 Evaluate Expression
3 Transmit Interface to Client
4 Render Interface
5 Interact with Interface
6 Transmit New Values to Server
7 Reacquire the Context
8 Update the Context
etc. Repeat from phase 2

The following sections describe each of these phases in detail with special attention to potential performance implications as well as optimizations introduced in recent Appian versions.

Example Interface

As each phase is discussed, we will refer to the same simple interface as a common example. It consists of two or three fields:

  • An integer field, always shown, that collects a dollar amount
  • A radio button field, always shown, that determines whether the payment is immediate or scheduled. The choice labels are dynamic based on the entered amount.
  • A date field, shown only when payment is scheduled, that sets the time of payment

You can see it live by pasting the expression below into the INTERFACE DEFINITION in EXPRESSION MODE.

load(
  local!amount: 0,
  local!payImmediately: true,
  local!paymentDate: date(2013, 4, 30),
  with(
    local!dollarAmount: dollar(local!amount),
    {
      a!integerField(
        label: "Amount",
        value: local!amount,
        saveInto: local!amount
      ),
      a!radioButtonField(
        labelPosition: "COLLAPSED",
        choiceLabels: {
          "Pay " & local!dollarAmount & " Immediately",
          "Schedule Payment of " & local!dollarAmount
        },
        choiceValues: { true, false },
        value: local!payImmediately,
        saveInto: local!payImmediately
      ),
      if(
        local!payImmediately,
        {},
        a!dateField(
          label: "Payment Date",
          value: local!paymentDate,
          saveInto: local!paymentDate
        )
      )
    }
  )
)

Phase 1: Acquire Context / Phase 7: Reacquire the Context

The interface expression context holds the data bound to variables for use in the interface expression. Context is not shared between clients or even between different tabs of the same browser.

Initial Context

When an interface is first loaded, an initial context is created based on data relevant to the particular interface type:

Interface type Context data
Start form Process variables (pv!)
Task form Activity class parameters (ac!)
Record view Record fields (rf!)
Report None
Site None

Only fields that are referenced by the expression are loaded into the context. For example, if an entity-backed record has fifty fields but only five are used in the expression, only those five are queried from the RDBMS.

Context variables are created in only two ways. Either they are created by the SAIL framework as part of the initial context or they are created during expression evaluation (phase 2) by the load() function.

Once created, the value of these variables can be used in the interface expression when it evaluates (phase 2), but a variable's value can only be modified by component saves when updates are applied (phase 8).

Ongoing Context Management

After it is created, the context is normally stored in application server memory for rapid access. On later evaluations, it is retrieved from memory, updated, and stored back in memory.

To avoid needless memory consumption, if the user does not interact with the interface for five minutes the context is moved to the Appian data source. If the user interacts with the same interface later, the context is moved from the database back into memory. There is no user-facing difference when loading a previously idle context other than a very small amount of extra overhead. Once the previously idle context is back in memory, it is kept there until it again hasn't been used for five minutes, after which it is again moved to the Appian data source.

When a start form or task is submitted, the context is removed from memory. If the form is never submitted or if the interface is not a form, the context remains for twelve hours and then is removed from the Appian data source.

When the context is about to be stored in memory, if the application server is already using a large amount of memory on SAIL contexts, SAIL will not write it to memory and will instead encrypt it and send it to the client. The client cannot decrypt the context and thus cannot view it or tamper with it. It simply sends the encrypted context back to the server, which then uses it for the next evaluation. This approach to context management saves memory but requires additional server processing (to encrypt and serialize the context) and network transfer (to send it back and forth). This additional overhead is often trivial, but for interfaces with large amounts of data in the context this can result in a noticeable slowdown.

In Appian 7.9 and later, the server compresses the context between uses. This has many benefits, including:

  • It reduces the amount of memory required to store the context
  • It reduces the storage required in the database for idle contexts
  • Because the memory usage is reduced, it reduces the likelihood it will be necessary to start sending contexts to clients
  • In the event it is still necessary to send the context to the client, it reduces the amount of data that must be encrypted and transmitted

Example

Viewed in the interface object preview or on a report, the context of the example interface is to be initially empty. During the first evaluation, three context variables are created by load():

  • local!amount
  • local!payImmediately
  • local!paymentDate

Because local!dollarAmount is a with() variable, it is not stored on the context.

Since this simple example doesn't have a submit button, the context typically stays in memory for five minutes after it is last interacted with and then stays in the primary data store for another twelve hours.

If the example were being used as a process form, local!amount and local!paymentDate would probably be either process variables (on a start form) or activity class parameters (on a task form), in which case they would be loaded in the initial context.

Phase 2: Evaluate the Expression

With each interaction, the entire interface expression must be evaluated to tell the client what has changed, including all the interfaces, rules, and functions it calls. This evaluation must happen no matter how small the change in what is displayed. The time taken by this evaluation is typically dominated by the designer's interface expression and thus can be optimized using the performance view, though Appian 7.7 and Appian 7.8 each reduced time taken in this phase for all expressions by reducing framework overhead as well as the time required to construct interface components.

Starting in Appian 17.4, interfaces that contain multiple data queries may evaluate those queries in parallel, reducing the overall evaluation time of the interface. See the parallel evaluation of expressions section for more information on how Appian parallelizes evaluation.

Analyzing Your Expression

The performance view shows how much time is spent evaluating your expression and which parts of the expression are slowest. The performance view only shows the time spent in your expression and excludes system overhead not directly under your control. Usually this overhead is trivial and the evaluation time in the performance view corresponds closely to the overall server response time, but it may become a factor for very large interfaces or interfaces with a large context that is being encrypted and sent to the client.

Another reason the server response time may exceed what is shown in the performance view is that the performance view does not capture time spent updating the context after an interaction, it only measures the reevaluation of the expression after the updates.

Conditional Display

Most functions evaluate all their parameters, but conditional functions like if() and choose() do not evaluate parameters that are not used. For example, consider the following expression:

  if(
    true,
    "No date",
    date(2013, 04, 30)
  )

When this expression evaluates, the date() function in the third parameter is not called because the if() function does not evaluate the third parameter when the first parameter is true.

In SAIL, this means that when a part of the interface expression is conditionally evaluated using if() or choose(), the server does not need to evaluate it while it is hidden. Often, responsiveness of large forms can be greatly improved by breaking them up into several-step wizards and using choose() to determine which section to show at a given time.

See also: Interface Recipe: Build a Wizard in SAIL

with() and load()

Although they both create variables in the local! domain, the with() and load() variables have differences in behavior that have major implications for performance.

The with() function recreates its variables each evaluation. From a performance perspective, this is beneficial since an expensive calculation can be done once and then referred to multiple times in the rest of the expression. Consider the following simple expression that provides a default value if a query returns null and otherwise returns the result of the query:

  if(
    isnull(rule!slowQuery()),
    "Default Value",
    rule!slowQuery()
  )

The above expression unnecessarily runs the slow query twice when it does not return null. The with() function allows you to avoid this:

  with(
    local!data: rule!slowQuery(),
    if(
      isnull(local!data),
      rule!getDefaultValue(),
      local!data
    )
  )

Now the slow query is made only once. However, if this is an interface, the slow query will still be made every time the interface expression is evaluated. In other words, the query will be made every time the user interacts with the form.

The first time load() is evaluated, it creates a variable on the context and initializes it. On subsequent evaluations, load() does not do anything. This means that, unlike with() variables, load() variables can be used to save user input, but it also means that moving slow calculations to a load() variable can cache the result on the context for use in later evaluations.

  load(
    local!data: rule!slowQuery(),
    if(
      isnull(local!data),
      rule!getDefaultValue(),
      local!data
    )
  )

If this is in an interface, it will only run the slow query once. On subsequent evaluations, the results in local!data will be used. Usually this provides improved performance without any change in the user experience, but if the queried data is rapidly changing and it's important for the user to see updated values, this pattern should be avoided.

Example

Suppose the user loads the example interface and then uses the radio button to opt for a scheduled payment. The interface expression is evaluated twice, once to show the initial interface and a second time in response to the user's click. The following table shows which functions are called in each evaluation:

Reason Functions Evaluated
1 Initial load() date() with() dollar() a!integerField() a!radioButtonField() a!if()
2 After Click load() with() dollar() a!integerField() a!radioButtonField() a!if() a!dateField()

Notice that:

  • date() is evaluated in the first evaluation to initialize local!paymentDate, but after that it won't be called again
  • a!dateField() is not initially evaluated because if() does not evaluate parameters that are not needed
  • dollar() is evaluated each time because it is part of a with() variable's definition, but it is called only once per evaluation (instead of twice, as it would be if a variable was not used)

Phase 3: Transmit Interface to Client

The interface description created by evaluating the expression must be transmitted to the client for display. The entire interface will be transmitted, no matter how small the change.

If the context is stored in application server memory, a very small encrypted key will be sent with the interface that identifies which SAIL context to use for the next evaluation. If the context is not being stored in application server memory, the entire encrypted context will be transmitted along with the interface description.

While these sizes can be large, this response is compressed by the web server when using Appian's recommended compression settings.

Phase 4: Render Interface

After receiving the interface description, the client must parse it and update the displayed interface accordingly. For simple interfaces, this is usually done so fast it is imperceptible to the user, but this can result in noticeable delay on complex interfaces with many components.

While web browsers are redrawing the page, they do not respond to user interactions like mouse clicks and key presses. This feels sluggish and the lack of feedback can cause users to make mistakes. This means that the user experience degrades far more quickly due to browser processing than when there is a slow response from the server.

Browser processing time is determined almost entirely by the number of components on the page. This includes components that are inside layouts such as sections or editable grids. It does not include hidden components, since they are not part of the interface description sent down to the client. However, it varies greatly by client. For example, Chrome is significantly faster than most other browsers while Internet Explorer is usually slower.

Recent Optimization: As of Appian 17.1, once an interface has loaded, only the changed components are rendered on subsequent evaluations. This was previously true under some circumstances in Appian 7.9 and above, but is now always the case starting in 17.1. This means that typically browser processing time is significantly less when the interface changes than when the interface initially loads and all the components must be rendered.

Phase 6: Transmit New Values to Server

Every time the user interacts with a component, the client sends the server the new component value. The client then waits for the server to respond and then renders the resulting interface.

The web client remains interactive while it is waiting to hear back from the server. The user can continue to interact with the interface, but the further messages to the server are queued until the server's response arrives. Once the response arrives, any queued messages are sent all at once. The server applies them in the order they occurred.

When the server is responding slowly, users can interact with most components as usual but dynamic results of their interactions are delayed. For example, a user might type into a text field, tab to the date field after it, select a date, and then tab into an integer field all while waiting for the server to respond to the original change in the text field's value. If the result of that change is some sort of dynamic behavior, such as the display of a new component or a validation message, this effect will be delayed until the server response arrives.

Conceptually, an offline mobile client acts similarly to an online client that is receiving a very slow response from the server. Changes to component values, including form submission, are queued until the client comes back online. If an online interface is known to be slow to respond due to an unavoidable issue like a very slow external data query, consider following the guidelines suggested for offline interfaces.

Examples

If the server hosting the example interface is responding slowly, the user can type into the integer field and pick dates without waiting for the server to respond. However, if they change the radio button field, the date field won't be added or removed until the server response arrives.

Phase 8: Update the Context

When the server receives a change to a component value, it must evaluate the expression again to locate the component's save configuration and update the context appropriately. Once it locates the component, it stops evaluating the expression. This causes parts of the expression above the component to get evaluated twice after an interaction: once as part of updating the context, then a second time when the expression is evaluated with the new context to create the new interface description.

This rarely has sufficient impact to warrant restructuring an interface to move frequently modified components higher up in the expression. However, it is another reason why the results of slow queries and expensive calculations should be placed in load() variables when possible (so they won't get evaluated once or even twice after interactions) instead of with() variables (which get evaluated at least once per interaction and might even get evaluated twice).

Example

Suppose a user has loaded the example interface and switched to scheduled payment. For their next interaction, a different amount of work is required to apply the update depending on which component they modify:

Component Functions Evaluated to Apply Update
Integer Field load() with() dollar() a!integerField()
Radio Button Field load() with() dollar() a!integerField() a!radioButtonField()
Date Field load() with() dollar() a!integerField() a!radioButtonField() if() a!dateField()

These evaluations are made in addition to those required to evaluate the interface expression once the context has been updated.

FEEDBACK