Introduction
Within Extensible Hypertext Markup Language (XHTML) web pages and Open Document Format (ODF) office documents, XForms can be used to orchestrate the processing of XML data. Separate trees of XML data can be placed in or dereferenced by XForms instance
elements.
The XML data in XForms instances are processed by two integrated mechanisms: declarative bindings and scripted actions. Declarative bindings use XPath expressions to create relationships among data nodes as well as between user interface controls and data nodes. When XML data nodes change, such as via a user interface control, other data nodes and user interface controls with declarative binding dependencies are automatically updated. The second mechanism, XForms actions, are scripts of one or more commands, triggered by XML Events, that can change the XML data by changing content values of nodes and by inserting and deleting nodes. However, XForms action scripts are not just imperative constructs. At the individual command level, the nodes to change, their new values, and the nodes to insert or delete are all specified by declarative XPath expressions. At the script level, data changes made imperatively are automatically augmented by declarative bindings that update dependent data nodes and user interface controls.
For more advanced data processing, XForms actions have also been equipped with the ability to perform conditional logic and loops. Thus, XForms is Turing complete [Pemberton 2018], a fact that this paper highlights by presenting a use case solution that combines the basic data manipulators with conditional logic, loops, and nested loops. XForms action scripting has also been equipped with a delayed execution capability, which was recently demonstrated in [Pemberton 2019]. This paper shows how to use delayed execution to implement non-blocking background execution as well as multitasking of processor-intensive XForms action scripts.
To anchor the discussion of advanced data processing operations, multitasking methods, and hybrid declarative/imperative computing, we focus in this paper on using XForms actions to sort the data presented by rows of a table. The sort keys are data elements presented by columns of the table. Furthermore, since algorithm performance is at a lower priority than efficient exposition of techniques, this paper discusses a reasonably efficient but non-optimal algorithm called selection sort (the additional implementation and performance details of an efficient quicksort in XForms will appear as part of [Boyer 2019]).
To begin the discussion, the Background section describes how to execute XForms markup and provides an overview of XForms data representation, data processing actions, and user interface controls. Next, the section on Data Processing Algorithms presents data processing methods for selection sorting. Then, the section on Multitasking Techniques presents methods for starting, stopping, pausing, and monitoring the progress of concurrently running XForms action scripts as well as for controlling their relative execution priorities. Final remarks are presented in the Conclusion.
Background
Getting Started with XForms
The XForms solutions presented in this paper can be executed directly in current web browsers using a library called XSLTForms. This library relies on the current web browser's XSLT engine to convert the XForms markup into XHTML and JavaScript run-time objects that implement the user interface controls and interaction behaviors specified by XForms 1.1 and XPath 1.0. The XSLTForms library need only be uncompressed into the same web application archive or local directory as an XHTML file, and then the following processing instruction must be placed at the beginning of the XHTML file to enable use of XForms within it:
<?xml-stylesheet href="xsltforms/xsltforms.xsl" type="text/xsl"?>
After processing instructions, such as the one above for XSLTForms, the root html
element of the XHTML file would typically be used to declare globally required Namespaces such as for XHTML and XML Events. The XForms markup appears in the html
element's two child elements: head
and body
. The XHTML head
element contains the XForms model
element, which serves as the parent of the data instance
elements and XForms action
elements that contain data processing scripts. The XForms model
is often written with two attributes: an id
, which makes it possible to target the model
with XML events that invoke action scripts, and the xmlns
attribute to place the model
and its descendants (instances, actions, etc.) into the XForms namespace, as follows:
<model id="M" xmlns="http://www.w3.org/2002/xforms"> ... <!-- Instances, Actions, etc. --> </model>The XHTML
body
element contains the XForms user interface controls that enable the user to interact
with the data in the instance
elements and dispatch events that invoke the action
scripts in the model
.
Data Representation
Each instance
element in the XForms model
can represent one XML data document, as a separate XML document from the containing
XHTML document. Multiple instances are differentiated by id
attribute values that give each instance a unique name within the containing XHTML
document. Throughout all features of XForms, the nodes of XML data are referenced
with XPath, and XForms adds to its XPath processor a function instance()
which receives the identifier of the desired instance and returns the root element
node of the instance's XML data document.
Each instance
element's XML document can be expressed as inline content within the instance
, or it can be obtained as the XHTML document loads from a URL in the instance
element's src
attribute. Both options are shown in this XForms code:
<instance id="ORIGDATA" src="./Sort_Data_10.xml"/> <instance id="DATA"> <data xmlns=""> <items> <item> <num>5</num> <name>Alice</name> <phone>333-1111</phone> </item> ... </items> </data> </instance>The namespace declaration
xmlns=""
is applied to the data so that XPath expressions can refer to elements of the data
without namespace qualification.
In the Data Processing Algorithms section, a sort algorithm puts the item
elements above into a numeric or lexicographic order using either the num
or name
subelement content as the key. The instances above can contain many item
elements, perhaps by virtue of declaration or perhaps due to a looping action script
that generates large lists of items with keys that are differentiated by suffixes
that are randomly generated with the random()
function that XForms adds to its XPath processor.
Data instances can also be used to create intermediate variables that help manage
interactive processes. As an example, here is an instance fragment showing the data
needed to help manage a windowing mechanism for scrolling through large lists of item
elements:
<instance id="VARS"> <vars xmlns=""> ... <window> <start>1</start> <size>20</size> <end>20</end> </window> </vars> </instance>The user interface mechanisms for using and changing these windowing variables appear in section “User Interface Controls”.
In addition to data instances, an XForms model can also contain bindings that express
purely declarative calculations. As an example, below is an XForms bind
element that dynamically calculates the window end
value based on the window start
and size
:
<bind nodeset="instance('VARS')/window/end" calculate="../start + ../size - 1"/>Due to this declarative calculation, the
end
value is automatically updated any time the window start
or size
is changed, such as with the imperative code associated with a user interface control
shown in section “User Interface Controls”.
One notable aspect of the XPath expression within the nodeset
attribute is that the element name vars
is omitted. It is not needed because the instance()
function returns the root element node of the identified instance, and that root
element is vars
in this case. This being said, XPath has some mechanisms that enable authors to specify
the root element if desired. For example, the above nodeset
expression could also be written as instance('VARS')/self::vars/window/end
or as instance('VARS')/../vars/window/end
).
XForms Actions Pertinent to Data Processing
XForms actions express scripts of one or more commands that are to be performed in
response to an XML event. The XForms action elements most pertinent to XML data manipulation
are described below. They may be used individually when a script needs only one command,
but when a script must perform multiple XForms actions, they are placed within a single
containing element named action
. In both cases, a script's root XForms action element includes an attribute to indicate
the specific XML event that activates the script. The attribute name is ev:event
, where ev
is an XML namespace prefix that must be mapped to the XML Events namespace (by a
namespace declaration that may appear in the html
element). According to the default XML event processing, the markup of the element
that bears the ev:event
attribute must define the event handler that is activated when the indicated event
occurs on the element's parent. In this paper, the parent will be either an XForms
model
element or an XForms user interface control.
XForms actions can be conditionally executed using the if
attribute, and looping is supported using the while
and iterate
attributes. Each of these attributes contains an XPath expression that operates over
the XML data instances in the XForms model. If an if
attribute is used, the XForms action script is performed if the XPath expression
evaluates to boolean true. When a while
attribute is used, the script is repeatedly executed zero or more times while the
XPath expression evaluates to true. For an iterate
attribute, the XPath expression is expected to indicate a set of XML data instance
nodes, and the script is executed once for each of the nodes. Most typically, these
attributes are used on a container action
element so that they control the conditional or repeated execution of all XForms
actions contained within the action
element.
The XForms actions most pertinent to moving elements are insert
and delete
. The delete
action has a nodeset
attribute containing an XPath expression that indicates the nodes to delete from
instance data. The insert
action has more attributes that offer various ways of copying instance nodes from
one location to another. In this paper, we will cover the two main usage patterns:
prepending and appending nodes into a parent element. In both cases, the origin
attribute contains an XPath that indicates the source node or nodes to be copied.
For prepending nodes, only the context
attribute is used to specify an XPath indicating the parent node into which the origin
nodes are copied. Due to default behaviors of the other attributes of insert
, if the parent node has any child nodes, the new nodes are prepended to its child
list. For appending child nodes, the context
attribute can still be used to indicate the parent node, but three other attributes
are specified. The nodeset
attribute specifies an XPath that selects the child nodes, the at
attribute is assigned the literal XPath expression "last()"
, and the position
attribute is set to the literal string "after"
. The overall effect is that the origin nodes are copied after the last child of the
parent node. As an example of insert
and delete
, the following code initializes the item
element content in the DATA
instance:
<action ev:event="xforms-model-construct-done"> <delete nodeset="instance('DATA')/items/item"/> <insert context="instance('DATA')/items" origin="instance('ORIGDATA')/items/item"/> </action>Because the
action
element above is a child of the XForms model
, it is performed in response to the xforms-model-construct-done
event that the XForms processor automatically dispatches to the model
after the model is initialized and before the user interface controls are created.
The delete
action removes any item
elements that may already be in the DATA
instance's items
element. Then, the insert
action copies the item
elements from items
element of the ORIGDATA
instance to the items
element of the DATA
instance. This is the pattern for using a template to reinitialize a list, and the
opposite pattern can be used to move elements from one location to a new location:
first use insert
to copy the nodes from an original location to the new location, then use delete
to remove them from the original location.
The setvalue
action is used to changing the text content of a node of instance data. It has a
ref
attribute whose XPath expression indicates an XML data instance node whose content
value will be changed. The value
attribute contains an XPath expression that evaluates to a string that becomes the
new text content for the node. As an example, the following code reduces the value
of the window start
node by the windows size
, unless it is already the minimal value:
<setvalue ref="instance('VARS')/window/start" value="choose(. > 1, . - ../size, .)"/>The
ref
indicates the start
node. For convenience, the value
expression is evaluated relative to the node being changed, so the node being changed
and its parent can be indicated by the .
and ..
shorthand notations, respectively. The choose()
function that XForms adds to its XPath processor performs conditional logic within
an expression. The invocation above first tests whether the start
node is greater than 1. If so, then it subtracts the window size
from it, and if not, then the start
node is given the same value it currently has.
One final XForms action that is relevant to data manipulation is the dispatch
action, which creates an XML event with the name given by its name
attribute and dispatches it to the document element with the XML identifier that
matches the value in its targetid
attribute. This action enables form authors to convert events that occur on XForms
user interface controls into events that occur on the XForms model
element, which contains the action scripts that perform data manipulation. The dispatch
action has an additional optional attribute called delay
that specified a number of milliseconds to wait before dispatching an event. This
enables an action sequence to perform processing later, after yielding to allow event
loop and user interface processing. This capability can be used to help create a non-blocking
loop, as demonstrated in the section on Multitasking Techniques.
User Interface Controls
The group
element is an XForms user interface control that acts as a container for other XForms
user interface controls. One useful technique is to use a group
to declare XForms as the default namespace, as shown below, so that explicit namespace
qualification is not required for the group
nor for the XForms user interface controls it contains.
<group xmlns="http://www.w3.org/2002/xforms"> <!-- XForms user interface controls --> </group>
The trigger
element allows a user to activate an XForms action script without directly entering
any data, such as via a clickable button user interface control. When a user activates
the user interface control, the trigger
element automatically receives a DOMActivate
event. Therefore, an XForms action within the trigger
can run a script in response to the occurrence of the event by declaring itself to
be a handler for the DOMActivate
. As an example, the trigger
element below creates a user interface control that allows a user to scroll to a
previous window of item
elements if the current window start
is above the minimal value:
<trigger> <label>Previous</label> <setvalue ev:event="DOMActivate" ref="instance('VARS')/window/start" value="choose(. > 1, . - ../size, .)"/> </trigger>
The output
control allows users to view data and calculated values. For example, the following
output
elements show the user what range of items out of the total is being shown by a windowing
mechanism:
<output ref="instance('VARS')/window/start"/> to <output value="choose(instance('VARS')/window/end < count(instance('DATA')/items/item), instance('VARS')/window/end, count(instance('DATA')/items/item))"/> <output value="count(instance('DATA')/items/item)"> <label> of </label> </output>The first
output
element shows how to display the value of a node of instance data using the ref
attribute. The second output
element uses the value
attribute to display the result of evaluating an XPath expression. The third output
element shows how to add a label
text before the value to be displayed (rather than placing the text inline before
the output
element).
To show a table of the item
elements and enable user editing of the text content of each item's child elements,
we will use two XForms user interface controls, the repeat
element and the input
element. The repeat
element is a container element, similar to group
, except that its markup content is repeated once for each node in an XPath expression
given by its nodeset
attribute. In the code below, the nodeset
binds the repeat
to the range of item
elements indicated by the windowing variables. For each item
element so indicated, a copy of the input
elements within the repeat
is created. XForms input
elements are user interface controls that allow editing of the data nodes to which
they are bound by the XPath expressions in their ref
attributes. The expressions in the ref
attributes are evaluated relative to the surrounding context nodes, which in this
case are the item
elements from the nodeset
of the repeat
. In the code below, the input
elements allow a user to edit the content of the num
, name
, and phone
child elements of each item
element:
<repeat nodeset="instance('DATA')/items/item[position() >= instance('VARS')/window/start and position() <= instance('VARS')/window/end]"> <input ref="num"><label/></input> <input ref="name">><label/></input> <input ref="phone"><label/></input> </repeat>
There are a number of additional XForms user interface controls, but they are not directly related to the current topic. Details on them can be found in the XForms specification.
Data Processing Algorithms
The data processing algorithm on which we will focus in this paper is called a selection sort. It repeatedly iterates through a list of unsorted items to find the one with the least key, which is then removed from the list of unsorted items and appended to a list of sorted items. The newest item with least key is appended to the sorted list because its key is actually greater than the keys of the other items in the sorted list, which were appended in prior iterations. This iterative process continues until all items have been transferred to the sorted list. The articulation of the XForms markup for implementing[1] this algorithm is divided into parts on how to Initiate Data Processing, Sort by Numeric Keys, and Sort by String Keys.
Initiating Data Processing
The first step of data processing is to identify and declare the process control variables. It will be useful to have a variable to store a copy of the unsorted list of items so that the main process of sorting can be expressed as if the process had received the list as a parameter. Variables must also be declared to have a place to grow the sorted list and to keep track of the least key found in each iteration of the sort. The following instance data declaration creates the required process control variables:
<instance id="SVARS"> <sort xmlns=""> <items/> <sorted/> <least/> </sort> </instance>The
items
element is used to take a copy of the unsorted list of item
elements from the DATA
instance's items
element. The sorted
element is the one into which each item
is transferred when it is determined to have the least key in an iteration of the
sort. The least
element is used to obtain the least key value during each loop iteration.
To enable a user to initiate execution of a data processing algorithm, user interface
trigger
elements in the XHTML body
can be used. Upon activation, they can dispatch events to the XForms model
in the XHTML head
to request performance of the desired algorithms. For example, the following markup
shows how to initiate execution of the numeric and lexicographic sorting algorithms:
... <model id="M" xmlns="http://www.w3.org/2002/xforms"> ... <instance id="SVARS"> ... </instance> ... <action ev:event="sel-sort-num"> <!-- Processing actions to Sort by Numeric Keys here --> </action> <action ev:event="sel-sort-name"> <!-- Processing actions to Sort by String Keys here --> </action> ... </model> ... <trigger> <label>Selection Sort by Number</label> <dispatch ev:event="DOMActivate" name="sel-sort-num" targetid="M"/> </trigger> <trigger> <label>Selection Sort by Name</label> <dispatch ev:event="DOMActivate" name="sel-sort-name" targetid="M"/> </trigger> ...Because the
action
elements above are children of the XForms model
, they are event handlers for the sel-sort-num
and sel-sort-name
events that are dispatched by the trigger
elements above when they are activated by the user. The processing actions performed
by these event handlers are discussed in the next two sections.
Sorting by Numeric Keys
The numeric sorting algorithm is performed by the sel-sort-num
event handler. The first of the processing actions copies the unsorted items from
the DATA
instance into the SVARS
instance. Then, a while
loop is used to generate the sorted
list. The final two processing actions replace the DATA
instance's unsorted item
list with the item
elements in the sorted
list. The following XForms markup implements the top logical level of data processing:
<action ev:event="sel-sort-num"> <insert context="instance('SVARS')/items" origin="instance('DATA')/items/item"/> <action while="instance('SVARS')/items/item"> <!-- Code to find item(s) with least key and move to sorted list --> </action> <delete nodeset="instance('DATA')/items/item"/> <insert context="instance('DATA')/items" origin="instance('SVARS')/sorted/item"/> <delete nodeset="instance('SVARS')/sorted/item"/> </action>
For numerical sorting, we can take advantage of a function min()
that XForms adds to its XPath processor. Like the spreadsheet function of the same
name, min()
returns the least numeric value of the nodes in its nodeset parameter. Once the least
value is known, a setvalue
action can place it into the least
data element so that it can be used to identify the item(s) to move to the sorted
list. The insert
action in the code below uses an XPath predicate in its origin
attribute to obtain all item
elements whose num
child matches the least
key, so if there are duplicate keys, then all matching items will be copied to the
sorted list at the same time. The rest of the insert
element's attributes target the sorted
list with the append insertion pattern. Once the item or items are appended, the
matching items are deleted from the unsorted list of items
, as follows:
<setvalue ref="instance('SVARS')/least" value="min(instance('SVARS')/items/item/num)"/> <insert context="instance('SVARS')/sorted" nodeset="item" at="last()" position="after" origin="../items/item[num=instance('SVARS')/least]"/> <delete nodeset="instance('SVARS')/items/item[num=instance('SVARS')/least]"/>
Note that although there is only a single level loop expressed at the XForms action
scripting level, the run-time performance is still quadratic (as expected) because
the XSLTForms javascript implementation of the min()
function contains a loop.
Sorting by String Keys
Lexicographic sorting by name
is performed by the sel-sort-name
event handler. The same first processing action is needed to copy the unsorted items
from the DATA
instance, the same while
loop is needed to generate the sorted
list, and the same final two processing actions are needed to replace the unsorted
items with the item
elements in the sorted
list. However, for string sorting, the inner comparison loop to find the least
key value must be explicitly expressed with XForms actions because there is no string
equivalent of the min()
function. XForms does add to its XPath processor a compare()
function that performs lexicographic comparison of the values of two nodes given
as parameters. In the body of the outer while
loop, a first setvalue
action can initialize the least
value, and then the expressed inner loop can iterate through the unsorted list of
items
and, for each node, if its name
is lexicographically less than the least
value, then an inner loop setvalue
action can update the least
value with the string value of the iteration node's name
element, as follows:
<action ev:event="sel-sort-name"> ... <action while="instance('SVARS')/items/item"> <setvalue ref="instance('SVARS')/least" value="../items/item[1]/name"/> <action iterate="instance('SVARS')/items/item"> <setvalue if="compare(./name, ../../least) < 0" ref="../../least" value="context()/name"/> </action> ... </action> ... </action>
Note that the context()
function that XForms adds to its XPath processor must be used to obtain the iteration
node because the value
attribute is normally evaluated relative to the result of the ref
attribute. Also, note that the compare()
function is used instead of the XPath less-than operator because the latter duck
types the values being compared. For example, in a list of movie titles, the XPath
less-than operator would place "300" before "1984" and after "42" whereas lexicographic
comparison would make both decisions differently.
As with the numeric sort, the lexicographic sort also has quadratic performance, but the numeric sort is about 20% faster because the inner loop is pushed down one level of abstraction (into the JavaScript implementation). As Figure 1 shows, these algorithms take several seconds to execute as the number of items approaches the mid-hundreds, during which time no other algorithms can execute and the main thread of user interaction is blocked.
Multitasking Techniques
For algorithms that may take longer to execute than a brief pause, it is important to change the implementation[2] so that the user experience is never blocked and the user is aware of their progress. The Coordination section shows how to start, stop and pause any number of non-blocking algorithms as tasks that run concurrently with one another as well as the main thread of user interaction. Then, the Progress section shows how to monitor and display the progress of running tasks, and the Priority section shows how to dynamically vary their execution priorities.
Coordination
Starting a non-blocking process begins the same way as for a blocking process, with
a trigger
or other means of dispatching the initiating event to the XForms model. The handler
for the model event is implemented differently, though, because it must start up a
non-blocking process on a task list and request its first time slice. We begin by
adding a few more process control variables that will help control task execution,
along with a separate data instance to keep a list of running tasks:
<instance id="SVARS"> <sort xmlns="" key="" state="play" priority="4"> ... </sort> </instance> <instance id="TASKS"> <list xmlns=""/> </instance>The
list
element in the TASKS
instance will receive, as a child element, a copy of the sort
element (i.e. a set of process control variables) for each sort algorithm while it
is running. Now that sorting by num
and by name
will both be able to run at the same time, the key
attribute will indicate which sort algorithm a sort
element represents. The state
attribute will help control execution of a sort algorithm. Valid states are play
(start or continue running), pause
, and stop
(interrupted or completed). The priority
attribute will be used to specify the number of milliseconds to wait before allocating
another time slice to a sort algorithm.
Starting up a non-blocking algorithm involves first pushing a copy of its process
control variables into the TASKS
list
and making that copy distinguishable from other sets of process control variables.
In the case of sorting, this involves setting the key
attribute appropriately. Then, as with the blocking sort algorithms, we take a copy
of the unsorted items from the DATA
instance. Of course, we skip these initial steps if the user activates the sorting
trigger
when the sort is already in progress. One reason the user may do this is to continue
a sort that has been paused, so if that is the case, then in lieu of those initial
steps, we change the state
back to play
. Here's how to start up (or continue) numeric sorting:
<action ev:event="sel-sort-num"> <action if="not(instance('TASKS')/sort[@key='num'])"> <insert context="instance('TASKS')" origin="instance('SVARS')"/> <setvalue ref="instance('TASKS')/sort[@key='']/@key">num</setvalue> <insert context="instance('TASKS')/sort[@key='num']/items" origin="instance('DATA')/items/item"/> </action> <setvalue if="instance('TASKS')/sort[@key='num']/@state = 'pause'" ref="instance('TASKS')/sort[@key='num']/@state">play</setvalue> ... </action>Note that in the blocking numeric sort algorithm, XPath subexpressions that referred to
instance('SVARS')
must be changed to start with instance('TASKS')/sort[@key='num']
. The same is true for lexicographic sorting, except for using the value 'name''
for the key
attribute.
The processing performed by each time slice of the non-blocking algorithm is encoded
in a separate XForms action that is activated by a distinct event. The event name
for numeric sorting is sel-sort-num-background-task
. The final step of the non-blocking algorithm's initiator action is to dispatch that
assigned event to the XForms model
to initiate processing in time slices:
<action ev:event="sel-sort-num"> ... <dispatch if="instance('TASKS')/sort[@key='num']/@state = 'play'" name="sel-sort-num-background-task" targetid="M"/> </action>
The processing of each time slice begins with a test to ensure that the algorithm
is still in a state
of play
. If so, then the task performed in each time slice will, in this case, be one iteration
of the selection sort outer loop. So, instead of using a while
loop that blocks processing until all selection sort iterations are done, an if
attribute is used instead to test whether one iteration should be performed. Then,
after performing one iteration of the previously defined actions, a non-blocking loop is achieved by using a delayed dispatch
action to reinvoke the sel-sort-num-background-task
handler after a given number of milliseconds, as follows:
<action ev:event="sel-sort-num-background-task"> <action if="instance('TASKS')/sort[@key='num']/@state = 'play'"> <action if="instance('TASKS')/sort[@key='num']/items/item"> <!-- Same actions to find smallest key and move matching item(s) from unsorted list to end of sorted list --> <!-- (except replace subexpression "instance('SVARS')" with "instance('TASKS')/sort[@key='num']" in XPaths) --> <dispatch name="sel-sort-num-background-task" targetid="M" delay="4"/> </action> ... </action> ... </action>
Once all items have been sorted, the unsorted list of items
becomes empty. The blocking algorithm's while
loop terminates and then execution proceeds to post-processing code that copies the
sorted list back into the DATA
instance. The non-blocking algorithm requires the same post-processing, but since
it is another possible subtask after an iteration that may not be the last, the post-processing
is placed within a conditional test that the unsorted list is empty (an empty nodeset
evaluates to boolean false). By virtue of its position in the markup, the post-processing
step runs in the same time slice as the last sorting iteration. Once the post-processing
logic is performed, the algorithm is changed to the stop
state. When this occurs, execution proceeds out of the play
state markup, where a final action removes the algorithm's set of process control
variables from the TASKS
list
if the algorithm is in the stop
state:
<action ev:event="sel-sort-num-background-task"> <action if="instance('TASKS')/sort[@key='num']/@state = 'play'"> ... <action if="not(instance('TASKS')/sort[@key='num']/items/item)"> <!-- Same actions to copy sorted list back to DATA instance, replacing the unsorted list --> <!-- (except replace "instance('SVARS')" with "instance('TASKS')/sort[@key='num']" in XPaths) --> <setvalue ref="instance('TASKS')/sort[@key='num']/@state">stop</setvalue> </action> </action> <delete if="instance('TASKS')/sort[@key='num']/@state = 'stop'" nodeset="instance('TASKS')/sort[@key='num']"/> </action>
Now that an algorithm can be executed without blocking the main thread of user interaction,
a user can continue to enter data into input
elements, view data shown by output
elements, and activate trigger
elements. Although this is the intended effect in general, users should be restricted
from changing content within the DATA
instance's items
element because the changes will be overwritten with the sort algorithm's results.
For trigger
elements that let a user insert or delete an item
, XForms already offers the if
attribute, so insertion and deletion into the items
element can be made conditional upon the TASKS
list
containing no sort
elements. To help restrict editing content within existing item
elements, XForms provides a readonly
property for every node of instance data, and the value of each node's readonly
property value can be declaratively controlled by an XPath expression that tests
whether there are any sort
elements in the TASKS
list
. User interface controls bound to read-only nodes allow users to view but not edit
the bound node's content value. Moreover, nodes default to read-only if they have
a read-only ancestor. Hence, only the items
element in the DATA
instance must be declared to have a readonly
value of true
to restrict the user from editing the num
, name
, and phone
text content within all item
elements. In the following markup, the readonly
property changes between true
and false
whenever the TASKS
list
changes between containing and not containing a sort
element:
<bind nodeset="instance('DATA')/items" readonly="instance('TASKS')/sort"/>
The execution of a non-blocking algorithm can be paused at any time by programmatically
taking it out of the play
state without putting it in the stop
state. Furthermore, the algorithm doesn't block user interface interactions, so the
form author can give the user direct control of this capability. The following markup
puts the algorithm into the pause
state upon a user's activation of an XForms trigger
in the user interface:
<group ref="instance('TASKS')/sort[@key='num']"> ... <trigger> <label>Pause</label> <setvalue ev:event="DOMActivate" if="@state = 'play'" ref="@state">pause</setvalue> </trigger> ... </group>In the next time slice, the background task event handler
action
performs no operations since all code in the handler is executed on the condition
of being in the play
or stop
states. This includes the fact that no further time slices are allocated since no
further delayed dispatch
actions are performed. As mentioned above, the event handler for the sel-sort-num
event can be used not only to start the sorting algorithm but also to continue it
if it is in the pause
state. In that case, the handler does not create a new set of process control variables,
but it changes the state back to play
and then requests a time slice.
A non-blocking algorithm can also be programmatically stopped at any time before completion
by changing it to the stop
state. Of course, if the algorithm is paused, then a time slice must also be requested
to ensure that the stopping logic executes. Since we must test the pause
state before changing to the stop
state, we request the extra time slice with a delayed action so that it runs only
after the last action changes to the stop
state[3], as follows:
<group ref="instance('TASKS')/sort[@key='num']"> ... <trigger> <label>Stop</label> <action ev:event="DOMActivate"> <dispatch if="@state = 'pause'" name="sel-sort-num-background-task" targetid="M" delay="1"/> <setvalue ref="@state">stop</setvalue> </action> </trigger> ... </group>
Progress
Since the user interface remains active during the execution of a non-blocking algorithm, it is also possible to calculate and report the algorithm's progress. The progress can be calculated and stored in an additional process control variable. For the selection sort, a few additional attributes are also needed to help with calculating progress, as follows:
<instance id="SVARS"> <sort xmlns="" ...> ... <progress unsortedCount="" unsortedGauss="" totalCount="" totalGauss=""/> </sort> </instance>
The unsortedCount
and totalCount
attributes represent the number of items still to be sorted and the total number
of items being sorted. These can be calculated using declarative calculate
expressions as follows:
<bind nodeset="instance('TASKS')/sort/progress/@unsortedCount" calculate="count(../../items/item)"/> <bind nodeset="instance('TASKS')/sort/progress/@totalCount" calculate="../@unsortedCount + count(../../sorted/item)"/>These values are important for tracking the progress of each sort, but using their direct ratio to help calculate progress would be inaccurate because selection sort processing is quadratic, not linear. Specifically, the number of steps, i.e. the work, needed to process n items is given by the Gaussian summation formula: (n+1)*n/2.
The progress of an algorithm is the difference between the total work and the ratio
of the remaining work and total work. For the selection sort, the number of remaining
work steps and total work steps can be calculated by applying the Gaussian summation
formula to the values of the unsortedCount
and totalCount
attributes. These calculated values are stored in the unsortedGauss
and totalGauss
attributes. Then, the value of the progress
element can be declaratively calculated as follows:
<bind nodeset="instance('TASKS')/sort/progress/@unsortedGauss" calculate="(../@unsortedCount div 2) * (../@unsortedCount + 1)"/> <bind nodeset="instance('TASKS')/sort/progress/@totalGauss" calculate="(../@totalCount div 2) * (../@totalCount + 1)"/> <bind nodeset="instance('TASKS')/sort/progress" calculate="concat(round(100 * (1 - @unsortedGauss div @totalGauss)), '%')"/>
The declarative calculations above automatically bind to the process control variables
of all sort algorithms as their representative sort
elements are added to the TASKS
list
. Then, they are automatically updated as implicit additions to the processing of
each time slice. Thus, the sort algorithm is transformed into a hybrid declarative/imperative
algorithm. Furthermore, since the algorithm is non-blocking, declarative user interface
bindings can automatically show the updated progress of each time slice using the
following markup:
<group ref="instance('TASKS')/sort[@key='num']"> ... <output ref="progress"> <label> Progress: </label> </output> </group>
When examining the progress updates on larger data sets, there can be a small lag in updating near the beginning, and the processing seems to slow down more perceptibly toward the end. These effects occur because of the simplicity of the method used to divide the processing across time slices. Each time slice performs one and only one iteration of the sort's inner loop regardless of how much or little time that may take. In very early iterations on large data sets, the inner loop may slightly block processing enough to perceive. The lag near the end is more noticeable because there is so little work being done per time slice that the idle wait time between time slices becomes a bottleneck to completion. A more balanced approach to assigning work to time slices could help to smooth out the processing. Solving the early stage lag would come at a cost of designing unnatural breaking points in the algorithm processing, whereas smoothing out and optimizing the later processing seems both more pertinent and also easier to solve with natural breaking points in the algorithm processing.
Priority
Any non-blocking XForms action script can be given a higher or lower execution priority
using a minor variation of the delayed dispatch
action. In the markup for the non-blocking processes above, the request for each
new time slice was hard-coded to be delayed by 4 milliseconds using the delay
attribute. Instead, the delay can be controlled with a process control variable in
instance data, such as the priority
attribute of the sort
element. For a number of its elements, XForms added child elements to match certain attributes so that the child element could
use a value
attribute's XPath expression to dynamically obtain the value rather than using a
hard-coded attribute value. For the dispatch
action, the value
attribute of a delay
child element can be used to obtain the delay from the priority
attribute in instance data as follows:
<dispatch name="sel-sort-num-background-task" targetid="M"> <delay value="instance('TASKS')/sort[@key='num']/@priority"/> </dispatch>
Increasing or decreasing the execution priority is then as simple as using the setvalue
action to change the priority
attribute value, after which the subsequent time slices will use the new value to
set the length of delay for the successor time slice. The priority can be increased
and decreased throughout the execution of the algorithm. In the following markup,
trigger
elements give the end-user direct control of the execution priority:
<group ref="instance('TASKS')/sort[@key='num']"> ... <trigger> <label>Decrease priority</label> <setvalue ev:event="DOMActivate" ref="@priority" value=". * 2"/> </trigger> <trigger> <label>Increase priority</label> <setvalue ev:event="DOMActivate" ref="@priority" value="choose(. > 1, . div 2, .)"/> </trigger> ... </group>In this markup, the execution priority level is decreased by doubling the number of milliseconds of delay, and it is increased by halving the number of milliseconds, down to the minimum of a 1 millisecond delay. It is instructive to run[2] both sort algorithms simultaneously, and then decrease the priority level of the first sort and increase the priority level of the second sort so that it finishes first.
Conclusion
This paper has presented advanced use of XForms actions to perform processor-intensive data processing scripts and to multitask those scripts so that they can operate concurrently without blocking one another nor the web browser's main thread of user interaction. Via the code for the selected use case of sorting, this paper highlighted many capabilities enabled by features of XForms, including:
-
use of the
if
,iterate
, andwhile
attributes and thechoose()
andcontext()
functions to express the conditional and loop logic of algorithms; -
use of instance data for additional process control variables for algorithm operations and for managing multiple processes;
-
use of the
setvalue
,insert
anddelete
actions for data manipulation as well as process control; -
use of process control variables with
repeat
andtrigger
to implement size-restricted views of large data sets; -
use of the
if
attribute and delayeddispatch
actions to implement non-blocking loops that enable non-preemptive multitasking; -
use of
bind
andreadonly
as a hybrid declarative/imperative technique, specifically to restrict editability of data being processed by non-blocking algorithms; -
use of
bind
andcalculate
as a hybrid declarative/imperative technique, specifically to attach a declarative progress monitor to the time slice processing of a non-blocking algorithm; and -
use of computable delays on
dispatch
actions to control process execution priorities.
Future work should improve the multitasking method to smooth out processor-intensive algorithm execution with a better balance of work done per time slice, again focusing on selection sort as the example. As part of an upcoming work, a hybrid declarative/imperative variant of quicksort will show how XForms actions and data instances can be used to perform explicit recursion [Boyer 2019]. Future work should determine how to do so in a non-blocking manner that enables multitasking during the recursion. This would require a dynamic work balancing method across time slices due to the highly variable sizes of logical work items throughout the recursive processing. Future work should explore performance tuning to mitigate the event processing overhead of many short-duration time slices while still achieving the perception of non-blocking execution. Finally, future work should identify and articulate additional XForms action scripting use cases that can benefit from Turing completeness, hybrid declarative/imperative approaches, and multitasking.
References
[Boyer 2019] John M. Boyer. On the Expressive Power of Declarative Constructs in Interactive Document Scripts. To appear in the Proceedings of the 19th ACM Symposium on Document Engineering, Sept. 23-26, 2019. Berlin, Germany.
[Events] Shane McCarron, Steven Pemberton, and T. V. Raman, eds. XML Events: An Events Syntax for XML. W3C Recommendation. 2003. [online]. https://www.w3.org/TR/2003/REC-xml-events-20031014/.
[JavaScript] Allen Wirfs-Brock, ed. ECMAScript® 2015 Language Specification (6th edition). European Computer Manufacturers Association (ECMA) Standard. 2015. [online]. https://www.ecma-international.org/ecma-262/6.0/.
[Namespaces] Tim Bray, Dave Hollander, Andrew Layman, Richard Tobin, and Henry S. Thompson, eds. Namespaces in XML 1.0 (Third Edition). W3C Recommendation. 2009. [online]. https://www.w3.org/TR/2009/REC-xml-names-20091208/.
[ODF] Patrick Durusau and Michael Brauer, eds. Open Document Format for Office Applications (OpenDocument) Version 1.2. OASIS Standard. 2011. [online]. http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os.html.
[Pemberton 2018] Steven Pemberton. XForms 2.0: What's New in Proceedings of Balisage: The Markup Conference 2018, Washington DC, USA. 2018. [online]. doi:https://doi.org/10.4242/BalisageVol21.Pemberton02.
[Pemberton 2019] Steven Pemberton. A Clock in XForms in XML.com Articles. 2019. [online]. https://www.xml.com/articles/2019/04/07/clock-xforms/.
[XForms] John M. Boyer, ed. XForms 1.1. W3C Recommendation. 2019. http://www.w3.org/TR/2009/REC-xforms-20091020/.
[XHTML] Steve Faulkner, Arron Eicholz, Travis Leithead, Alex Danilo, and Sangwhan Moon, eds. HTML 5.2. W3C Recommendation. 2017. [online]. https://www.w3.org/TR/2017/REC-html52-20171214/.
[XPath] James Clark and Steve DeRose, eds. XML Path Language (XPath) Version 1.0. W3C Recommendation. 1999. [online]. https://www.w3.org/TR/1999/REC-xpath-19991116/.
[XML] Tim Bray, Jean Paoli, C. M. Sperberg-McQueen, Eve Maler, and François Yergeau, eds. Extensible Markup Language (XML) 1.0 (Fifth Edition). W3C Recommendation. 2008. [online]. https://www.w3.org/TR/2008/REC-xml-20081126/.
[XSLT] James Clark, ed. XSL Transformations (XSLT) Version 1.0. W3C Recommendation. 1999. [online]. http://www.w3.org/TR/1999/REC-xslt-19991116.
[XSLTForms] Alain Couthures. XSLTForms. 2018. [online]. https://sourceforge.net/projects/xsltforms/.
[1] To run in your web browser or download the XForms markup for the numeric and lexicographic selection sort algorithms, please visit or download from here.
[2] To run in your web browser or download the XForms markup for the multitasking versions of the numeric and lexicographic selection sort algorithms, please visit or download from here.
[3] A delayed dispatch
action sets up a callback that is only invoked after the specified delay and also
after the containing XForms action sequence finishes. This is because XForms processors
were intended to be implementable even with a single-threaded event loop technology
(for example, see Javascript's setTimeout()
method).