Does ServiceNow® Best Practice pay off?
Over the past year as a relatively new developer to the ServiceNow platform I've read a lot of ServiceNow documentation and posts created by the community regarding best practices/industry standards. While many seem relatively obvious/easy to implement I've often wondered what the exact performance (increase in platform experience) they can provide. This blog post aims to uncover the benefits of some common best practices I use while developing on the platform.
All the below tests and diagnostics have been gathered on an OOB ServiceNow developer instance using the demo data provided on the instance itself. Note they were executed 50000 times each to avoid any random CPU clock cycles or interference from any other processes that might have been running at that time on the instance.
Counting Records and GlideAggregate
Several times while on the platform I've needed to obtain a count of the records in a specific table or query and often as a relatively inexperienced developer I would jump to GlideRecord.getRowCount to perform the request. While sometimes there are scenarios where .getRowCount is much easier to implement it can cause scalability issues as a table grows over time.
Using a GlideAggregate is generally recommended as it allows direct database aggregation queries (using a built-in database function) which is much quicker and doesn’t suffer from the scalability issues that GlideRecord does.
This is the description you will find on a number of ServiceNow articles regarding this best practice but exactly how performant is GlideAggregate actually.
The below two code snippets were executed on the task table and the performance timing results can be found below for each implementation.
var start = new Date().getTime(); for (i = 0; i < 50000; ++i) { var getRowCountDiagnostics = new GlideRecord('task'); getRowCountDiagnostics.query() var count = getRowCountDiagnostics.getRowCount(); } var end = new Date().getTime(); var time = end - start; gs.info('Execution time: ' + time);
var start = new Date().getTime(); for (i = 0; i < 50000; ++i) { var getRowCountDiagnostics = new GlideAggregate('task'); getRowCountDiagnostics.addAggregate('COUNT'); getRowCountDiagnostics.query() if(getRowCountDiagnostics.next()){ var count = getRowCountDiagnostics.getAggregate('COUNT'); } } var end = new Date().getTime(); var time = end - start; gs.info('Execution time: ' + time);
Results
As you can see from the execution time of each snippet the execution time for GlideAggregate is almost half that of the GlideRecord.getRowCount. Curious I also tested the above snippets on the problem table (a much smaller table) and the graph below shows a comparison of their execution times. Interestingly getRowCount slightly outperforms GlideAggregate in this circumstance. In this case. though the Problem table consisted of only 20 records and performance is likely to drop off similar to the differences in the task table as the table grows in size.
(TLDR: GlideAggregate is almost always more performant than getRowCount being almost twice as fast on large tables. Performance might be slightly better with getRowCount on smaller tables but as tables balloon in size it is almost certain to drop off.)
When to Dot Walk
As you may know, Dot-walking provides you access to fields on related tables by building a chain of field names separated by dots. This is a neat way of easily accessing data across the ServiceNow platform but what you might not know is that you don't always have to dot-walk. When trying to obtain a sys_id of a referenced object often as a new developer to the platform I would find myself dot-walking to it however this is not necessary.
var id = current.assigned_to.sys_id //Wrong var id = current.getValue('caller_id'); //Right var id = current.assigned_to+''; //Right var id = current.assigned_to.toString(); //Right?
The value of the reference field itself is a sys_id. So dot-walking to it just causes the system to perform an additional database query to retrieve the assigned_to record and then retrieves the sys_id. ServiceNow suggests this can lead to performance issues so let's test this out.
The below results were obtained by running the below script on the same OOB developer instance.
var start = new Date().getTime(); for (i = 0; i < 10000; ++i) { var dotWalkingSysId = new GlideRecord('task'); dotWalkingSysId.query() if(dotWalkingSysId.next()){ var id = dotWalkingSysId.assigned_to.sys_id+''; } } var end = new Date().getTime(); var time = end - start; gs.info('Execution time: ' + time);
Results
From the above results, we can see that dot walking to the sys_id does indeed impact performance with an execution time of 81332 ms. Interestingly getValue() slightly outperforms +' ' as the best way to retrieve a sys_id from a reference field. I also wanted to test a practice I had seen developers use online with the use of the toString() method which uses an override to return the sys_id. Interestingly it is much slower than just simple blank string concatenation however it can deal with null values.
(TLDR: .sys_id does indeed impact performance slightly and should be replaced with either + ' ' or getValue. If left many .sys_id calls could add up and lead to a decrease in customer experience/performance lose.)
I Only Want One
From the first section of this blog, we have worked out that we should generally shift the work to the database when querying/counting vast amount of records. This is no different when we want to limit our table search. When we want to fetch 1 record from a list of 1 million records ServiceNow recommends the use of the setLimit() function to restrict the search.
var start = new Date().getTime(); for (i = 0; i < 10000; ++i) { var limitedQuery = new GlideRecord('task'); limitedQuery.setLimit(1); //Limit applied to the query. limitedQuery.query() if(limitedQuery.next()){ } var end = new Date().getTime(); var time = end - start; gs.info('Execution time: ' + time);
Without this setLimit method, the limitedQuery.query() will bring back all the records and then filtering will have to be performed either on the client-side or server-side resulting in potential performance loss.
Results
The above code was tested on both the Task and Problem table and the below graph shows a comparison between using setLimit() and not.
As you can see on the Task table with a record count of just over 900 records performance is massively impacted without the use of the setLimit method with the same amount of code taking almost 6 times as long to execute. Equally, on the Problem table, the query takes almost twice as long. Similar to the results of GlideAggregate it really does show that wherever possible the built-in database functions really are incredibly powerful and will always outperform any code you could hack together in client/server-side.
(TLDR: When performing any query where you want only a limited number of records returned use setLimit() as the performance gained from using the built-in database functions will greatly outperform any filtering on client/server side.)
What Now?
Great so we've identified some issues that could creep into your codebase and could cause performance/customer experience to suffer but how do we go about remediation or even early intervention to prevent these. We could take the approach that let's not bother and we'll do nothing as performance is fine right now and we can patch later. However, as we've seen these issues become increasingly impactful as your data balloons in size.
For me, the solution that has helped me the most in developing my skills and preventing me from slipping into more 'hacky' solutions is Code Reviews. Something that was missing from many of my University lectures or solutions but helps a great deal in learning the platform and being more proactive instead of reactive in development practices. Yes, this can be a relatively time-consuming practice but as we have seen without it the consumer/customer is likely to suffer.
Want more advise on best practices or looking to extend your ServiceNow platform further? Reach out to the team at Vorto using our contact form or drop me an email: harry.pearson@vorto.co