The C++ framework for developing highly scalable, high performance servers on Windows platforms.

Example Servers - Echo Server Perf

This example shows you how to add performance monitoring to a server using the JetByte PerfMon Tools Library. A server that exposes performance counters in this way can be monitored using the standard Windows perfmon tool. The basic structure is very similar to the Basic Echo Server example and you should go and read about that first and have a good understanding of how everything fits together. This document will only cover the differences between the Basic Echo Server example and this example.

This example requires the "Peformance Counters" licensing option of The Server Framework and it requires libraries that only ship with that option (see here for licensing options). You can always download the latest version of this example from here; and although you will need the correct libraries to be able to build it you can look at the example code and see how it works and perhaps get ideas from it. A compiled, unicode release, build of this example is available on request if you require it for performance analysis of the framework.

Applications that use performance counters require a stand alone DLL that interfaces with the perfmon tool. The example includes such a DLL and the server project will build the DLL when the server is built.

The ServerMain.cpp file has some additional code to allow us to register and unregister the performance counters with the system. You only need to register the counters once and from then on they're available for the server to use. You need to have the appropriate access rights to register and use performance counters.

             if (commandLine.RegisterCounters())
             {
                if (CPerformanceCounters::InstallCounters())
                {
                   OutputEx(_T("Counters installed"));
                }
                else
                {
                   OutputEx(_T("Counters already installed"));
                }
             }
             else if (commandLine.UnRegisterCounters())
             {
                if (CPerformanceCounters::RemoveCounters())
                {
                   OutputEx(_T("Counters Removed"));
                }
                else
                {
                   OutputEx(_T("Counters not installed"));
                }
             }

Next we create an instance of our CPerformanceCounters object. We pass in some security attributes that will be used to restrict who can access the shared memory that is used to communicate between the server application and the counter dll. In effect the security descriptor can be used to secure the communication channel between the application and the perfmon application; you'll probably want to change the default security descriptor that we use here.

The structure of our CPerformanceCounters class follows a pattern that we've found works rather well. You'll notice that, in addition to the security attributes, we pass in true to the constructor. This determines if we attempt to enable performance counters at all and, in a real server, could be externally configured to allow for running the server either with or without performance monitoring enabled. Why might you want to run without monitoring? Well, to create the shared memory that the library requires for communication you need to have the SE_CREATE_GLOBAL_NAME privilege on your account, not having this privilege will prevent you from running the server if you attempt to enable performance counters, so by allowing the server to be configured to run without counters you may make the server easier to develop and debug in some circumstances.

                CSecurityDescriptorAllowAll sd;

                CSecurityAttributes sa(sd);

                CSharedCriticalSection lockFactory(
                   commandLine.NumberOfLocks());

                CPerformanceCounters counters(true, sa);

Once we have created our counters we pass the object to the things that we want to monitor. Many of the classes used include built in support for monitoring via specific interfaces, so our CPerformanceCounters object actually does most of its work via these interfaces by inheriting from them and implementing them. We implement the following monitoring interfaces JetByteTools::IO::IMonitorBufferAllocation, JetByteTools::IO::IMonitorIOPool and JetByteTools::Socket::IMonitorSocketAllocation. We also provide three monitoring functions that the server uses directly.

                CIOPool pool(
                   counters,
                   commandLine.NumberOfIOThreads());

                pool.Start();

                CSequencedStreamSocketAllocator socketAllocator(
                   counters,
                   lockFactory,
                   commandLine.SocketPoolSize());

                CBufferAllocator bufferAllocator(
                   counters,
                   commandLine.BufferSize(),
                   commandLine.BufferPoolSize());

                const CFullAddress address(
                   commandLine.Server(),
                   commandLine.Port());

                const ListenBacklog listenBacklog = commandLine.ListenBacklog();

                CConnectionLimiter connectionLimiter(commandLine.MaxConnections());

                CSocketServer server(
                   counters,
                   commandLine.NoWelcomeMessage() ? "" : "Welcome to echo server\r\n",
                   address,
                   listenBacklog,
                   pool,
                   socketAllocator,
                   bufferAllocator,
                   connectionLimiter);

As you can see from our CSocketServer implementation, manipulating the counters is straight forward.

 void CSocketServer::OnConnectionEstablished(
    IStreamSocket &socket,
    const IAddress & /*address*/)
 {
    m_counters.OnConnectionStarted();

    Output(_T("OnConnectionEstablished"));

    socket.Write(m_welcomeMessage.c_str(), GetStringLengthAsDWORD(m_welcomeMessage));

    socket.Read();
 }

 void CSocketServer::OnConnectionClosed(
    IStreamSocket & /*socket*/)
 {
    m_counters.OnConnectionComplete();

    Output(_T("OnConnectionClosed"));
 }

These call result in the following operations inside our CPerformanceCounters object:

 void CPerformanceCounters::OnConnectionStarted()
 {
    if (m_pCounters)
    {
       m_pCounters->IncrementPerformanceCounter32(m_connectionsActiveCounter);
    }
 }

 void CPerformanceCounters::OnConnectionComplete()
 {
    if (m_pCounters)
    {
       m_pCounters->DecrementPerformanceCounter32(m_connectionsActiveCounter);
    }
 }

To understand what the above code is doing we need to look at how our CPerformanceCounter object is constructed.

The object contains an instance of a CPerformanceDataSchema class. This class is responsible for setting up the 'shape' of the performance counters that we expose. The schema is used in the application, to manipulate counters via the JetByteTools::PerfMon::CPerformanceCounter class and in the monitoring DLL to retrieve data via the JetByteTools::PerfMon::CPerformanceDataCollector class.

When the schema is constructed it builds and validates the objects and counters that your application will expose. It is here that we could add multiple language support if we wanted to by calling AddSupportedLanguage(), etc.

 CPerformanceDataSchema::CPerformanceDataSchema()
 {
    Object &exeObject = AddObject(
       s_objectName,
       _T("The \"JetByte Echo Server\" object type defines ")
       _T("counters that apply to the Echo Server process as ")
       _T("a whole. "),
       DetailLevelNovice);

    exeObject.AddCounter(
       s_bytesProcessedCounter,
       _T("The total number of bytes processed"),
       DetailLevelNovice,
       -6,
       CounterTypeCounter64);

    exeObject.SetDefaultCounter();

    exeObject.AddCounter(
       s_bytesPerSecondCounter,
       _T("The number of bytes processed per second"),
       DetailLevelNovice,
       -4,
       CounterTypePerSec);

The object and counter names are represented by static member variables on the schema object so that other parts of the system can interact with the schema using the names. Once the schema is constructed it can be used by the CPerformanceCounters class to access indexes which are then used to access the performance counters directly.

 CPerformanceCounters::CPerformanceCounters(
    const bool createCounters,
    CSecurityAttributes &sa)
    :  m_pCounters(CreateCountersIfRequired(createCounters, s_applicationName, m_schema, sa)),
       m_object(m_pCounters ? m_pCounters->GetPerformanceObject(m_schema.s_objectName) : 0),
       m_bytesProcessedCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_bytesProcessedCounter) : 0),
       m_bytesPerSecondCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_bytesPerSecondCounter) : 0),
       m_totalBuffersCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_totalBuffersCounter) : 0),
       m_buffersInUseCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_buffersInUseCounter) : 0),
       m_connectionsActiveCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_connectionsActiveCounter) : 0),
       m_totalSocketsCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_totalSocketsCounter) : 0),
       m_socketsInUseCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_socketsInUseCounter) : 0),
       m_ioThreadsActiveCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_ioThreadsActiveCounter) : 0),
       m_ioThreadsProcessingCounter(m_pCounters ? m_pCounters->GetPerformanceCounter(m_object, m_schema.s_ioThreadsProcessingCounter) : 0)
 {
 }

If we have created counters then we look up the indexes used to access the objects and counters that we want to interact with. Once we have the indexes then access to the counters is very fast as no lookups are required. The above code should probably be made neater by using the null object pattern when we don't wish to enable counters...

The final part of the CPerformanceCounters that is of interest is the code required for installation and removal of the counters.

Prior to this release of the library we used code that was based on some code from "Programming Server Side Applications" by Jeffrey Richter. This directly manipulated the Windows Registry to install the counters and this hasn't been the 'approved' way of installing counters for some time. The official way to add counters is to follow these instructions. But that's not that convenient as you need to have a couple of files laying around that aren't part of your actual counter implementation and you need to keep these in sync with any changes that you make to your counters and, well, the other way just worked... Well, it did work until Windows Vista, at which point it was no longer possible to update the registry values directly. Because of this, and several other issues, we decided to rewrite the library from scratch and include programatic support for the approved method of counter installation. To that end we have a JetByteTools::PerfMon::CPerformanceCounterInstaller class which installs counters in the approved fashion using LoadPerfCounterTextStrings() (it also allows for easy installation of side by side x64 and x86 counter dlls, and reports errors nicely...). This class needs to work with a symbol header file and an .ini file. The CPerformanceDataSchemaFileExporter class takes care of generating these files automatically from a CPerformanceDataSchema object, and cleans them up once the object goes out of scope. The schema has a checksum associated with it and this can be stored in the registry when you register your counters. This allows the monitoring dll to validate that the correct counters are installed and helps prevent the installed counters becoming out of sync with the compiled counters (this can often happen during development when you may be adding new counters on a regular basis).



 bool CPerformanceCounters::InstallCounters()
 {
    bool installed = false;

    CPerformanceCounterInstaller installer;

    if (!installer.CountersAreInstalled(s_applicationName))
    {
       const _tstring counterPath = GetModulePathName() + _T("\\EchoServerPerfCounters.dll");

       CPerformanceDataSchema schema;

       CPerformanceDataSchemaFileExporter exporter;

       installer.Install(
          s_applicationName,
          counterPath,
          false,
          exporter.ExportHeaderFile(s_applicationName, schema),
          exporter.ExportIniFile(s_applicationName, schema),
          _T(""),
          10000,
          10000,
          schema.GetChecksum());

       installed = true;
    }

    return installed;
 }

 bool CPerformanceCounters::RemoveCounters()
 {
    bool removed = false;

    CPerformanceCounterInstaller installer;

    if (installer.CountersAreInstalled(s_applicationName))
    {
       installer.Uninstall(s_applicationName);

       removed = true;
    }

    return removed;
 }

That's pretty much all there is to adding performance monitoring to a server.

Constructing the accompanying DLL is mostly a case of boiler-plate code and I expect this will be simplified in future releases of the library. The DLL's DLLMain() is provided by the library and to interface with it you must provide an implementation of this function:

 ICollectPerformanceData *CreatePerformanceDataCollector()
 {
    CSEHException::Translator sehTranslator;

    try
    {
       return new CPerformanceDataCollector(s_applicationName);
    }
    catch(CException &e)
    {
       const _tstring message = _T("CreatePerformanceDataCollector() -  Exception: ") + e.GetDetails();

       OutputDebugString(message.c_str());
    }
    catch(CSEHException &e)
    {
       const _tstring message = _T("CreatePerformanceDataCollector() -  Exception: ") + e.GetDetails();

       OutputDebugString(message.c_str());
    }
    catch(...)
    {
       OutputDebugString(_T("CreatePerformanceDataCollector() - Unexpected exception"));
    }

    return 0;
 }

Which we do in EchoServerPerfCountersDLL.cpp. All this does is carefully create an instance of our CPerformanceDataCollector class and return it to the boiler-plate infrastructure that will do the work for us.

Our collector implements JetByteTools::PerfMon::ICollectPerformanceData and does so by simply passing the calls on to its JetByteTools::PerfMon::CPerformanceDataCollector member variable. Our custom CPerformanceDataCollector exists solely to tie together the security descriptor and schema needed by the library class, and to provide some optional debug tracing from the calls that will be made into the DLL by perfmon

Generated on Sun Sep 12 19:06:45 2021 for The Server Framework - v7.4 by doxygen 1.5.3