Don't Use The GetView Method In a Loop
I have a bit of code in a WebQueryOpen agent, which loops a field called "Members", which stores a list of Notes-style user names and prints to the browser their name in a formatted style. The code is very simply:
Set item = web.document.GetFirstItem("dspMembers") Forall v In web.document.GetFirstItem("Members").values Call item.AppendToTextList( "<li>"+GetUserDetails( Cstr(v), "Formal" ) +"</li>") End Forall
No prizes for working out what those does. For each member listed in the document it adds a bullet point to the displayed list of members. How the name of the user is displayed is governed by a separate function.
This GetUserDetails() function is a bit like an extended @NameLookup for LotusScript. I keep it in my "CommonRoutines" Script Library and it's accessible from all my agents. It looks like this:
Function GetUserDetails(username As String, detail As String) As String Dim uname As NotesName Dim userDoc As NotesDocument Dim userView as NotesView Set uname = New NotesName(username) If web.directory.IsOpen Then 'web.directory is "names.nsf" Set userView = web.directory.getView("($VIMPeople)") If Not userView Is Nothing Then Set userdoc = userView.GetDocumentByKey(uname.Abbreviated, True) If Not userdoc Is Nothing Then If detail = "Long" Then GetUserDetails = userdoc.Salutation(0) _ + " " + Left(userdoc.FirstName(0), 1) + " " + userdoc.Lastname(0) _ +", " + userdoc.CompanyName(0) + "<br />"+userdoc.OfficePhoneNumber(0) _ +"<br /><a href=""mailto:"+userdoc.MailAddress(0)+""">"+userdoc.MailAddress(0)+"</a>" Elseif Lcase(detail) = "formal" Then GetUserDetails = userdoc.Salutation(0) + " " + Left(userdoc.FirstName(0), 1) + " " + userdoc.Lastname(0) Elseif Lcase(detail) = "fullname" Then GetUserDetails = userdoc.Salutation(0) + " " + userdoc.FirstName(0) + " " + userdoc.Lastname(0) Else 'Unknown format. Must want field value? If userdoc.HasItem(detail) Then GetUserDetails = userdoc.GetItemValue(detail)(0) Else GetUserDetails = uname.Abbreviated End If End If Else GetUserDetails = uname.Abbreviated End If Else GetUserDetails = uname.Abbreviated End If Else GetUserDetails = uname.Abbreviated End If End Function
The idea is that, given a name like Jake Howlett/ROCKALL it uses the address book to return a name in the form Mr J Howlett, Rockall Design ltd, Nottingham. Or you can just use it to get a field's value by name. If for any reason it can't find the user document or work out what to return it just returns the user name in abbreviated form.
It all works well, but, after not very long I noticed the WQO agent which used it was taking longer and longer to run. The slowness of the WQO was directly proportional to the number of Members. Most of you can probably see why. If not, then the title of this page should give you a clue.
The problem with my code is, of course, that I'm repeatedly calling the getView() method. Consider this from Julian's list of preformance tips:
If you need to use a reference to a view multiple times in your code, get the view only once and share the reference (either using a global or static variable, or by passing a NotesView object as a parameter in functions/subs/methods). Accessing views using getView is a very expensive operation
It turned out that each call to web.directory.getView("($VIMPeople)") was taking 0.3s. For 100 members that means it takes way, way too long to open. Remember no web page should take longer than 7s to open!
So, taking Julian's advice I turned the user view in the directory in to a global variable as part of the WebSession class. Agents that were taking 20 seconds or more to load are now taking less than one!
I had no idea this was such bad practice. More than ten years with Notes and I'm still learning the basics...
Jake,
I've just entered *smug mode*, for once there's something I was aware of that you weren't - a pleasant change!
Also if you use print in a loop, that can cause performace issues as well, certainly on large datasets.
Keep up the good work - the Flex stuff is great.
Phil.
Reply
Hey Jake,
Would it make sense to not instantiate the objects for the directory object and ($VIMPeople) view object?
Then, wrap "get" properties around the directory and ($VIMPeople) view objects. You could check to see if the object is first instantiated (is nothing) and if it is you could instantiate it just once.
Something like:
Property Get PeopleView as Notesview
if Me.vimpeople is nothing then
set Me.vimpeople = NAB.getview("($VIMPeople)")
end if
set PeopleView = Me.vimpeople
End Property
Property NAB as NotesDatabase
if Me.directory is nothing then
Set Me.directory = New NotesDatabase(Me.database.Server, "names.nsf")
end if
set NAB = Me.Directory
end Property
I think it's some sort of design pattern but I can't remember which. This way you don't waste time and resources for the agents that don't use those objects.
Reply
I guess so Tom, if you say so. Don't know though really. As Rob points out in a comment below, expert I am not.
Reply
Show the rest of this thread
Jake, this is one of the reasons you're the best. Very few developers/bloggers are willing to publish something they've done wrong and help others avoid the same mistake. Brilliant.
Reply
@tom, it's called lazy loading I think.
Reply
Thanks Andrew. The Wikipedia article on Lazy Loading was the funniest thing I've read today. The term "eager loading" is still making me giggle.
Reply
yes, that would work for 1user/use/session very well.
Otherwise, been there, done that
Reply
Jake,
Always nice that you are so public with your learnings, a good attitude to have. On performance tuning, although not being in day-to-day Domino development anymore, I remember a few years back reading the redbook "Performance considerations for Domino applications", if I remember that title correctly.
It is pretty much the only red book I ever read cover to cover, and takes a really deep dive into Domino, its internal workings, and the costs of doing certain operations.
If you have the time and the interest, I recommend it. If you don't, the summary is "VIEWS". Almost all performance problems originate there. Indexing, #of docs in a view, use of readers fields, #of columns, # of sorted columns, column formulas, date calculations...there is a long list of design considerations that will make your view shine or fail. I advise not to waste time millisec-tuning a Lotusscript string concatenation. Focus on views and you will win the most.
I hope this helps :)
Reply
Jake,
Further to the performance thread:
Set userView = web.directory.getView("($VIMPeople)")
historically, if I wanted to "freeze" a view, I would then add the line:
userView.autoupdate =false
I would not add this line if I wanted to always be checking with the latest index, but...(I think it was on Chris Toohey's site, maybe someone else's, it was a recent posting which brought this to my attention) if you don't do the autoupdate =false, every time you perform a userView.getDocumentByKey( "", true ) it re-indexes the view.
I managed to find a link to IBM Application tuning article, search page for view.GetDocumentByKey, which backs this up:
http://www.ibm.com/developerworks/lotus/library/ls-AppPerfPt2/
"Note: view.AutoUpdate = False is used primarily to avoid error messages when getting a handle to the next document in a view if the previous document has been removed from the view, but it also improves performance substantially for the agent running. When changing data in documents, you may see significant improvement in your views with view.AutoUpdate = False."
...so I now use this setting much more!
Been reading you since the beginning, keep up good work.
Nick
Reply
Hi, did the same kind of mistake couple of days back..
mine was with view.FTSearch and db.search i was using view.FTSearch which restricts the search output to 5000 docs (default) whereas db.search do not has such limits :)....
what i learned is mistakes are easiest way to learn, unless they are not of the same kind ;)
Reply
Instead of making it global, I usually make these kind of views static. So your code would look something like:
static userView as NotesView
If not userView is Nothing then
Set userView = web.directory.getView("($VIMPeople)")
userView.AutoUpdate = false
End If
...rest of the code below
The first time you hit the function, it grabs the view. The next time you call the function in your loop, it already has the view so you don't get the performance hit. Is there a huge difference between making it Global and static? Probably not other than keeping down the number of Global variables.
Reply
The Notes "help" mentions you can't use Static within a class.
I really wish Lotusscript classes would allow static members and procedures... oh well.
Reply
Show the rest of this thread
@Tom. You actually don't need static inside a class. If you want a variable to "survive" a method call you just define it on the class level. Something like
Class ViewTool
Private db as NotesDatabase
Private view as NotesView
Public sub new(theDB as NotesDatabase)
if theDB is nothing then
RaiseError ...
end if
set me.db = theDB
endSub
public function getUserDoc(userName as String) as NotesDocument
if me.view is nothing then
set me.view = me.db.getview("WhateverNameYouneed")
.... now the search ....
end function
End Class
Reply
I only use Global Declarations for classes and constants.
Static variables as such would scare me because I have no idea where or how they're being set.
Reply
Jake just wanted to tell you thank you for this tip. It greatly increased the speed of an scheduled agent I was working on. Thanks alot :D
Reply
Hi,
I'm also using a WebSession-like base class in all of my Lotusscript agents. One of the problems I ran into to was that in different functions/subs I needed a handle to the same view(s).
Instead of adding variables to the base class for every view I needed, I added a private list variable to the class and added a function to retrieve a view. In the function I check if the specified view was retrieved previously. If it is, the existing handle is returned. If it´s not, the view is retrieved, added to the list and than returned:
retrievedViews_ List As NotesView
Public Function getView( strViewName$ ) As NotesView
If Not Iselement( Me.retrievedViews_( strViewName ) ) Then
Set Me.retrievedViews_( strViewName) = Me.db.GetView( strViewName )
End If
Set getView = Me.retrievedViews_( strViewName )
End Function
(Me.db is a private NotesDatabase variable filled with the current database when instantiating the class)
Besides views you can also use this solution for documents or database handles you need in multiple locations. The getView function can also easily be extended to retrieve views from other databases.
Reply