Fonto has a very useful API that can run an XPath query and notify the users when the results of that query may change. It does this without computing the query eagerly, meaning that the results may actually be the same, but just retrieved in a different way.
This can be extremely useful when one wants to implement for example a sidebar that lists some information on a document, such as a soft validation panel. It has also been used for many other applications, such as triggering CMS calls whenever something changes in the document.
ObserveQueryResults is also the basis of the often-used higher-level useXPath React hook.
This API is very powerful, but with great power comes great responsibility!
Lately, we have gotten reports of these editors feeling slow, the common denominator of these editors was the (mis)use of the ObserveQueryResults API. In this blog post, we will list how to resolve some of the common mistakes that we saw.
Performance of the ObserveQueryResults API
Due to the nature of this API, it may heavily impact performance when used incorrectly.
Recognizing bad observeQueryResults performance
You can use the Performance profiler in your browser to recognise bad performance in observeQueryResults. We find the flamechart to be most useful. Take for example this screenshot of a single keystroke:
We see a couple of things: first, the keystroke takes about 150 milliseconds to complete, which is a lot (ideally, it should well be below the ~16ms frame budget). Secondly, we see that it is dominated by two large evaluateXPaths that are coming in through a call to ‘getResults’. This is a clear sign that we are looking at the observeQueryResults API. Let’s go over the two major pitfalls of the API to see whether we can improve the performance!
Dependencies of the query
Whether an XML change will cause the resultsChangedNotifier to be fired is determined by the dependencies of the query, as well as the XML changes that were just made.
Some queries have more dependencies than others. Take for example the query ./div. This query will only invalidate when the childNodes of the context node change. Contrast this with the ./descendant::div, which is invalidated whenever the childNodes of any descendant of the context node change, which is way more often. Even worse is the .//div query, which is per spec expanded to ./descendant-or-self::node()/child::div. The descendant-or-self::node() part may be invalidated by every keystroke a user performs, especially when the fontoxml-track-changes add-on is enabled. The XPath engine does not yet optimize these queries.
In general, try to limit the dependencies for your query. When in doubt, please use the Dependencies tab in the XPath playground.
The dependency tracking framework is discussed in more detail in the proceedings of XML Prague 2017, Soft validation in an editor environment: https://archive.xmlprague.cz/2017/files/xmlprague-2017-proceedings.pdf#page=31.
The query that we looked at at in the profile looks like this:
this._currentVariablesObserver = indexManager.observeQueryResults( '//html/head/template[1]/dl/di', globalDocumentNode, { expectedResultType: evaluateXPath.NODES_TYPE } );
We can see it starts with //html, which we just discovered to be bad practice. Knowing that the html element can only be at the root of the document, we can rewrite it to /html/head/template[1]/dl/di. Doing so removes the invalidation fully from the profiles, shaving off 80 milliseconds in total. This brings back the time to process a keystroke to a more manageable 50 milliseconds!
Delayed execution of queries
The notifier returned by this API will trigger every time the value for getResults may change. Calling getResults will evaluate the query immediately.
This frequency of updates is not always needed, especially for queries that invalidate on every keystroke. Consider to either make the usage a pull-based system, where another component decides the timing, or use a debounce to limit these updates.
Because we have already removed the profile fully, we can not see the difference for a keystroke. However, we added a 500 ms debounce, to prevent the possibly expensive query to run too often!
this._currentVariablesObserver = indexManager.observeQueryResults( '//html/head/template[1]/dl/di', globalDocumentNode, { expectedResultType: evaluateXPath.NODES_TYPE } ); this._timoutId = null; this._currentVariableObserver.resultsChangedNotifier.addCallback(() => { if (this._timeoutId) { return; } this._timeoutId = setTimeout(() => { this._currentVariables = this.__currentVariableObserver.getResults(); this._currentVariablesChangedNotifier.executeCallbacks(); this._timeoutId = null; }, 500); });
Return values
Not necessarily related to performance, but a common mistake nonetheless is forgetting to pass a expectedResultType to indexManager#observeQueryResults. This usually does not have any effect when developing, but may cause errors later on. This also applies to the useXPath React hook.
Take for example the query ./paragraph[@class=”interesting”]. You might think this will result in a list of all paragraph children of the context node with the given class. That is usually correct, but it can result in the following JavaScript values if the expectedResultType is absent:
- An array of nodes, if there are multiple.
- A single node, if there is one.
- An empty array, if there are none.
This is the behavior of the ‘ANY_TYPE’ result type, which is the default when you omit the expectedResultType.
The easiest way to circumvent these bugs is to provide a result type. To encourage our developers and partners to do so, the upcoming Fonto release will start outputting warnings when the expectedResultType is not passed in any API where it should have been passed.
Conclusion
We discussed how to interpret a performance profile of Fonto and how we can act on it. We also discussed a number of other tips and tricks regarding the observeQueryResults API.
We hope this information helps you to make your editor faster and more stable! Please get in contact with us if you have any further questions!
If you see any way to further improve the performance of observeQueryResults, feel free to contact us! We’re hiring!
Developer advocate / Evangelist. Has been with Fonto since it all began in 2013. He’s currently designing the next steps in Fonto Developer APIs with the input of our valuable partners.
In his spare time, Martin is an avid home brewer.