logo

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...

Comments

    • avatar
    • Phil Petts
    • Fri 12 Feb 2010 03:38 AM

    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.

  1. 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.

      • avatar
      • Jake Howlett
      • Fri 12 Feb 2010 08:04 AM

      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.

      Show the rest of this thread

  2. 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.

  3. @tom, it's called lazy loading I think.

    1. 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.

  4. yes, that would work for 1user/use/session very well.

    Otherwise, been there, done that

    • avatar
    • Ferdy
    • Fri 12 Feb 2010 04:06 PM

    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 :)

    • avatar
    • nick wall
    • Mon 15 Feb 2010 05:22 AM

    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

    • avatar
    • shailesh dhal
    • Mon 15 Feb 2010 09:13 AM

    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 ;)

    • avatar
    • Steve Cannon
    • Mon 15 Feb 2010 10:49 AM

    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.

    1. 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.

      Show the rest of this thread

  5. @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

    1. 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.

    • avatar
    • Gerald
    • Fri 19 Feb 2010 08:39 AM

    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

    • avatar
    • Mark Leusink
    • Thu 11 Mar 2010 06:01 AM

    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.

Your Comments

Name:
E-mail:
(optional)
Website:
(optional)
Comment:


About This Page

Written by Jake Howlett on Fri 12 Feb 2010

Share This Page

# ( ) '

Comments

The most recent comments added:

Skip to the comments or add your own.

You can subscribe to an individual RSS feed of comments on this entry.

Let's Get Social


About This Website

CodeStore is all about web development. Concentrating on Lotus Domino, ASP.NET, Flex, SharePoint and all things internet.

Your host is Jake Howlett who runs his own web development company called Rockall Design and is always on the lookout for new and interesting work to do.

You can find me on Twitter and on Linked In.

Read more about this site »

Elsewhere

Here are the external links posted on the same day.

More links are available in the archive »

More Content