COM+ Call Timers: the unofficial way
Not satisfied with the official way, I went looking for a simpler way. The Component Services Console displays the data so the information must be there somewhere. It is a waste of time and resources me to collect the data and calculate the call times when the information I want is already available.
As it turns out the unofficial way is a lot simpler to implement than the official way.
Step 1: include the comsvc.dll library
// ComSvcs library for internal com+ method call tracking
#import "c:\windows\system32\comsvcs.dll" exclude("IAppDomainHelper")
Step 2: get the statistics
- Get an instance of the internal com+ tracker: COMSVCSLib::IGetAppDataPtr ptrAppData(_T("{ecabafb9-7f19-11d2-978e-0000f8757e2a}"));
- Get a list of applications and lop through them: ptrAppData->GetApps(&nAppData, &appData);
- Get the application data you are interested in: calls per second, total calls, etc
- Get a list of classes in the application and loop on them, extracting the information you want: ptrAppData->GetAppData(oneApp.m_idApp, &nClsIDs, &aClsidData);
- Get the application data you are interested in: calls per second, total calls, etc
Example
This simple routine builds a XML string the brutal way with the com+ performance information (most error handling code removed for clarity)
/*
Get the statistics from the hidden COM+ interface
*/
void CTracker::getStatistics(BSTR *output)
{
const unsigned long MAX_APP_DATA = 500;
const unsigned long PUSH_RATE = 1000;
unsigned long nAppData = MAX_APP_DATA;
LPSTR lpString;
LPWSTR lpwString;
COMSVCSLib::appData *aAppData;
CString csStatistics = "";
// Get an instance of the internal com+ tracker objet
COMSVCSLib::IGetAppDataPtr ptrAppData(_T("{ecabafb9-7f19-11d2-978e-0000f8757e2a}"));
csStatistics.Append("
// Step through the list of running application
ptrAppData->GetApps(&nAppData, &aAppData);
for (unsigned long idxApp=0; idxApp < nAppData; idxApp++)
{
unsigned long nClsIDs;
COMSVCSLib::CLSIDDATA *aClsidData;
COMSVCSLib::appData oneApp = aAppData[idxApp];
csStatistics.Append(_T("
UnicodeToAnsi(oneApp.m_szAppGuid, &lpString);
csStatistics.AppendFormat(_T("
CoTaskMemFree(lpString);
// Application information
csStatistics.AppendFormat(_T("
csStatistics.AppendFormat(_T("
COMSVCSLib::APPSTATISTICS appStatistics = oneApp.m_AppStatistics;
csStatistics.AppendFormat(_T("
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\r\n"));
// Get class information for this application
ptrAppData->GetAppData(oneApp.m_idApp, &nClsIDs, &aClsidData);
csStatistics.AppendFormat(_T("
for (unsigned long idxClass = 0 ; idxClass < nClsIDs; idxClass++)
{
COMSVCSLib::CLSIDDATA oneClass = aClsidData[idxClass];
csStatistics.AppendFormat(_T("
// Get the progID for the guid.
// This FAILS during recyling on Win2k3
if (!FAILED((ProgIDFromCLSID(oneClass.m_clsid,&lpwString))))
{
UnicodeToAnsi(lpwString, &lpString);
csStatistics.AppendFormat(_T("\t
CoTaskMemFree(lpString);
}
// Performance information
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\t
csStatistics.AppendFormat(_T("\r\n"));
}
CoTaskMemFree(aClsidData);
csStatistics.AppendFormat(_T("\r\n"));
csStatistics.AppendFormat(_T("\r\n"));
}
csStatistics.Append("\r\n");
CoTaskMemFree(aAppData);
*output = csStatistics.AllocSysString();
}
That's it!
Less than a 100 lines of code to build a XML document with the call times for the running COM+ applications on the system.
This approach as several benefits over the official way:
- It is a lot simpler to implement and maintain (100 lines of C++ vs 1.500)
- I have found no memory leaks after 9 months in production
- Less resource usage. COM+ already maintains the counters, I just get them when I need them.
Keep in mind that this is an undocumented API so it may change one day in the future (Longhorn?).
Thanks for the code.
ReplyDeleteExactly what I was looking for. I wanted it in C# though, so here's the C# version using two unsafe blocks (to call GetApps and GetAppData); compile with /unsafe option.
ReplyDeleteusing System;
using System.Runtime.InteropServices;
namespace ptdpc
{
/// <summary>
/// Summary description for ComMonitor.
/// </summary>
public class ComMonitor
{
static public void GetData()
{
// Get an instance of the internal com+ tracker objet
Type comPlusTrackerType = Type.GetTypeFromCLSID(new System.Guid("{ecabafb9-7f19-11d2-978e-0000f8757e2a}"));
COMSVCSLib.IGetAppData getAppData = (COMSVCSLib.IGetAppData)Activator.CreateInstance(comPlusTrackerType);
// Step through the list of running application
uint appCount;
IntPtr appDataPtr = new IntPtr();
unsafe
{
getAppData.GetApps(out appCount, new IntPtr(&appDataPtr));
}
int appDataSize = Marshal.SizeOf(typeof(COMSVCSLib.appData));
for(int appIndex=0; appIndex<appCount; appIndex++)
{
COMSVCSLib.appData appData = (COMSVCSLib.appData)
Marshal.PtrToStructure(new IntPtr(appDataPtr.ToInt32() + (appIndex * appDataSize)), typeof(COMSVCSLib.appData));
Console.WriteLine("AppID={0}", GetString(appData.m_szAppGuid));
// Application information
Console.Write("ID={0}", appData.m_idApp);
Console.Write(", PID={0}", appData.m_dwAppProcessId);
Console.WriteLine();
Console.Write("Calls/Sec={0}", appData.m_AppStatistics.m_cCallsPerSecond);
Console.Write(", Total Calls={0}", appData.m_AppStatistics.m_cTotalCalls);
Console.Write(", Class Count={0}", appData.m_AppStatistics.m_cTotalClasses);
Console.Write(", Instnc Count={0}", appData.m_AppStatistics.m_cTotalInstances);
Console.WriteLine();
uint nClsIDCount;
IntPtr clsIDDataPtr;
unsafe
{
getAppData.GetAppData(appData.m_idApp, out nClsIDCount, new IntPtr(&clsIDDataPtr));
}
int clsIDDataSize = Marshal.SizeOf(typeof(COMSVCSLib.CLSIDDATA));
for(int clsIDIndex=0; clsIDIndex<nClsIDCount; clsIDIndex++)
{
COMSVCSLib.CLSIDDATA clsIDData = (COMSVCSLib.CLSIDDATA)
Marshal.PtrToStructure(new IntPtr(clsIDDataPtr.ToInt32() + (clsIDIndex * clsIDDataSize)), typeof(COMSVCSLib.CLSIDDATA));
Console.WriteLine("\tCLSID={0}", clsIDData.m_clsid.ToString());
// Performance information
Console.Write("\tBound={0}", clsIDData.m_cBound);
Console.Write(", References={0}", clsIDData.m_cReferences);
Console.Write(", Pooled={0}", clsIDData.m_cPooled);
Console.WriteLine();
Console.Write("\tCallsCompleted={0}", clsIDData.m_cCallsCompleted);
Console.Write(", CallsFailed={0}", clsIDData.m_cCallsFailed);
Console.Write(", InCall={0}", clsIDData.m_cInCall);
Console.Write(", RespTime={0}", clsIDData.m_dwRespTime);
Console.WriteLine();
Console.WriteLine();
}
Marshal.FreeCoTaskMem(clsIDDataPtr);
}
Marshal.FreeCoTaskMem(appDataPtr);
}
static string GetString(ushort[] arr)
{
byte[] buf = new byte[arr.Length * 2];
for(int i=0; i<arr.Length; i++)
{
buf[i * 2] = (byte)(arr[i] & 0xFF);
buf[i * 2 + 1] = (byte)(arr[i] >> 8);
}
string s= System.Text.UnicodeEncoding.Unicode.GetString(buf);
return s;
}
}
}
Whoops, forgot to Release the getAppData COM object. Add this at the end:
ReplyDeleteMarshal.ReleaseComObject(getAppData);
Thanks for sharing the C# version!
ReplyDeleteIm trying to compile this c# source usingo this command line:
ReplyDeletecsc /r:\WINNT\system32\comsvcs.dll test.cs
But im getting this error:
fatal error CS0009: Metadata file 'c:\WINNT\system32\comsvcs.dll' could not be
opened -- 'There isn't metadata in the memory or stream'
does anyone could help me?
Egil Hogholt u seems to be an angel for us. You have solved my assignment. You are great.
ReplyDeleteThanks,
Rizwan
Renato ,
ReplyDeleteFirst of all you have to you use Microsoft "TlbImp.exe" tool to create "Interop.COMSVCSLib.dll" file from "comsvcs.dll" file.
Then u can compile with Visual Studio .NET 2003 Command Prompt like that:
csc /unsafe /reference:Interop.COMSVCSLib.dll /out:TestExe.exe test.cs
I hope it helps you clarify ur thoughts.
Regards,
Rizwan
Ultimus Inc.
Thanks, Muhammad... i tried to add a reference to COMSVCSLib with visual studio .net and it worked fine... and im using the toguether with COMAdminCatalog to get the application names...
ReplyDeleteBuuuuttt now there is a new question: im getting a few counters, like response time, with 4294967295 (full usigned long).... whats it mean? can you help me?
thanks...
Renato
Can you post the entire XML fragment of the counter?
ReplyDelete4294967295 is the unsigned value of -1 which I believe is an error value. Do you always see the value 4294967295 for the same counter or do you only see it once in a while, for example when a package is being recycled?
First, I want to thank Egil and Mark for sharing their great code. Mark's C# code is almost what I need.
ReplyDeleteI want to monitor call times on a series of remote servers where COM+ applications are installed. I found a few suggestions on the related thread ("How to get COM+ call times from VB and VBScript"), but I don't feel any of those would work for me. What I need is some way to connect to a remote server without doing anything on the server itself. After all, COM+ explorer is capable of connecting to remote servers and getting all information that you can see there for your local computer. When you use ComAdmin classes and want to connect to a remote server, one line of C#
new ComAdmin.ComAdminCatalog().Connect(servername);
(after you added a reference to comadmin.dll) brings you there. I expect to have something similar in comcvcs.dll, but I cannot find it.
Thank you for any suggestion,
Igor
Comsvcs is a COM object so you can call it on a remote machine using DCOM. Pass the server name you want to reach as an argument to GetTypeFromProgID() or GetTypeFromCLSID()
ReplyDeleteModify the C# code above like this:
Type comPlusTrackerType = Type.GetTypeFromCLSID(
new System.Guid("{ecabafb9-7f19-11d2-978e-0000f8757e2a}"),
"myserver");
Cool.
ReplyDeleteThanks for sharing the updated code.
This is great info. Exactly what I was looking for. Does anyone know of any issues using the COMPlusTrackerType on Windows 2000 Server? I have used ComAdmin.dll and had to import the type library separately for windows 2000 vs. 2003.
ReplyDeleteAny help is greatly appreciated.
I have not tested the code on Win2k. Let me know if you have problems, and I will test it on a Win2k virtual machine.
ReplyDeleteHi Egil. Thanks for the reply.
ReplyDeleteAfter submitting my post I went ahead and tested it on Win2k. XP & 2003 work with no issues but when attempting to run on Win2k I'm getting the following error "Value cannot be null. Parameter name: type"
Thanks Egil. Just to make sure you are aware, I used the c# code under .Net framework 1.1 on all 3 platforms.
ReplyDeleteGreat code, thanks for sharing.
ReplyDeleteI ran the original code and all I get is a list (incomplete) of the components in the "System applications" application.
ptrAppData->GetApps(&nAppData, &aAppData) return just that. I thought about a permissioning issue but using COMAdmin in C# lists all my applications...
Any hint ?
Thanks again for sharing.
The code only returns the objects that are active. COMAdmin on the other hand returns all configured applications.
ReplyDeleteAh, I see... Thanks for replying me.
ReplyDeleteActive means at least a component in the application is running I guess. I want to check for components that breaks and have an high call time, but I guess if the parent application is not there it means that any component is not running hence it's ok so far....
Great code again !
This is good stuff. I'm new to COM, but I was able to get the C++ code working on my local computer. Now I'd like to get it working remotely, similar to the way the COM+ MMC lets you see apps on other servers. I believe this may be easier to do using the C# example, but I need something that I can call from VB6. Any suggestions?
ReplyDeleteMany Thanks!
Thanks for this great solution.
ReplyDeleteI have this scenario:
I've a Com+ application made by a lot of com objects. I'd like to track the shutdown of this application, particularly I'd like to log the call time of the objects before the shutdown, so I'm thinking about the IComAppEvents implementation, in particulary the OnApp(forced)shutdown, in wich I'm thinking to invoke the getStatistics of ComTracker. I've read about COM+ event, but I've not understood how to apply it to the IComAppEvents. Any suggestion?
Hi Egilh... what an useful stuff... thanx a lot....
ReplyDeleteI've deployed it as windows application... I wraped this code into a function which returns me an arraylist with all the COM+ stats in local/remote machine...it works fine as windows app... but the problem occured when I tried to make it as C# class library... when I tried to build its throwing following error...
"Assembly generation failed -- Referenced assembly 'Interop.COMSVCSLib' does not have a strong name"
can u please help me on this...
Hi Egil,
ReplyDeletethnx a lot for ur previous reply 'n sry for delayed response.
I have this scenario:
I have implemented this (getting COM statistics) as a COM+ component (a .net class library-C#, registered to COM+) along with few other methods to get the list of all configured COM+ applications on a remote server and to start & stop selected COM+ application. 'm calling these methods from a
coldfusion (.cfm) page, by creating an instance of that component.
Everything worked fine when I deployed it on XP.
But when I deployed it on Windows 2003 server, the object count of this component kept on increasing on every method call and never reduced till I did shut down / restart COM+ application.
I'm releasing every object / comobject 'm creating in the class (COM+ component)
using marshal.releasecomobject() in finally block of each method, I even tried forcing GC in the finally block.
'm releasing instance of the component in the coldfusion page also.
it is COM+ 1.5 'm using, there is version difference in Component Services of XP machine (Version: 03.00.00.4414) and Windows 2003 machine (Version: 2001.12.4720.1830).
'm not able figure out the cause for this increase in object count.
Is there anything that holds the reference of these com objects, which has to be taken care of??
'n one more thing I have to mention here,
I've created interops (RCW) for COMSVCS.dll and COMADMIN.dll using "tlbimp" and 've put these in GAC,
other wise coldfusion is unable to execute those methods.
can u please help me on this..........
If I have understood correctly; coldfusion calls your component registered in COM+ and the object count of this component keeps rising.
ReplyDeleteTry to add these two calls in the finally block of the methods registered in COM+:
ContextUtil.DeactivateOnReturn = true;
ContextUtil.SetComplete();
It tells COM+ that your component is done so it can deactivate your component.
Hi !
ReplyDeleteI was looking over Mark's code and I think that would be what I need... but I need it in VB.NET, do you think the convertion is possible ? I've been trying to translate it, and the bug I'm at is on that line :
Dim comPlusTrackerType As Type
comPlusTrackerType = Type.GetTypeFromCLSID(New System.Guid("{ecabafb9-7f19-11d2-978e-0000f8757e2a}"))
Dim getAppData As COMSVCSLib.IGetAppData
getAppData = CType(Activator.CreateInstance(comPlusTrackerType), COMSVCSLib.IGetAppData)
Dim appCount As System.UInt32
Dim appDataPtr As IntPtr
Dim aAppData As IntPtr
aAppData = IntPtr.Zero
==> getAppData.GetApps(appCount, aAppData)
I get the following error message :
An unhandled exception of type 'System.Runtime.InteropServices.COMException' occurred in comtrackervbnet.dll
Additional information: A null reference pointer was passed to the stub.
can you help me out ? I don't quite get how those objects work ...
I think I found out what's wrong (there are a lot of things but...) the main problem is on that part
ReplyDeleteDim appDataPtr As IntPtr
Dim pAppData() As comsvcslib.CAppData
Dim aAppData As New IntPtr
Dim gh As GCHandle = GCHandle.Alloc(aAppData, GCHandleType.Pinned)
Dim AddrOfaAppData As IntPtr = gh.AddrOfPinnedObject()
getAppData.GetApps(appCount, AddrOfaAppData)
I got it into the debugger, and ... well... normally the value of aAppData should be modified after the GetApps call, but it is not, and it stays 0 .... so basically the Marshalling is done with a pointer that starts at 0 in the memory and Marshal.PtrToStructure is converting "garbagelike" memory to the apps/classes, that's why I don't get any info ...
any idea on how to modify it to work ? (at least I saved you the trouble of finding this problem.... ahah)
This code works fine in C#
ReplyDeleteunsafe
{
IntPtr intPtrAppDataPtr = new IntPtr(&appDataPtr);
getAppData.GetApps(out appCount, intPtrAppDataPtr);
}
The VB-like code doesn't work in C# either:
GCHandle gh = GCHandle.Alloc(appDataPtr, GCHandleType.Pinned);
IntPtr intPtrAppDataPtr = gh.AddrOfPinnedObject();
getAppData.GetApps(out appCount, intPtrAppDataPtr);
I will have a look this weekend and let you know what I find
Hi Pete.
ReplyDeleteI think I found the problem:
Dim appCount As System.UInt32
Dim appDataPtr As New IntPtr
Dim gh As GCHandle = GCHandle.Alloc(appDataPtr, GCHandleType.Pinned)
Dim intPtrAppDataPtr As IntPtr = gh.AddrOfPinnedObject()
getAppData.GetApps(appCount, intPtrAppDataPtr)
'This is the magic line: it reads that data back from the pinned pointer
appDataPtr = New IntPtr(Marshal.ReadInt32(intPtrAppDataPtr))
PS : I don't know if you can, but I would delete the post I did with the code that doesn't work, just so no one gets fooled... hehe
ReplyDeleteI have deleted the old VB.NET code that didn't work.
ReplyDeleteThanks for sharing the code.
Hello Egil !
ReplyDeleteit's been quite a while since I posted, but I was wondering something about the code of this function....
What components does it check exactly ?
Does it check all the components and packages under the COM+ Applications folder in the Component Services window ?
Or is it only then ones under the System Applications package ?
Or is it only the components that there are data available ?
I'm having people that want to know the package name of the classes that are getting call times over a set threshold... so I need to change the DLL I think... do I ?
Are there definitions for the counters: Bound, Calls Completed, Calls Failed, In Call, Pooled, References, Response Times. Especially Bound and References. I am being tasked with monitoring our COM+ apps and I'm entirely clear what each counter is and where to establish thrsholds.
ReplyDeleteHi,
ReplyDeleteI have a problem on getting com+ information on remote computer (UATSERVER1) using this command.
Set oTracker = CreateObject("egilh.ComTracker", "UATSERVER1")
It shows the following error.
Script: C:\Backup\Script\test.vbs
Line: 8
Char: 1
Error: ActiveX component can't create object: 'Egilh.ComTracker'
Code: 800A01AD
Source: Microsoft VBScript runtime error
I've already registered the egilhComTracker.dll on both client machine and on server (UATSERVER1). Calling the above command at the server itself is OK but failed on calling from other computer. Please help.
Best Regards,
Noppadon S.
A couple of questions:
ReplyDelete- which operating systems are you using on the machines
- how did you register the component?
My guess is that you cannot create the object because of security settings. DCOM settings are stricter in Win2k3 than Win2k for example so everything works locally but you encounter problems when you try to create an object remotely
Hi,
ReplyDelete- The server (UATSERVER1) is Windows 2000 Advanced Server and the client trying to call the component from server is Windows XP. (creating object locally at server is OK but creating server's object remotely from client is failed)
- I'm using regsvr32 D:\script\egilhComTracker.dll command to register the component.
Which security setting should I check?
Thank you very much.
You can review the security settings by running dcomcnfg.exe on the server.
ReplyDeleteI don't have access to a win2k machine, but if I remember correctly there are "Access Permissions" and "Launch and Activation Permissions.
The account that creates the component on the XP machine must have the rights to launch and access the component on the server.
Hi,
ReplyDeleteI am looking for something in VB, also looking for to monitor on the remote system, without installing anything on the server.
Is this possible, or are there any ways to do the same.
Eagerly waiting for the reply.
If anyone know do forward me the way.
Thanks,
Chandresh
You can do this in VB.NET but not in older versions of VB. Just create the comsvcslib.IGetAppData object on the remote machine and you should be OK.
ReplyDeleteI can update the com tracker to support getting data from remote machines if you think it would be useful.
Thanks for your reply,
ReplyDeleteI am looking for something, but not too sure how to begin,
I wanted to have the entire configuration information of the com & dcom component & a way to restore them back.
I am not sure if someone is changing the same.
Is this possible to track and record them.
I am new to the field of programming.
Thanks once again for all your help and support.
Hi Egil,
ReplyDeletecan you provide information on what the reported values mean. For example the response time. Is it in ms, is it a rolling average what is the sample size/interval?
Thank you,
Oz
The reported values are the same as the ones you see in the COM+ admin console. The values are in milliseconds. I have not investigated how the admin console calculates the values but I guess it shows the average call time of the currently executing calls.
ReplyDeleteHi Egil,
ReplyDeleteWe've been using this functionality successfully for a while, but when we've ported it to 2008 / 64 bit (from 2003 32 bit), we're experiencing some odd behavior.
Occasionally the ptrAppData->GetApps () call will return information (without an error) saying that there aren't any apps running (even though there are). Restarting my app won't correct this issue, but the issue eventually goes away on its own.
So, my questions are:
have you ported the code that uses this functionality to either Server 2008 or a 64bit platform?
have you seen this behavior?
Do you have any idea what could be causing this issue?
Any thoughts on how we can access the data that the dll is accessing without going through the dll?
Thanks for any information you can provide.
-Mark
Hi Mark.
ReplyDeleteNo, I am afraid I haven't tried it on Server 2008 or a 64 bit platform.
I moved to Mac and Linux a while back so I don't even have access to a 64 bit Windows machine to try it on.
Strange that it works some times but not always. Have you identified when it happens? When apps are being recycled, every X hours, etc?
This was an undocumented API so it is possible/likely that Microsoft has changed the way the DLL works.
The timing is really odd, as there seems to be a vague correlation with starting a backup process (the failure tends to start within a few minutes of the backup starting, but only about 10% of the time).
ReplyDeleteI'm currently looking at the IGetAppTrackerData object, as it seems to use the same DLL, but the information isn't quite the same. On the other hand, it is documented, and hopefully supported.
Thanks for getting back to me.
-Mark
I agree, it's better to use the IGetAppTrackerData API now that there is documented and supported API.
ReplyDeleteI hope the new API is simpler to work with than the old version (http://blog.egilh.com/2005/02/552aspx.html)
mersin
ReplyDeleteadana
aksaray
ankara
antalya
İTİ