A Desktop Performance Monitor






4.93/5 (11 votes)
Nov 1, 2002
6 min read

229955

4816
How to implement an Explorer Desktop Band that uses the Microsoft’s Performance Data Helper interface to display current performance data about activity such as memory, disk, and processor usage.
Introduction
The Windows performance monitor is a great tool for determining what is happening with the performance of a computer. I have always found the ability to monitor every aspect of a computer extremely useful and very powerful but wished that Microsoft’s performance monitor would dock to the task bar so I could continuously monitor my system without having to bring the window to the front. One day I decided to write it. This article will demonstrate how to implement an Explorer Desktop Band that uses the Microsoft’s Performance Data Helper interface to display current performance data about activity such as memory, disk, and processor usage.
Supported Platforms
Requires Internet Explorer 4.0 or greater and one of the following operating systems:
Windows NT 4.0
Windows 2000
Windows XP (Home and Pro)
Installation
To install the desktop performance monitor band you need to run regsvr32.exe on the IETools.dll. The IETools.dll can be downloaded or built from the source provided as part of this article. See links above.
Regsvr32.exe IETools.dll
Once the band object has been successfully registered you can add the toolbar to the taskbar by right clicking the task bar. Go to the Toolbars sub-menu and select the “Performance Monitor” option.
To remove the performance monitor unselect the “Performance Monitor” option on the toolbar sub-menu and run the following command on the IETools.dll.
Regsvr32 /U IETools.dll
Reboot the computer and then the IETools.dll can be deleted.
Creating the Band Object
A Desktop band is simply a COM object that implements certain interfaces and registers itself as belonging to specific component category. There are three interfaces every desktop band needs to implement:
The IDesktopBand
interface provides the desktop bands container
with information about the band object. IObjectWiteSite
interface
enables the communication between Desktop and container.
IPersistStream
is uses to store state so an object can be saved and
loaded.
Additionally the desktop band must also register itself as belonging to the
CATID_DeskBand
component category. This lets explorer know that
your object can be hosted in the taskbar and to add it to the list of available
toolbars in the taskbar’s context menu.
The COM object is created by running the ATL COM application wizard creating new COM server dll. Once that is done you derive you new band objects from the three interfaces mentioned above.
class ATL_NO_VTABLE CPerfBar : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CPerfBar, &CLSID_PerfBar>, public IDispatchImpl<IPerfBar, &IID_IPerfBar,&LIBID_IETOOLSLib>, public IObjectWithSite, public IPersistStream, public IDeskBand, public IContextMenu, public CWindowImpl<CPerfBar>
Next add the interfaces to ATL’s COM map
BEGIN_COM_MAP(CPerfBar) COM_INTERFACE_ENTRY ( IPerfBar ) COM_INTERFACE_ENTRY ( IDispatch ) COM_INTERFACE_ENTRY ( IObjectWithSite ) COM_INTERFACE_ENTRY ( IDeskBand ) COM_INTERFACE_ENTRY ( IPersist ) COM_INTERFACE_ENTRY ( IPersistStream ) COM_INTERFACE_ENTRY ( IDockingWindow ) COM_INTERFACE_ENTRY ( IOleWindow ) COM_INTERFACE_ENTRY ( IContextMenu ) END_COM_MAP()
Then we need to let explorer know about our band object by registering it in the component category.
BEGIN_CATEGORY_MAP( CPerfBar ) IMPLEMENTED_CATEGORY(CATID_DeskBand) END_CATEGORY_MAP()
Drawing the Performance Meter
To draw our performance meter the first thing we need to do is create a
window to draw on. ATL has a nice HWND
wrapper class named
CWindowImpl
that makes it very easy for an object to handle and
respond to Windows messages. To use it we first derive our band object from
CWindowImpl
.
class ATL_NO_VTABLE CPerfBar : … public CWindowImpl<CPerfBar>
To handle window message we must create a message map and message handlers for every message that will be handled. These are added to our desktop band class.
BEGIN_MSG_MAP( CPerfBar ) MESSAGE_HANDLER( WM_CREATE, OnCreate ) MESSAGE_HANDLER( WM_DESTROY, OnGoodBye ) MESSAGE_HANDLER( WM_PAINT, OnPaint ) MESSAGE_HANDLER( WM_ERASEBKGND, OnEraseBg ) END_MSG_MAP() LRESULT OnPaint ( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); LRESULT OnEraseBg( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); LRESULT OnCreate ( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); LRESULT OnGoodBye( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled );
Now we can implement our drawing routine. The performance data is stored as a percentage from 0 to 100 in a STL deque. The deque was chosen over a STL vector because it provides much faster insertions in the front.
typedef deque<FLOAT> PerfValueQ; typedef PerfValueQ::iterator PerfValueQIterator ; PerfValueQ m_qPerfValues;
The OnPaint
handler creates a memory device context to avoid
flickering during the drawing process. The meter is then drawn on to the memory
device context and finally the memory device context is BitBlt on to the screen.
LRESULT CPerfBar::OnPaint( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { PAINTSTRUCT ps = {0}; RECT rect = {0}; HDC hdcMem = NULL; HBITMAP hbmMem = NULL; HBITMAP hbmOld = NULL; BeginPaint( &ps ); GetClientRect( &rect ); hdcMem = CreateCompatibleDC( ps.hdc ); hbmMem = CreateCompatibleBitmap( ps.hdc, rect.right - rect.left, rect.bottom- rect.top); hbmOld = (HBITMAP)SelectObject(hdcMem, hbmMem); DrawBarMeter( hdcMem ); BitBlt( ps.hdc, rect.left, rect.top, rect.right-rect.left, rect.bottom-rect.top, hdcMem, 0, 0, SRCCOPY); SelectObject( hdcMem, hbmOld ); DeleteObject( hbmMem ); DeleteDC( hdcMem ); EndPaint( &ps); return 0; } VOID CPerfBar::DrawBarMeter( HDC hdc ) { INT barHeight = 0; RECT rect = {0}; HPEN hOldPen = NULL; PerfValueQIterator QIterator = m_qPerfValues.begin(); GetClientRect( &rect ); FillRect( hdc, &rect, m_backBrush ); hOldPen = (HPEN)SelectObject( hdc, m_forePen ); for ( ; rect.right >= rect.left; rect.right-- ) { if ( QIterator != m_qPerfValues.end() ) { barHeight = (INT)( (*QIterator) * ( rect.bottom - rect.top ) ); QIterator++; } else barHeight = 0; barHeight = barHeight < 2 ? 2 : barHeight; MoveToEx( hdc, rect.right, rect.bottom, NULL ); LineTo ( hdc, rect.right, rect.bottom - barHeight ); } m_qPerfValues.erase( QIterator, m_qPerfValues.end() ); SelectObject( hdc, hOldPen ); }
Band Object Persistence
Persistence in band objects is used for more than just saving the state of a band when shutting down Windows and reloading the state when you start back up. Every time the band object is undocked from the taskbar or desktop it is persisted to a stream and destroyed. Once it has been relocated to the new location a brand new instance of the band object is created and its state is loaded from the persisted stream.
Persistence in band objects is implemented through the
IPersistStream
interface. The IPersistStream
interface
provides methods for saving and loading objects in a stream. Initially the
stream requests the maximum size in bytes that is needed for the stream to save
the object. The GetSizeMax
method is called to request this
information.
STDMETHODIMP CPerfBar::GetSizeMax( ULARGE_INTEGER* pcbSize ) { if ( pcbSize == NULL ) return E_INVALIDARG; ULISet32( *pcbSize, sizeof( m_clrFore ) + sizeof( m_clrBack ) + sizeof( m_ThreadData.dwRefreshRate ) + sizeof( m_ThreadData.szCounterPath ) ); return S_OK; }
The object is then persisted by a call to the save method.
STDMETHODIMP CPerfBar::Save( LPSTREAM pStream, BOOL bClearDirty ) { HRESULT hr = S_OK; if ( FAILED( pStream->Write( &m_clrFore, sizeof(m_clrFore), NULL ) ) || FAILED( pStream->Write( &m_clrBack, sizeof(m_clrBack), NULL ) ) || FAILED( pStream->Write( &m_ThreadData.dwRefreshRate, sizeof(m_ThreadData.dwRefreshRate), NULL ) ) || FAILED( pStream->Write( m_ThreadData.szCounterPath, sizeof(m_ThreadData.szCounterPath), NULL ) ) ) { hr = STG_E_CANTSAVE; } else { if ( bClearDirty ) m_bDirty = FALSE; } return hr; }
The band object will then be destroyed while the user picks a new location to dock the band. Once the new location has been chosen a new band object is created and the state is reloaded.
STDMETHODIMP CPerfBar::Load( LPSTREAM pStream ) { HRESULT hr = S_OK; if ( FAILED( pStream->Read( &m_clrFore, sizeof(m_clrFore), NULL ) ) || FAILED( pStream->Read( &m_clrBack, sizeof(m_clrBack), NULL ) ) || FAILED( pStream->Read( &m_ThreadData.dwRefreshRate, sizeof(m_ThreadData.dwRefreshRate), NULL ) ) || FAILED( pStream->Read( m_ThreadData.szCounterPath, sizeof(m_ThreadData.szCounterPath), NULL ) ) ) { hr = E_FAIL; } return hr; }
Implementing IContextMenu
A nice feature is explorer allows band objects to add menu items to its context menu. This allows the performance monitor band object to add a command for opening up the options dialog so the user can change the properties of the band object.
This is implemented through the IContextMenu
interface which a band object must implement to add items to the context menus.
When the context menu is displayed explorer passes the band object a
HMENU
to add menu items to.
#define MENU_ITEMS_ADDED 2 STDMETHODIMP CPerfBar::QueryContextMenu( HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags ) { HRESULT hr = S_OK; if( CMF_DEFAULTONLY & uFlags ) hr = MAKE_HRESULT( SEVERITY_SUCCESS, 0, 0 ); else { TCHAR lptstrMenuString[MAX_STRINGTABLE] = {0}; LoadString( _Module.m_hInstResource, IDS_MI_CONFIGURE, lptstrMenuString, MAX_STRINGTABLE ); // Add a seperator InsertMenu( hMenu, indexMenu, MF_SEPARATOR | MF_BYPOSITION, idCmdFirst + IDM_SEPERATOR, 0 ); // Add the new menu item InsertMenu( hMenu, indexMenu, MF_STRING | MF_BYPOSITION, idCmdFirst + IDM_CONFIGURE, lptstrMenuString ); hr = MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, MENU_ITEMS_ADDED ); } return hr; }
The return value to the QueryContextMenu
function is a
successful HRESULT
with the number of items added to the context
menu place in the status code section.
The context menu is then displayed to the user. Band objects are notified if
the user selects a custom menu item by the InvokeCommand
method.
The performance monitor band launches a dialog to allow the user to change the
properties of the band.
STDMETHODIMP CPerfBar::InvokeCommand( LPCMINVOKECOMMANDINFO pici ) { HRESULT hr = S_OK; if ( HIWORD( pici->lpVerb ) != 0 ) hr = E_INVALIDARG; else { switch ( LOWORD( pici->lpVerb ) ) { case IDM_CONFIGURE: if ( m_dlgOptions.IsWindow() == FALSE ) { m_dlgOptions.SetCounter ( m_ThreadData.szCounterPath ); m_dlgOptions.SetBackgroundColor ( m_clrBack ); m_dlgOptions.SetForegroundColor ( m_clrFore ); m_dlgOptions.SetRefreshRate( m_ThreadData.dwRefreshRate ); m_dlgOptions.SetDialogParent( m_hWnd ); m_dlgOptions.Create ( m_hWnd ); m_dlgOptions.CenterWindow( GetDesktopWindow() ); m_dlgOptions.ShowWindow( SW_SHOW ); } else m_dlgOptions.SetFocus(); hr = S_OK; break; default: hr = E_INVALIDARG; } } return hr; }
Performance Monitoring
The Performance Data Helper (PDH) library is an API provided by Microsoft to retrieve real-time performance information. This library is only supported on the NT platforms so the performance monitor band will only function on the NT platform as well. The API for monitoring a source is fairly simple and Microsoft provides a way to add your own sources. To begin monitoring a source you need to open a query and create a counter.
class CPerfMon { public: CPerfMon( ); virtual ~CPerfMon(); BOOL Start( LPTSTR lpstrCounter ); VOID Stop(); LONG GetValue(); private: HQUERY m_hQuery; HCOUNTER m_hCounter; }; BOOL CPerfMon::Start( LPTSTR lpstrCounter ) { PDH_STATUS pdhStatus = ERROR_SUCCESS; Stop(); pdhStatus = PdhOpenQuery( NULL, 0, &m_hQuery ) ; if ( pdhStatus == ERROR_SUCCESS ) { pdhStatus = PdhValidatePath( lpstrCounter ); if ( pdhStatus == ERROR_SUCCESS ) { pdhStatus = PdhAddCounter( m_hQuery, lpstrCounter, 0, &m_hCounter ) ; } } ASSERT( pdhStatus == ERROR_SUCCESS ); if ( pdhStatus != ERROR_SUCCESS ) Stop(); return pdhStatus == ERROR_SUCCESS; }
The counter requires a path to a source which looks something like “\Processor(_Total)\% Processor Time”.
Once a performance counter has been successfully created, the current value
can be retrieve by a call to the PdhGetFormattedCounterValue
method.
LONG CPerfMon::GetValue() { PDH_STATUS pdhStatus = ERROR_SUCCESS; LONG nRetVal = 0; PDH_FMT_COUNTERVALUE pdhCounterValue; pdhStatus = PdhCollectQueryData( m_hQuery ); if ( pdhStatus == ERROR_SUCCESS ) { pdhStatus = PdhGetFormattedCounterValue( m_hCounter, PDH_FMT_LONG, NULL, &pdhCounterValue ); if ( pdhStatus == ERROR_SUCCESS ) nRetVal = pdhCounterValue.longValue; } return nRetVal; }
Cleanup of the allocated counters and queries is simple and can be accomplished by removing the counter and closing the query.
VOID CPerfMon::Stop() { if ( m_hCounter ) PdhRemoveCounter( m_hCounter ); if ( m_hQuery ) PdhCloseQuery ( m_hQuery ); m_hQuery = NULL; m_hCounter = NULL; }
The performance monitoring for the band object is handled by a separate thread that posts custom windows messages to the main window. The separate thread is used to break the monitoring of the system from the windows message loop. The main thread procedure takes a structure that contains all the information the thread needs to monitor the system and an extra Boolean value for performing synchronization between the thread and the main window.
typedef struct { HWND hWndNotify; // Window to post messages to LONG bContinue; // Set to false to cause the thread to stop // monitoring DWORD dwRefreshRate; // How often we get a new performance value TCHAR szCounterPath[ MAX_COUNTER_PATH ]; // what we are monitoring } PerfMonThreadData, *LPPERFMONTHREADDATA;
The thread loops so long as bContinue
is TRUE
.
While the thread loops, it performs the following functions:
- Retrieves a new performance value from the performance data helper API
- Posts the new performance value to the main window
- Sleeps for a predetermined timeout period
VOID PerfMonThreadProc( LPVOID lParam ) { LPPERFMONTHREADDATA lpData = (LPPERFMONTHREADDATA) lParam; INT nTime = 0; CPerfMon PerfMon; if ( PerfMon.Start( lpData->szCounterPath ) && IsWindow( lpData->hWndNotify ) ) { while ( InterlockedExchange( &lpData->bContinue, TRUE ) ) { PostMessage(lpData->hWndNotify, WM_ADDPERFVALUE, 0, PerfMon.GetValue()); nTime = ( nTime == 0 ) ? lpData->dwRefreshRate : lpData->dwRefreshRate - (GetTickCount() - nTime); if ( nTime > 0 ) { Sleep( nTime ); } nTime = GetTickCount(); } } PerfMon.Stop(); }
The main window simply waits for a message from the monitoring thread and handles them as it would any other window message. The new performance value is stuffed in the dequeue and the main window is invalidated.
LRESULT CPerfBar::OnNewPerformanceValue( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { m_qPerfValues.push_front( ((FLOAT)lParam / 100.0f ) ); Invalidate(); return 0; }
Further Enhancements
- Add different meter types (Line, Bar, Pie chart, Etc.).
- Allow for multiple meters to be displayed at once.
- Save history to a log file.
- Ability to kill a process from the context menu.
Reporting Defects and Suggestions
Please report all defects to me by email: mailto:[email protected]?subject=Desktop Performance Monitor
Please also feel free to submit suggestions and comments. Enjoy!