devxlogo

Server-Side Caching in COM+ and VB

Server-Side Caching in COM+ and VB

Is server-side caching good or bad? This question can create many heated discussions. Since I’m from Sweden, I like what we call the golden middle course. We even invented a word, lagom, which means something like not too much and not too little. (This is my favorite expression in Swedish and therefore I just have to tell it to the world all the time.) Obviously, whether it is good or bad depends upon the context. In this article I will try to show that there are situations when server-side caching can be useful by showing how it can be done with different techniques based on COM+.

There are a lot of different solutions to choose from; therefore I will not invent one of my own. Instead I will test a few that have been around for a while such as the Shared Properties Manager (SPM), LookupTable, Global Interface Table (GIT), Commerce.Dictionary, and more.

This article is written for Visual Basic developers and I will focus on presentation-independent caching of data in configured COM+ components. All tests and code will work in MTS too if you are using NT 4. I will not show any results from MTS, only from COM+.

Even though I will show some results from my tests, my goal with the article is to encourage you to do tests on your own, so that you get results applicable to your own environment and for a real application.

THE ARCHITECTURE AND TEST ENVIRONMENT

I ran my tests in an environment with a very simple architecture. My default architecture is a lot like the one used by Duwamish OnLine. For the configured COM+ components Duwamish calls the different layers Workflow, Business Logic, and Data Access. (Mostly to get a nice sort-order in project groups in VB, I call them Application, Domain, and Persistence Access instead, but that’s another story.)

Where does the caching tested above fit in? In my opinion the most typical place would be the Data Access Layer. Be very sure that you encapsulate all the caching so that you can change it at will easily and without risk.

All the tests discussed in this article have been run in my lab. All the machines are totally untuned. The database server machine is running SQL Server 7 (sp2) on Windows 2000 Server. This 650 Mhz server has 256 MB of memory. The COM+ components are running on a 2 x 533 Mhz Windows 2000 Server with 256 MB of memory. The simulated clients are running on a 266 Mhz NT 4 Workstation (sp4) with 128 MB of memory. The network is a switched 100 Mb and very lightly loaded.

To run the tests, I added the following in the Operating Systems section in boot.ini:

multi(0)disk(0)rdisk(0)partition(1)WINNT=”ONE CPU” /fastdetect /onecpu

That way I created a boot-selection to disable one of the CPUs to do the single CPU-tests.

Developers often believe that if they double the number of CPUs, the throughput will increase 100%. That is of course not the case. Other parts of the system will become bottlenecks, and contention will often be the enemy.

However, it is very important to have an SMP computer in your test environment to help find bugs before you go into production with your applications. I’m always surprised to find that only about 5% of the attendees at my MTS/COM+ course say they have an SMP in their testing environment. Developers often have SMPs in their customer’s production environment, but not in their own testing environment. This is not a realistic choice. In this scenario, problems often occur in production and not in testing, but when the CPUs in production are disabled to make it a one-way box, the problem could not be duplicated

For my company, a one-man-shop, I tried to borrow a dual processor machine for my tests. When I asked around, none of my friends at different companies (and now I am talking about large companies) had an SMP in their testing environment–at least not one running Windows 2000. I finally decided to ask a friend to build me a cheap SMP machine, with off the shelf components. However, when I executed the tests for this article on my SMP, I ran into a problem with the Shared Properties Manager (SPM) that I couldn’t duplicate on my other machines. But I’ll talk more about this later.

THE TESTS

I decided to test two different things:

Reading a static value; in this case, the VAT (Value Added Tax). (This value could be implemented as a constant, but then a recompile would be needed if and when it is changed.) The value fetched is of the datatype single. By static I mean a value that is changeable with some administrative work, but doesn’t require recompilation. Typical examples are the VAT or values needed for business rules that seldom change.

To read some semi-static values; in this case, the id and description of all products that a company produces. The values fetched will be a string with information about approximately 60 products and the total size of the string will be approximately 800 bytes. By semi-static I mean data that is seldom changed, but it must be updateable without the support of an administrator. Since these values are semi-static, they will change every now and then, but I don’t want any user to see an old value at any time. Therefore, it is a requirement that the value seen by the user must always be the same value that is stored, at least at the point in time when the component fetches the value.

I wrote a third test to update the semi-static data, but I won’t show any results from that test since the updating doesn’t happen often and therefore, is not time critical.

Figure 1. ITest Interface

To really stress the COM+ server, I decided to create a loop at the server side for each call from the client tier. This code shows how the looping is accomplished and Figure 1 shows the methods in the ITest interface.

Private Function ITest_StaticFetch() As Single

Dim intI As Integer

For intI = 1 To StaticMax

ITest_StaticFetch = StaticFetch()

Next intI

End Function

For both the static and semi-static tests I made 10 calls to the caching mechanism for each call from the client. If I made only one call for each call from the client over DCOM, the DCOM call would take perhaps 90% of the time, and therefore the different caching mechanisms would appear to have similar performance. My descision is realistic to some degree since it is common to fetch several values from the cache at each client request. This also helps to make the tests more CPU-bound on the COM+ server.

Performance comparisons can vary for many reasons and with distributed applications, the problem is much worse. Note that the results I give in this article are only an indication of what you might see in your own environment. You’ll also note that the code is written as simply as possible to perform the task, often at the expense of good coding style. (See Example of Code Structure for COM+ Component.)

TEST TOOLS

One of the test tools I wrote is a single-threaded Visual Basic application that I call Td for test driver. (See Figure 2.) To simulate a high load, I just start several instances of the application. This test tool is a bit of a fast hack, and it is highly coupled, not only to the interface ITest that the coclasses are implementing, but also to the coclasses themselves. Anyway, it does its work and it could easily be adapted to be more flexible and competent. All the test results in this article have been created with this tool.

Figure 2. TD application

I can specify when Td is to start calling the component so that I can synchronize several instances of Td. I can also state how many seconds the component will get calls before I count the transactions so that the component gets its working temperature up. In the Execute (s) box, I enter the length of time I want the test to be executed and in Server I specify the machine where the component is located.

The Activate button puts the application in a wait-state until its Start time. When the Save button is pressed, all the settings in the form are saved to the registry. When the Get button is pressed, settings read from the registry fill the form. The Save and Get buttons will make it easy to synchronize multiple client instances so they start at the same time, do the same work, and so on. The close button closes the form.

I have created eight different ActiveX DLLs (one for each technique that I test). Each ActiveX DLL has a coclass called Test that implements the ITest interface. The coclasses also implement the IBank interface (see Figure 3). The DoCommitOnly method is used for StaticFetch. This means the VAT will be returned as a Long, which appears strange since the VAT (in Sweden at least) is 25% or 0.25 (a decimal value). DoDebitCreditTx is used for SemiStaticFetch, and DoSimpleReadSP is used for SemiStaticUpdate.

The coclasses implement the IBank interface to make it possible to use them from the MTClient application in the Windows DNA Performance Toolkit (http://www.microsoft.com/Com/resources/WinDNAperf.asp). MTClient is a multithreaded client that can simulate loads more or less like Td does, but you don’t need several instances on every client machine (unless you want to mix different transactions). To make the tests more realistic, you often want a mix of transactions (for example you might want to have 70% reading and 30% updating). However, each instance of MTClient can only call one specific method and therefore only one transaction-type (unless the method itself is using several transactions to create a mix of transactions). To simulate mixed transactions with MTClient, two instances must be used.

MTClient is also more loosely coupled to the progids than Td since you give the progid to MTClient by typing it in. Another good thing with MTClient is that you can enter your estimation for the number of Transactions Per Second (TPS) needed. Then MTClient will tell you if your component met your expectation.

Figure 3. IBank interface implemented by the coclasses

Using MTClient like this is a bit of a hack of course. This interface isn’t needed for the tests I did, but I implemented it so I could use MTClient. Nevertheless I think using MTClient is sometimes a good solution. Figure 4 shows some results from MTClient although I am not comparing this solution to the other solutions. For these results, I’m using the ProgId SrvCacheFile.Test and the method DoCommitOnly. You can see the settings I’ve used in the Figure 4 (10 client threads, 5 seconds ramp up time, etc). You can see that my goal of 10 TPS was met. You can easily experiment with MTClient directly if you install the Windows DNA Performance Toolkit and download the code for this article.

Figure 4 MTClient results

MTClient -PSrvCacheFile.Test -M1 -#10 -RU5 -SS15 -RD5 -RT2 -SC10 -T1000

test parameters:

Using JIT (Just In Time) Activation

DriverID: 0

SrvCacheFile.Test

DoCommitOnly

10 client threads

5 seconds rampup time

15 seconds work time

5 seconds rampdown time

2 seconds response time required

10 TPS scaling

1000 milliseconds think time

———-

Driver 0 running mtclient.exe

TPS Tx % calls % calls met Secs Secs/Tx

succeeded response time

___________________________________________________________________________

Driver 0 9.89 150 100 100 15.17 0.011

I tested these eight techniques. I will explain each technique and show the test results in the following sections.

Going to the database server each time to get data

Doing nothing (only to give the best possible results for comparison)

Using the Shared Properties Manager

Using the file system

Using global variables for read-only data

Using a LookupTable

Using the Global Interface Table (GIT)

Using Commerce.Dictionary

With the code available for download with this article, you can choose to run the tests for any one of the techniques. You don’t have to install Site Server Commerce Edition, for example, if you only want to compare SPM with LookupTable.

All results will be shown relative to the first scenario of going to the database server each time using 1 CPU in the COM+ server. Therefore, this base value is set to 1. All other values in the test results will be relative to this value. (CPU-usage is for the COM+ server.) As you can see in Figure 5, by adding another CPU, the number of transactions increases by 10% and the CPU-usage goes down from 100% to 94%.

Figure 5 Test results example

Go to the database server each time

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

1

100%

1.1

94%

Semi-static read

1

100%

1.5

99%

All tests are run with five instances of Td on one client machine. Observe also that I instantiate the COM+ component from the client and then that instance is used until the test finishes. The components will not call SetComplete and they do not implement the ObjectControl object. The components will not have error handling either. Once again, the components are by purpose extremely simple.

OK, let’s move on to the different techniques I tested for this article.

GOING TO THE DATABASE EACH TIME

Going to the database each time even if the value is readonly is one extreme of these techniques. If you meet your performance goal with this solution, then this is a good solution. Simple and flexible.

And since this is the technique that I’m trying to beat with the other solutions, I have (as stated above) recalculated the results from this test so that the result from going to the database always equals one. This way the relative difference will be obvious when you compare the results.

Figure 6. Data access

Public Function VatGet() As Single

Dim con As ADODB.Connection

Dim cmd As ADODB.Command

Set cmd = New ADODB.Command

With cmd

.Parameters.Append .CreateParameter(“”, adSingle, adParamOutput)

.CommandType = adCmdStoredProc

.CommandText = “VatGet”

Set con = Connect()

Set .ActiveConnection = con

cmd.Execute , , adExecuteNoRecords

VatGet = .Parameters(0).Value

.ActiveConnection = Nothing

End With

‘Clean up

Set cmd = Nothing

ConRelease con

End Function

Observe that the code shown in Figure 6 is used for the database access in the other components too. As you can see, I have used stored procedures in the database. Another thing to note is the use of the GetString method to convert the recordset into a string. You may not want to receive information back to the client tier in this way, but I wanted to use a string as a simple caching unit. (Otherwise I could use disconnected recordsets for moving data between layers, but I decided to free my tests from all recordset-related issues. It’s actually not a recommended solution at all to use ADO recordsets for caching solutions in a shared scenario as is the case with server-side caching). The code for fetching static data for this technique looks like this:

Private Function StaticFetch() As Single

StaticFetch = VatGet()

End Function

In the table in Figure 7, I show the CPU-usage from the COM+ server (which is what I am going to show in all the following result tables too). Don’t forget that in this case there will also be a lot of CPU-usage on the database server which will be avoided in the other solutions. Often the database server is the hardest component to scale out and therefore this is valuable.

Figure 7. Results for Going to the database server each time

Go to the database server each time

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

1

100%

1.1

94%

Semi-static read

1

100%

1.5

99%

For static reads, there is little use of the extra CPU on the COM+ server, and for semi-static reads there is significant benefit from the extra CPU. I assume that in the second case there is more work done by ADO in the COM+ server than in the first case. Observe that the static read test was CPU-bound on the COM+ server, even though it has a significant CPU footprint on the database server too.

DOING NOTHING

Figure 8 shows the results when nothing is really done at the server. That is exactly what the Do nothing coclass is trying to do. It returns a constant for the static read and the semi-static read.

The results for the number of transactions of all other techniques should probably be somewhere between Go to the database server each time and Do nothing.

Figure 8 Results for Do nothing

Do nothing

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

50

100

64

98%

Semi-static read

90

100

105

74%

So far I have a nice range from 1 to 64 for the static read and 1 to 105 for the semi-static read to compare all the other techniques. I expect those ranges to be the lower and upper results.

USING THE SHARED PROPERTIES MANAGER

When using server-side caching in MTS/COM+, the SPM might be the first solution you think of. If you use COM+, the SPM is included in the ordinary COM+ Services Type Library. If you use MTS, you must reference the Shared Property Manager Type Library.

In October 1999 I wrote an article for VB-2-The-Max about 18 common programming traps when using MTS and one of those errors was forgetting to use the SPM. Then I gave some test results from an extremely simple test as a proof of its benefits. A little later I received an e-mail from Troy Cambra at Microsoft where he said the following:

“This is potentially a very bad recommendation. The Shared Property Manager does not scale well in many circumstances. On a single processor box it is fine and in the single client test, it is great, but as you add processors and users, you will often find the scaling/performance curve to be opposite to what you expect. Under any load (concurrent users), you will typically see SPM scale inversely with the number of processors. This is because the implementation is rather simplistic and serializes all reads as well as writes. We are going to be coming out with several new best-practices documents and most of them recommend against SPM for such a purpose. SPM is OK in situations were contention will not be an issue or where scalability is not an issue, but other than that, it is usually quite detrimental.

Storing state in the database is currently the best practice and will continue to be so in the immediate future. Writing a solid, well-performing, well-scaling general purpose in-memory cache is extremely difficult. Databases have had decades to optimize and tune accordingly and as such are simply much better at managing state.”

After the discussion with Troy I wrote a new article where I discussed the whole SPM matter further. (For the complete article, see The 19th common programming trap in MTS.) I also decided to reduce to a minimum the amount of SPM-related information in my MTS course. As a result, I was especially interested in seeing how the SPM would do in those tests. Figure 9 shows the code I use to do a static fetch in the class for the SPM.

Figure 9. Static fetch in the class for the SPM

Private Function StaticFetch() As Single

Dim spmMgr As SharedPropertyGroupManager

Dim spmGroup As SharedPropertyGroup

Dim spmProperty As SharedProperty

Dim bolGroupAlreadyExists As Boolean

Dim bolPropertyAlreadyExists As Boolean

Dim sngVat As Single

Set spmMgr = New SharedPropertyGroupManager

Set spmGroup = _

spmMgr.CreatePropertyGroup(PropertyGroupName, LockSetGet, _

Process, bolGroupAlreadyExists)

Set spmProperty = spmGroup.CreateProperty(PropertyVat, _
bolPropertyAlreadyExists)

If bolPropertyAlreadyExists = False Then

‘Write to SPM!

sngVat = VatGet()

spmProperty.Value = sngVat

Else

sngVat = spmProperty.Value

End If

Set spmProperty = Nothing

Set spmGroup = Nothing

Set spmMgr = Nothing

StaticFetch = sngVat

End Function

Unfortunately I get an Access Violation when I stress the SPM on my SMP machine. I don’t want to draw any conclusions from this because perhaps the problem is related to my cheap, home-built SMP machine. The error 800706be is being raised back to the client and there is information in the event log about the Access Violation. I added symbols to my SMP machine and the call stack reported in the event log is shown in Figure 10:

Figure 10. Call Stack for Error

The serious nature of this error has caused the process to terminate.

Exception: C0000005

Address: 0x7310DC0F

Call Stack:

COMSVCS!CSharedPropGroupObject::CreateProperty(unsigned short *,short *,struct ISharedProperty * *) + 0x7D

COMSVCS!const ATL::CComObject::’vftable'{for ‘ISupportErrorInfo’} + 0x0

COMSVCS!ATL::CComObject::AddRef(void) + 0x0

COMSVCS![thunk]:CSharedPropObject::QueryInterface’adjustor{4}’ (struct _GUID const &,void * *) + 0x0

+ 0x136CFA70

I haven’t determined the cause of the error, but after some experimentation I found a way to make the tests work. The recommendation I’ve seen is to always use Requires for the Synchronization support for STA-components. Then you get the same behavior as in MTS. However, I unchecked Enable Just In Time Activation and changed the Synchronization support to Not Supported in the Component Services snap-in and the problem disappeared. Because I had to change the Synchronization support to Not Supported for the component that uses the SPM, the results are hard to compare with all the other components that use Requires as the setting. When I tested the difference for global variables (with Requires and Not Supported) I saw an increase in the throughput between 20% and 25%. Therefore I have decreased the SPM-values by 20% to make the comparison between apples-vs.-oranges less obvious. The results are shown in Figure 11.

Figure 11. Results for using SPM

SPM

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

29 (see text!)

100%

39 (see text!)

60%

Semi-static read

51 (see text!)

100%

38 (see text!)

33%

I also found it interesting that when I tested with two CPUs, there was a large difference in the load between the CPUs. The first was loaded to about 95% and the second to 30%.

To support Troy’s comments above, you’ll see that the number of transactions dropped relative to the usage of global variables (discussed later) when I compared 2 client processes and 5 client processes.

After reconfiguring the Component Services snap-in, the SPM sometimes performed quite well in this test. However, the results from time to time varied considerably especially for the semi-static read with 2 CPUs. Even though I show the results of a good test, the result drops when the second CPU is added. It may be my lab environment, but I will be reluctant to use the SPM in heavy load scenarios since there are signs of instability and bad scalability.

USING THE FILE SYSTEM

Another common solution to the problem is to use the file system. For example, database triggers could write to a specific file when something changes, but a more typical solution is to recreate the file for semi-static data when the update method updates the database. The update method will be responsible for updating the file too. I don’t think that the files should be the main storage for the data. The main storage should take place in the database and then the values can be cached in files.

I have decided not to use the File System Object for the file handling here, but that is often a good idea. It often performs better than the ordinary Visual Basic file-handling code, but that wasn’t the case in my simple tests.

Something else to consider here is the multithreading aspects of the reading and writing of the files. There are specific solutions for this when it comes to files, but in this test I decided to use a mutex to protect the critical section when I read from and write to the semi-static file. (Read more about Win32-techniques for protecting critical sections on msdn.microsoft.com.)

By the way, watch out in coding critical sections. Be very sure you know what you’re doing before you go there! Figure 12 shows the code I used for the semi-static fetch in this scenario and Figure 13 shows the results.

Figure 12. Semi-static fetch in the class for Files

Private Function SemiStaticFetch() As String

Dim lngHandle As Long

Dim lngResult As Long

Dim intFile As Integer

Dim strBuffer As String

intFile = FreeFile

lngHandle = CreateMutexAndWait(MutexName)

Open App.Path & FileNameProducts For Input As intFile

Input #intFile, strBuffer

Close intFile

ReleaseMutexAndCloseHandle lngHandle

SemiStaticFetch = strBuffer

End Function

Figure 13 Results for Using the File System

Files

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

6

100%

7

100%

Semi-static read

9

100%

10

69%

I haven’t optimized this code so much better performance is no doubt possible. Even so, I don’t like this solution because the added CPU didn’t help very much at all. The only scenario I can think of for using files are when I don’t want to disturb the database server and I have a scarce amount of memory on the COM+ server or something large to cache. It can also be good for large XML files.

USING GLOBAL VARIABLES

A global variable in Visual Basic is a variable that is declared in the declarations section of a module. It doesn’t matter if the global variable is declared as public, private, dim, static, or global, or even if the static variable is in a function in a module.

Using global variables in COM+ components (built with Visual Basic) can be really dangerous if you don’t know how they will work in that environment. Each activity (logical thread) in COM+ will run on its own physical thread and each physical thread has its own instance of global data. When there are more activities than physical threads, the activities will be multiplexed over the physical threads. Therefore, you will see your value in global variables sporadically overwritten if you use them for more than read-only values. For a deeper discussion about this, read my article MTS (and COM+) and global variables.

When you know how global variables work in COM+ components, you can benefit from using them instead of seeing the unpredictable results as a problem. If you use them for read-only values, there is no danger. Use a Sub Main in your component to set the values. If you have checked Retain in Memory for the project properties of your component (and you definitely should), the Sub Main will only be called once for each thread for the lifetime of the COM+ application. (Watch out for doing too much in Sub Main so that you don’t hit a specific DCOM timeout. You may also introduce code that is harder to debug. A better approach would be to use lazy initialization so that the value will be cached the first time it’s requested.)

Since each thread has its own value, there is no contention whatsoever between the threads. But beware that you are using an implementation detail that may change in any version! (In the current version of COM+ you have approximately ten threads per CPU. In MTS you had 100 threads by default.) This is the code I use in the Sub Main procedure for my tests.

Option Explicit

Global gsngVat As Single

Global gstrProducts As String

Public Sub Main()

gsngVat = VatGet()

gstrProducts = ProductsGet()

End Sub

And this is the code I use to fetch the static data in this scenario.

Private Function StaticFetch() As Single

StaticFetch = gsngVat

End Function

The test for reading semi-static data is not applicable without a totally different approach. In the case of global variables, there are really several values and not just one single value since each STA will have its own global data. Somehow the update has to be propagated out to the global data in all the different threads. Therefore I will not show any results here from that test. I will discuss this further in the section called Other algorithms below.

Figure 14 Results for Global variables

Global variables

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

50

100%

64

99%

From the results shown in Figure 14, you can see that using global variables (2 CPUs) is 64 times faster than going to the database each time (1 CPU). When was the last time you got a wage increase so that the new wage became 64 times larger?

Please note that this information is VB-specific. If you use global variables in Delphi for example, they will by default be global for the whole process.

In my opinion global variables are great for read-only data. In the VB-case it’s very hard to use them effectively for semi-static data.

USING A LOOKUPTABLE

A lot of ASP-developers started to use the VBScript dictionary object found in scrrun.dll but they found it produced threading issues except when used at the page level. (See the section about Common Traps for more information.) To solve the problem the ASP-team developed a dictionary object called LookupTable. LookupTable has Any Apartment (or Both as it is called in MTS) as its Threading Model.

Microsoft does not officially support this component, but as I understand it, it has become very popular in the ASP-community lately. To download the component, go to msdn.microsoft.com/workshop/server/downloads/lkuptbl.asp. You will also get the source code for the component, which could be nice if you want to extend it to your specific needs.

Working with the LookupTable is not complex. After instantiation, you use the method LoadValues to read the values from a text-file. After that you can use Value(index) or LookupValue(key) to read the values. You can also call ReadLock and ReadUnlock so that a read won’t interfere with a LoadValues call. A nice feature that comes for free is the option of having the object update its values automatically after a specific number of seconds (sometimes called aging view). Most often that is real-time enough for your application’s semi-static data.

Where do you save the object reference to the LookupTable-object? In my tests I chose to save the object in the SPM, but in order to keep the SPM from influencing the test results, I am caching the object in a global variable. Observe that with this solution, you will not hit the SPM often at all and the problems that I talked about earlier are not an issue.

The update of the semi-static value will refresh the file and then do a new load to the LookupTable. Therefore the read can always assume that the cache is up-to-date. This is the code I use for a semi-static fetch when using a Lookup Table. You can see the results in Figure 15.

Private Function SemiStaticFetch() As String

With gobjLookup

.ReadLock

SemiStaticFetch = .LookupValue(KeyProducts)

.ReadUnlock

End With

End Function

Figure 15 Results for LookupTable

LookupTable

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

42

100%

55

98%

Semi-static read

79

100%

99

81%

I’m sure a lot of you will be happy when you see the results with the LookupTable. It works very well as you can see and the implementation of aging view makes it very usable in many scenarios.

USING THE GLOBAL INTERFACE TABLE

GIT stands for Global Interface Table and is a Win32 facility. You can store interface pointers in the GIT to make an object global for all threads in a COM+ application. The GIT will also automatically take care of inter-apartment marshaling if necessary. The GIT is not usable directly from Visual Basic, but Don Box (yes, he’s my hero too) has written a small DLL (vbgit.dll, Global Interface Table Helper) that makes it possible to use the GIT from VB. You can download it from www.develop.com/dbox/com/vbgit.

You can use the GIT for creating a singleton. The GIT is also handy for solving a problem with repeated unmarshaling of interface pointers for objects that use the FTM, but that is nothing you have to think about in Visual Basic.

This technique is a bit different from the others that I have tested in this article since it’s possible to store complete objects even if they have been built with Visual Basic. Therefore I create an extra class called CacheInGIT with a property get/let for the VAT and the products-string.

When an object is passed to the GIT, a long value (called a cookie) is returned and that value is the key for getting access to the object again. Observe that I have kind of a catch 22 here since that cookie must be cached so that all requests can find it. The solution I use here is to store that long value in the SPM, but also in a global variable. I am protecting the creation and registration of the object with a mutex so that I only get one instance in the GIT.

I tried to cache the whole object in a global variable after having fetched it from the GIT, but then I sometimes had stability problems. I received Error 8001010e. The application called an interface that was marshalled for a different thread. The problem didn’t occur all the time, but when it occurred, the COM+ Application had to be shut down. As I understand it (and I also discussed it with MS-friends), this technique should be possible to use and the throughput was about 15 times better than what I now show. Anyway, if it only works every second time, the result isn’t interesting to show.

(BTW, you will get the same error if you put a VB6-object in the SPM and try to use the objects from different threads, but in this case the problem is expected.) This is the code I use to create the object and store it in the GIT. I’m also saving a long value in the SPM so that I can get back to the object in the GIT.

Set obj = New CacheInGIT

obj.Products = ProductsGet

obj.VAT = VatGet

lngDw = githelplib.RegisterInterfaceInGlobal(obj)

spmProperty.Value = lngDw

Set obj = Nothing

In this code, I get a reference to the object from the GIT, use the object, and then destroy the reference.

Private Function StaticFetch() As Single

Dim obj As CacheInGIT

Set obj = githelplib.GetInterfaceFromGlobal(glngDw)

StaticFetch = obj.VAT

Set obj = Nothing

End Function

Another strange thing to note is that the first time a GIT test runs, there can be an enormous difference in the throughput between the different test clients. One can have 50,000 tx and the other four can have 130. If the test is executed again without a reboot, all the five clients will have something like 400 tx. I have decided to not show the first case in the results that you can see in Figure 16.

Figure 16 Results for GIT

GIT

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

0.7

100%

0.9

99%

Semi-static read

1.9

100%

2.4

98%

The reason for the comparatively low performance improvement here is probably the marshaling across apartments and also the blocking issues for the lookup itself. In my opinion the GIT is not interesting in scenarios similar to my simple tests. It is probably useful in other situations when the object won’t be as stressed as it was here, especially if the object has a complex structure.

USING COMMERCE.DICTIONARY

Yet another dictionary object is the Commerce.Dictionary found in Site Server Commerce Edition. This dictionary component is easy to use (see the code example below), and it is more competent than the LookupTable. Commerce.Dictionary has Any Apartment as the Threading Model.

This test is similar to the LookupTable test in how the problem with the reference to the object is solved, and how the object is marked as dirty. The biggest difference here is that the data is not fetched from a file, but from the database when the first thread is started.

It was hard to find information as to whether I needed to protect reading from and writing to this dictionary object with a mutex or whether that is taken care of internally and automatically. The throughput results indicate that this is taken care of automatically, and J.D. Meier at Microsoft confirmed my guess, thanks J.D.!

Observe in this code for the static fetch that the key-names look like properties on the object; vat is really a key. The results are shown in Figure 17.

Private Function StaticFetch() As Single

StaticFetch = gobjDictionary.vat

End Function

Figure 17 Results for Commerce.Dictionary

Commerce.Dictionary

No of transactions (1 CPU)

CPU-usage (1 CPU)

No of transactions (2 CPUs)

CPU-usage (2 CPUs)

Static read

8

100%

8

76%

Semi-static read

18

100%

18

74%

As a matter of fact, the extra CPU has a slightly negative impact, probably because of contention. The result isn’t that impressive, but Commerce Server is often using this dictionary component internally. Because of that, my guess is that this component will be developed in the future.

SUMMARY OF THE RESULTS

I’ll try to summarize my results. A picture says more than 1000 words, right?

Figure 18 compares the results for both the static read and the semi-static read with 1 CPU and 2 CPUs.

Figure 18. Transaction and CPU usage results for static read, 1 CPU

Figure 19. Transaction and CPU usage results for static read, 2 CPU

Figure 20. Transaction and CPU usage results for semi-static read, 1 CPU

Figure 21. Transaction and CPU usage results for semi-static read, 2 CPU

Figure 22 compares the 8 techniques.

Figure 22 Summary of techniques

Go to db server each time

SPM

Files

Global Vars

Lookup Table

GIT

Commerce.Dictionary

What to cache

State

State (not VB-objects)

State (and serialized objects)

State (not VB-objects)

State

Objects

State

Threading model

N/A

N/A

N/A

N/A

Any Apartment

Creates a thread neutral object

Any Apartment

Implementation detail

X

Not supported

X

A wrapper needed to use from VB

You need Site Server Commerce Ed

Built-in locking

X

X

X (I chose to use a mutex in my tests)

Typically not needed, one instance per thread

X

Typically not needed

X, automatic

Automatically realtime OK with farm for COM+ servers also for semi-static data

X

No contention because of several copies of the data

X

Need for holding a reference somewhere between requests

X

X

X

And Figure 23 summarizes my recommendation.

Figure 23 Recommendation summary

Go to db server each time

SPM

Files

Global Vars

Lookup Table

GIT

Commerce.

Dictionary

Per user data

X

Static data

OK if performance and scalability is good enough

X

X

X

X

Perhaps OK if complex structure

X

Semi-static data

X

X

X

No (because of several instances)

X

Perhaps OK if complex structure

X

Data seldomly read

X

Data often updated

X

Much data

X

No (because of several instances)

High isolation level needed/real time consistency

X

OTHER ALGORITHMS

In the semi-static tests, the fetch assumes that the cache is up-to-date because the update method refreshes the cache. A problem with this solution occurs when the update isn’t done through this update method (but directly in the database, for example) or when you need to use a farm of servers. Support for aging views in LookupTable is a good solution (if real time consistency isn’t needed) and it’s easy to build something similar, for example, for the SPM too.

Another approach could be to check before using the cache to see if it is up-to-date, for example by comparing timestamps between the database and the cache. The drawback here will be that the database still needs to be touched anyway.

Yet another solution could be to not put the cache on each machine in the farm but to have it on a dedicated machine and communicate with it by DCOM. This is usually not a very good idea since the communication cost will be high.

There are of course several other algorithms. Combine your requirements for real-time consistency with your imagination.

COMMON TRAPS

One common trap you can get into when trying to keep state at the server-side is using the VBScript dictionary object found in scrrun.dll. Unfortunately that component is not agile, and to make things even worse it has been marked as such in the installation process. The component isn’t thread safe which will give problems after a while when used in a context where thread safety is expected. (See msdn.microsoft.com/workshop/management/planning/MSDNchronicles2.asp for more information.)

Another common trap is trying to save Visual Basic objects in the SPM. Visual Basic objects have thread affinity which means that they can only be used by the thread that instantiated them. (Don’t you want to have Visual Basic .NET now?) When another thread tries to use the object, there will be a crash.

Remember that if you use configured transactions in COM+, you will work with SQL Server with the transaction isolation level set to Serializable. Then it’s not a good idea to accept a low consistency level for the cache if it’s used in a transaction!

OTHER PERFORMANCE IMPROVEMENT OPTIONS TO CONSIDER

Well, this is an enormous topic that I have only touched upon in this article. For example I haven’t talked much about using XML documents for the cached data. (Can you write an article these days without talking a lot about XML?)

Perhaps you wonder why you shouldn’t always transform

data to HTML and store the HTML-string in ASP.Application? That is certainly a very good solution, but there are scenarios when it’s not the correct way to go:

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist