I’m into performance at the moment, trying to solve some nasty issues in one of the applications I work on. While researching the topic I came across Karsten Lehmann’s Domino JNA project that allows you to use some low-level C-API methods using Java. It contains functions that are very useful in my scenario, but I also wondered how the library performed. So I wrote some basic tests, comparing JNA with the ‘standard’ (or ‘legacy’) Domino API and, while I was at it, the org.openntf.domino (ODA) API.
So I wrote a couple of tests using the 3 APIs that traverse a view from the well known ‘Fakenames’ database. It is based on the Domino Name & Address book template and contains 40,000 documents. The tests will loop through a view called ‘People’ containing all the documents and will read a value from one of the columns.
At this point I will be taking bets:
Time to see for yourself! I created a simple application that allows you to test the different methods and view the results (latest 10 are shown only): take a look here.
The source code of the application and test code can be found here. The environment: CentOS 6 (64 bit), SSD, 1GB RAM, Domino 901FP5 (64 bit), ExtLib 17, ODA 3.2.1, JNA 0.9.5. Please let me know if I made any errors in the tests.
I don’t know what about you, but the results surprised me! I didn’t expect the overhead of the non-standard APIs to be that big. In every day use I guess this won’t affect your applications a lot, but it’s something you definitely have to be aware of if you needs to squeeze just a bit more performance out of your application.
UPDATE
– Upgraded the JNA project to 0.9.5
– Based on Andy Cunliffe’s comment I’ve added a new test using a ‘manual’ Java loop in the ODA:
ViewEntry ve = nav.getFirst();
while (null != ve) {
// code here
ve = nav.getNext();
}
That code seems to run about 30% faster that a standard Java loop in ODA:
for (ViewEntry ve : nav) {
// code here
}
The test matches my expectations. As mentioned on Slack, using ReadMask.SUMMARY retrieves both programmatic column names and values, so more data to decode, while ReadMask.SUMMARYVALUES only retrieves column values into the lookup result buffer (64 KB max), which is probably what IBM Is using in the legacy API. I added code to the Domino JNA that caches string conversion results so that the column names are only converted once from LMBCS to Unicode per lookup, but they still fill up the lookup buffer and more NIFReadEntries calls are required to read the same data.
The only reason to use ReadMask.SUMMARY might be for some lookups where NIFFindByKeyExtended2 is used internally, which got introduced in 9.0 to improve lookup performance in busy views that change a lot between Find and Read operations (see https://www.mindoo.com/web/blog.nsf/dx/06.03.2015144848KLEJ83.htm for details). I could only make NIFFindByKeyExtended2 work using ReadMask.SUMMARY although IBM dev said it would also return data for SUMMARYValues.
Using JNA to call C API methods instead of using JNI has the advantages that I do not have to write my own DLL/so library and reduces the risk of crashes when code is not working as expected.
But there is definitely a performance overhead that IBM’s legacy API does not have, which is mentioned in the JNA FAQ:
https://github.com/java-native-access/jna/blob/master/www/FrequentlyAskedQuestions.md#how-does-jna-performance-compare-to-custom-jni
Somewhere on my todo list is an entry to test “JNA direct mapping”, that is mentioned in the FAQ, which should reduce method invocation overhead, but that would mean a lot of testing and many code changes.
As shown in your third JNA test that is just reading the note ids, you have more choices to select which data you actually need by calling C API methods directly while IBM in many cases read too much data. Using the legacy API,
at least you can let it skip decoding column values and counts by setting ViewNavigator.VN_ENTRYOPT_NOCOLUMNVALUES and ViewNavigator.VN_ENTRYOPT_NOCOUNTDATA.
BTW reading all note ids in a view can be done much faster using com.mindoo.domino.jna.NotesCollection.getAllIds(Navigate, boolean, NotesIDTable), e.g. with Navigate.NEXT_NONCATEGORY, which uses an undocumented C function internally to speed this up.
Another area where the legacy API is probably much slower is everything that is using ViewEntryCollections, e.g. reading many keyword lookup results and reading FT search results, because in the legacy API ViewEntryCollections are not optimized for performance (yet) like the ViewNavigator is by using setBufferMaxEntries / setCacheGuidance.
I replicated the legacy and ODA tests and got broadly similar figures, but found changing the ODA test to use a getFirst/getNext while loop like the legacy test boosted performance much closer to the legacy API. I’m interested to know if you find the same results. The key appears to be to use ViewNavigator.getNext() and not ViewNavigator.getNext(ViewEntry) as is used in ODA’s ViewNavigatorEntryIterator.
Interesting. I’ve added the test and would say that it’s definitely faster. It gets closer to the legacy API.