Fast Notes view reading via Java API:New ViewNavigator cache methods in 8.5.3
Karsten Lehmann 17 January 2013 08:53:08
PrefaceA posting about new API methods in 8.5.3 may look a bit weird, now that the Notes/Domino R9 beta is already out for one month. I wanted to blog about this topic for one year now, actually since last year's Lotusphere conference, where the new API methods got presented by IBM (in session "AD112 What's new in the IBM Lotus Domino Objects: Version 8.5.3 in Demos"), but could not find the time and have always expected that IBM dev would write a wiki article about it - which they haven't so far.
Let's get started!
This blog article is about getting better view reading performance for both Java and LotusScript based applications. As discussed back in 2010 in two blog posts
Fast Notes view reading via C API: A comparison C vs. Notes.jar
Fast Notes view reading via C API Part 2: How to read views 99.7% faster than Notes.jar
it is possible to get an extreme performance boost traversing view data by using raw C API methods instead of Java/LotusScript, which both bypass ECL checks that cost performance and in addition they are able to fetch multiple view entries at once (in one server transaction) instead of one at a time.
Not everyone likes developing C code and compiling it for each platform an application should run on. So IBM decided to add a few methods to the 8.5.2 Java API so that everyone can get better performance.
Most of the improvements have been documented in the Designer wiki article Fast Retrieval of View Data Using the ViewNavigator Cache - V8.52.
Improvements have been made in the ViewNavigator class so that the methods
ViewNavigator.getNext()
and ViewNavigator.getPrev()
can use an internal cache to preload up to 400 view entries. All you have to do is:
//decide how many entries should be preloaded
int numCacheEntries=400;
//get a view from a database
View lookupView=db.getView("myview");
//disable auto updating
lookupView.setAutoUpdate(false);
//get a ViewNavigator instance for all the view entries
ViewNavigator nav=lookupView.createViewNav();
//and set the size of the preloading cache:
nav.setBufferMaxEntries(numCacheEntries);
//and then traverse the view:
ViewEntry currEntry=nav.getFirst();
while (currEntry!=null) {
if (currEntry.isValid()) {
doSomethingWithTheEntry(currEntry);
}
ViewEntry nextEntry=nav.getNext();
//don't forget to recycle!
currEntry.recycle();
currEntry=nextEntry;
}
nav.recycle();
lookupView.recycle();
This sample code reads all the entries of a view from start to end. For categorized views, entries get read as if they all were expanded, so we read parent entries first, then their child entries, next the children's child entries.
Reading subsets of a view
If reading the whole view is too much, you might just as well read a specific view category:
ViewNavigator subCategoryNav=lookupView.
createViewNavFromCategory("topcategory\\subcategory_level1");
or read unread documents:
ViewNavigator unreadEntriesNav=lookupView.createViewNavFromAllUnread("Firstname Lastname/Company");
or start reading document entries beginning with a specific letter (first column must be sorted, e.g. by lastname):
ViewEntry firstEntry=lookupView.getEntryByKey("L", false);
if (firstEntry!=null) {
ViewNavigator subEntriesNav=lookupView.createViewNavFrom(firstEntry);
//we use a cache size of 50; use more if you expect more results
subEntriesNav.setBufferMaxEntries(50);
//traverse the view:
//...
firstEntry.recycle();
subEntriesNav.recycle();
}
Undocumented feature: ViewNavigatort.skip(int entriesToSkip)
There is one more new method in 8.5.2 that is missing in IBM's wiki article:
int start=50;
//try to skip 50 entries and return the entries actually skipped
int skippedEntries = viewNav.skip(start);
if (skippedEntries==start) {
//read the current entry after the skip operation
ViewEntry currEntry=viewNav.getCurrent();
//work with currEntry
//...
currEntry.recycle();
}
This method skips a number of entries from the current position (e.g. from the first entry) and is very fast, because view traversal is done in C and no column data is decoded. It returns the number of entries that were actually skipped.
If
skippedEntries
is less than start
, the view does not contain enough rows, so there is nothing relevant to read. The
skip
method is used in the XPages Extension Library to find relevant data for a view navigator based REST service: the code first skips "start" entries and then reads the next "count" entries from the view and returns their data as JSON. Another use case for this method is counting view entries:
The funny thing is, that both
ViewNavigator.getCount()
and ViewNavigator.skip(Integer.MAX_VALUE)
both the return the same count, but the latter is much faster. New in 8.5.3: Better performance for other traversal methods
While 8.5.2 only provided improved performance for
ViewNavigator.getNext()
and ViewNavigator.getPrev()
, 8.5.3 adds better performance for all the other traversal methods, e.g. public ViewEntry getNextSibling();
public ViewEntry getPrevSibling();
public ViewEntry getNextCategory();
public ViewEntry getPrevCategory();
public ViewEntry getNextDocument();
public ViewEntry getPrevDocument();
public ViewEntry getParent();
public ViewEntry getChild();
To activate the cache, there is a new method in
ViewNavigator
that defines the cache size and the cache filling strategy: public void setCacheGuidance(int maxEntries, int readMode);
which replaces the previous
setBufferMaxEntries
method (you can still use the old method, but the new method can do the same and more). readMode
can either be the constant ViewNavigator.VN_CACHEGUIDANCE_READALL
or
ViewNavigator.VN_CACHEGUIDANCE_READSELECTIVE
. With the first constant, the ViewNavigator fills the cache like it did in 8.5.2, that means it preloads the next
maxEntries
entries of the view as if a categorized view would be totally expanded. The second constant however fills the cache with entries based on the getter method that you call next (see examples below; you can reset the cache size and mode at any time).
Reading category entries
So to quickly read all categories of a view, you could do
//decide how many entries should be preloaded
int numCacheEntries=400;
//get a view from a database
View lookupView=db.getView("myview");
//disable auto updating
lookupView.setAutoUpdate(false);
//get a ViewNavigator instance for all the view entries
ViewNavigator nav=lookupView.createViewNav();
ViewEntry currEntry=nav.getFirst();
if (currEntry!=null) {
if (!currEntry.isCategory()) {
//make sure currEntry is a category or null
ViewEntry firstCategory=nav.getNextCategory();
currEntry.recycle();
currEntry=firstCategory;
}
//set cache strategy to optimize getNextCategory() calls,
//fills the cache with the result of 400 getNextCategory() calls
nav.setCacheGuidance(numCacheEntries, ViewNavigator.VN_CACHEGUIDANCE_READSELECTIVE);
while (currEntry!=null) {
doSomethingWithTheCategory(currEntry);
//move to next category; refills the cache after 400 categories
ViewEntry nextEntry=nav.getNextCategory();
currEntry.recycle();
currEntry = nextEntry;
}
}
nav.recycle();
lookupView.recycle();
Reading direct child entries
In our web development projects, the most important use case for the new cache support in 8.5.3 is to read the direct child entries in a view.
We often do this to populate a lazy loading JavaScript tree or tree table: When the user expands a tree entry, the next tree level gets loaded and displayed.
This is demonstrated in the following snippet.
parentEntryPos
would contain the position of the expanded parent entry. //get a view from a database
View lookupView=db.getView("myview");
//disable auto updating
lookupView.setAutoUpdate(false);
//position of parent view entry to read direct child entries or null to load the first level
String parentEntryPos="3.1.2";
//get a ViewNavigator instance for all the view entries
ViewNavigator nav=lookupView.createViewNav();
boolean foundFirstEntry=false;
if (parentEntryPos!=null && !"".equals(parentEntryPos) {
if (nav.gotoPos(parentEntryPos, '.')) {
if (nav.gotoFirstChild()) {
foundFirstEntry=true;
}
}
}
else {
if (nav.gotoFirst())
foundFirstEntry=true;
}
if (foundFirstEntry) {
//get the first entry to read:
ViewEntry currEntry=nav.getCurrent();
//get the maximum number of siblings to find the right cache size:
int maxNumSiblings=currEntry.getSiblingCount();
//set cache strategy to optimize getNextSibling() calls
//fills the cache with the result of maxNumSiblings getNextSibling() calls
nav.setCacheGuidance(maxNumSiblings, ViewNavigator.VN_CACHEGUIDANCE_READSELECTIVE);
if (currEntry!=null) {
while (currEntry!=null) {
//not 100% sure that the user can see the child entries;
//currChildCount may contain hidden documents:
int currChildCount=currEntry.getChildCount();
boolean hasChildren=false;
if (currChildCount > 0)
hasChildren=true;
Read the position of the current entry in the view:
String currEntryPos=currEntry.getPosition('.');
doSomethingWithTheSibling(currEntry, currEntryPos, hasChildren);
//move to next sibling
ViewEntry nextEntry=nav.getNextSibling();
currEntry.recycle();
currEntry = nextEntry;
}
}
}
nav.recycle();
lookupView.recycle();
Of course, the code can be further improved to ignore the first
start
entries and stop after reading count
entries. Unfortunately, there is no method
skipSiblings(int siblingsToSkip)
in ViewNavigator
to quickly skip sibling entries in a view level. We requested this as a feature, but things are not moving very fast in this area of the product. ;-) Child and sibling counts in Domino: an eye opener
When you look at that last snippet, you might wonder if reading the sibling or child count and the entry position is a wise decision.
Same did I at first. I thought reading count information of ViewEntries would probably take a long time, because Domino has to go through all the entries to calculate the right amount.
The good news is:
Reading child and sibling counts in Domino is actually very fast.
The bad news is:
The count returned is not exactly what you might expect. :-)
I guess a picture is worth a thousand words:
What you see is a view in Sean Cull's Fakenames database with 40.000 fake user records. I added columns for both the child count and the entry positions.
Domino returns a child count of 8, but we can only see 6 entries and there seem to be gaps in the view: the entries 1.4 and 1.5 are missing.
The reason is that I added reader fields to those documents, so that I'm not allowed to see them anymore.
And since checking if a user is allowed to see a document is a pretty heavy operation of comparing reader fields, the user's roles in the database's ACL and the group documents in the server directory, Domino does not do this just to calculate a count.
Domino simply returns a count value instead, that is stored in the view index and is only a maximum of available child/sibling entries.
It's the amount of entries the server can see.
Please note that reading count values is not always a cheap operation. When working with
ViewEntryCollection
, DocumentCollection
or NoteCollection
, Domino will go through all entries in many cases, which takes a lot of time. Troubleshooting cache usage
If you are not sure if your cache settings are actually used, you can enable client_clock debugging via Notes.ini as described here or here (in German language).
While traversing view entries, you should see much less RPC calls to the server when using the cache than without using it.
Backward compatibility
If you want to use the new cache guidance functionality, but your code needs to run on older Domino servers as well, you should not call the new method directly. Your code would not compile in older versions and throw an exception when it runs.
Use Java reflection instead to check if
setCacheGuidance
actually exists: public static boolean setCacheGuidance(ViewNavigator viewNav, int maxEntries, int readMode) throws NotesException {
try {
Method setCacheGuidance=ViewNavigator.class.getMethod("setCacheGuidance", Integer.TYPE,
Integer.TYPE);
setCacheGuidance.invoke(viewNav, maxEntries, readMode);
return true;
}
catch (SecurityException e) {
//ignore, must be 8.5.2 or below
}
catch (NoSuchMethodException e) {
//ignore, must be 8.5.2 or below
}
catch (IllegalArgumentException e) {
//ignore, must be 8.5.2 or below
}
catch (IllegalAccessException e) {
//ignore, must be 8.5.2 or below
}
catch (InvocationTargetException e) {
if (e.getCause() instanceof NotesException)
throw (NotesException) e.getCause();
//ignore, must be 8.5.2 or below
}
return false;
}
ViewEntryCollection improvements?
Talking about
ViewEntryCollection
, there have been plans to add cache support in this class as well. This would be useful to quickly read the results of a fulltext search (e.g. up to 5000 entries by default). Unfortunately, the current R9 beta does not contain anything new in this area, so there is no big hope that it will get added in R9.
If this topic is interesting for you and you attend IBM Connect 2013, mark this session in your schedule:
AD211 - What's New In The IBM Domino Objects?
Elizabeth Sawyer - IBM; James Cooper - IBM
The Domino Objects for Release 9.0 debuts a major set of classes for Calendar & Scheduling as well as additional performance features for View Navigation. Join us as we explore the powerful new capabilities coming soon to application developers through discussion and demonstration of representative use cases.
I'm pretty sure that they will also demonstrate something new in the R9 API, for example
public Vector getColumnValues(int columnNumber)
in the View
class that you can find in the beta version and that should work like @DbColumn. :-) That's it for today. I hope this article is helpful and you can speed up your applications!
- Comments [3]