Inconsistencies in wincache statistics page explained

Problem description

 

Many customers running WinCache reported seeing different cache statistics on consecutive requests to wincache.php. Specifically, cache uptime and opcode cache summary returned second time by wincache.php was completely different than the data returned first time. Occasionally data shown in the opcode cache summary table didn’t match the chart displayed on its side. Many customers correctly guessed that this happens because there are multiple cache instances present in different php-cgi processes but thought that none of the PHP processes are sharing cache data and were worried about excessive memory usage by WinCache. I have been replying to many customers personally and posting on forums explaining why they see this behavior but as more and more customers are using WinCache, this question is becoming more common. Ruslan who is PM for WinCache suggested writing a blog post to help customers understand the reason. So here you go.

 

Detailed explanation

 

WinCache creates many memory segments which are shared among all the php-cgi processes launched by a particular IIS worker process. For simplicity sake I will use the term “all php processes” to refer to “all php processes launched by one IIS worker process”. In version 1.0, WinCache creates 5 shared memory segments. Mostly these segments are shared successfully among all the php processes. In fact 4 shared segments are always shared but memory segment for opcode cache is sometimes not shared. Reason for this is that 4 of the 5 memory segments store data structures which are completely defined by us. Anytime we need to store a pointer in the shared segment which points to some other data in the shared segment, we define pointer as simple integer which is then set to offset from start of memory segment. This way other processes can reach the data pointed by this pointer even when absolute address of the data might be different. In contrast, data stored in opcode cache is defined by the PHP engine and include many absolute pointers. These pointer values cannot be changed to offsets without incurring a very heavy performance penalty which will totally defy the purpose of WinCache. For this reason, we try to map the global opcode cache segment at same address in all processes using MapViewOfFileEx which can possibly fail if the address is already occupied. All other caches doesn’t have this restriction and are mapped anywhere which mostly succeed. If we fail to map the opcode cache at a particular address, we create another opcode cache in processs local memory so that requests processed by this process are also very fast. Typically this happens for 5%-10% of php processes. Below figures illustrates state of the caches in a normal php process (figure 1) and in a process which ends up creating a local cache (figure 2). For more information on these caches, read my previous blog.

 

Local Cache Explanation

Now that you understand this design, all the anomalies seen in responses of wincache.php can be explained. If the process which processes request to wincache.php has a local cache, opcode cache data will be different from the one shown when request is handled by a process which is using global opcode cache. Also since cache uptime in wincache.php is picked from opcode cache (ocache_fileinfo), it will be age of either global opcode cache or local opcode cache. Reason why chart sometimes didn’t match opcode cache summary is because request to draw the chart is sent separately. If the chart draw request reaches a process which has different cache structure than the one which returned summary data, there can be a mismatch.

 

Improvements in WinCache 1.0.1

 

In WinCache 1.0.1, we have made few changes to wincache.php which will make all this a little less confusing. Since cache uptime can be different for file cache and opcode cache, we have moved the cache uptime field to opcode cache and file cache summary tables. So you will now see two cache uptimes. File cache uptime will reflect age of global file cache and opcode cache uptime will reflect age of global or local opcode cache. We also added cache scope property in opcode cache summary table which will be either “global” or “local”. This will indicate if the opcode cache data you are looking at is for global cache or a local cache. To fix the chart discrepancy, we now send data required to draw the chart in the request itself so that chart always matches the data in the summary table. All these changes were made based on feedback from you. So keep sending your feature requests and feedback so that we can keep improving WinCache.

 

Hope this helps.

Kanwal

Using windows live as SMTP relay server

Today I started writing a program to notify me when IP assigned to my router by my broadband provider changes. I wanted this program to check IP address of the router every few minutes and send me email if it detects that IP is changed. For sending email, I used System.Net.Mail.SmtpClient which requires a smtp host. Instead of running a smtp server on my machine, I started looking for free smtp relay servers available on the internet. I was surprised to find out that there are plenty of them available. In fact most of the popular email service providers like windows live, yahoo, gmail offer smtp relay services. You can read more about smtp relay servers and get a list of freely available smtp relay servers here.

Below is the code I used to send email notification using windows live smtp relay. If you want to use gmail, you can use "smtp.gmail.com" as the smtphost with gmail credentials.

static
bool SendEmailNotification(string newIpAddress)
{
    bool success = false;

    //
    // Send email using live account
    //
    SmtpClient mailClient = new SmtpClient("smtp.live.com", 587);
    mailClient.UseDefaultCredentials = false;
    mailClient.EnableSsl = true;

    MailMessage message = new MailMessage("WindowsLiveUserName@live.com", destination@domain);
    message.Subject = "Require immediate action – IP address change";
    message.Body = "New IP address is " + newIpAddress;
    message.Priority = MailPriority.High;

    NetworkCredential credentials =
       
new NetworkCredential("WindowsLiveUserName@live.com", "YourPassword", "");
    mailClient.Credentials = credentials;

    try
    {
        mailClient.Send(message);
        success = true;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }

    return success;
} 

 

I am still making some changes to my IpChangeMailer program. I will make it available for download once it is complete.

Thanks.
Kanwal

WaitForAllObjects to wait on more than MAXIMUM_WAIT_OBJECTS handles

One of the painful limitations of WaitForMultipleObjects function is that you can use it to wait for a maximum of MAXIMUM_WAIT_OBJECTS number of object handles. Value of MAXIMUM_WAIT_OBJECTS is 64. If you need to wait on more than MAXIMUM_WAIT_OBJECTS handles, you can either create a separate thread to wait on MAXIMUM_WAIT_OBJECTS and then do a wait on these threads to finish. Using this method you can create MAXIMUM_WAIT_OBJECTS threads each of those can wait for MAXIMUM_WAIT_OBJECTS object handles. Few months ago I needed code for doing this and couldn't find it on the internet. Here is how I implemented WaitForAllObjects.

typedef
struct _WAIT_THREAD_INFORMATION
{
    DWORD      dwWaitObjectCount;
    PHANDLE    pWaitHandles;
    DWORD      dwWaitMSeconds;
} WAIT_THREAD_INFORMATION, *PWAIT_THREAD_INFORMATION;

VOID
WaitForAllObjects(
    DWORD     dwWaitHandleCount,
    HANDLE *  pWaitHandles,
    DWORD     dwMaxWaitMSeconds
)
{
    HANDLE *                  pThreadWaitHandles    = NULL;
    WAIT_THREAD_INFORMATION * pWaitInformationArray = NULL;

    DWORD                     dwThreadCount         = 0;
    DWORD                     dwLastWaitCount       = 0;
    DWORD                     dwWaitHandleIndex     = 0;
    DWORD                     dwMaximumWaitObjects  = MAXIMUM_WAIT_OBJECTS;

    ASSERT( dwWaitHandleCount    >  0 );
    ASSERT( pWaitHandles         != NULL );
    ASSERT( dwMaxWaitMSeconds    >= 1000 );
    ASSERT( dwMaximumWaitObjects <= MAXIMUM_WAIT_OBJECTS );

    if( dwWaitHandleCount <= dwMaximumWaitObjects )
    {
        WaitForMultipleObjects( dwWaitHandleCount,
                                pWaitHandles,
                                TRUE,
                                dwMaxWaitMSeconds );
    }
    else
    {
        //
        // Create separate threads to wait on maximum wait objects
        // and then make this thread wait on thread handles
        //
        dwThreadCount   = dwWaitHandleCount / dwMaximumWaitObjects;
        dwLastWaitCount = dwWaitHandleCount % dwMaximumWaitObjects;

        if( dwLastWaitCount > 0 )
        {
            dwThreadCount++;
        }

        //
        // This function can handle a maximum of
        // MAXIMUM_WAIT_OBJECTS * MAXIMUM_WAIT_OBJECT handles
        //
        if( dwThreadCount > dwMaximumWaitObjects )
        {
            dwThreadCount     = dwMaximumWaitObjects;
            dwLastWaitCount   = 0;
        }

        pThreadWaitHandles    = new HANDLE[ dwThreadCount ];
        pWaitInformationArray = new WAIT_THREAD_INFORMATION[ dwThreadCount ];

        if( pThreadWaitHandles == NULL || pWaitInformationArray == NULL )
        {
            //
            // Failure
            //
            goto Finished;
        }

        for( DWORD count = 0; count < dwThreadCount; count++)
        {
            //
            // Set information for the thread
            //
            pWaitInformationArray[ count ].dwWaitMSeconds =
                dwMaxWaitMSeconds;
            pWaitInformationArray[ count ].pWaitHandles =
                &pWaitHandles[ dwWaitHandleIndex ];

            if( count != dwThreadCount - 1 || dwLastWaitCount == 0 )
            {
                pWaitInformationArray[ count ].dwWaitObjectCount =
                    dwMaximumWaitObjects;
                dwWaitHandleIndex += dwMaximumWaitObjects;
            }
            else
            {
                pWaitInformationArray[count].dwWaitObjectCount =
                    dwLastWaitCount;
                dwWaitHandleIndex += dwLastWaitCount;
            }

            pThreadWaitHandles[ count ] =
                CreateThread( NULL,
                              0,
                              WaitForMultipleObjectsThread,
                              &pWaitInformationArray[ count ],
                              0,
                              NULL );
            if( pThreadWaitHandles[ count ] == NULL )
            {
                //
                // Not able to create threads break from the loop
                // and wait for threads
we already created.
                // dwThreadCount doesnt include this iteration
                //
                dwThreadCount = count;
                break;
            }
        }

        //
        // Failure is ignored
        //
        WaitForMultipleObjects( dwThreadCount,
                                pThreadWaitHandles,
                                TRUE,
                                dwMaxWaitMSeconds );
    }

Finished:

    if( pThreadWaitHandles != NULL )
    {
        for( DWORD count = 0; count < dwThreadCount; count++)
        {
            if( pThreadWaitHandles[ count ] != NULL )
            {
                CloseHandle( pThreadWaitHandles[ count ] );
                pThreadWaitHandles[ count ] = NULL;
            }
        }

        delete [] pThreadWaitHandles;
        pThreadWaitHandles = NULL;
    }

    if( pWaitInformationArray != NULL )
    {
        delete [] pWaitInformationArray;
        pWaitInformationArray = NULL;
    }

    return;
}

//static
DWORD
WINAPI
WaitForMultipleObjectsThread(
    LPVOID lpParameter = NULL
)
//
// Thread routine which calls WaitForMultipleObjects
// This is used to wait for more than MAXIMUM_WAIT_OBJECTS objects
//
{
    DWORD                    dwReturnValue      = 0;
    PWAIT_THREAD_INFORMATION pThreadInformation = NULL;

    ASSERT( lpParameter != NULL );

    pThreadInformation = ( PWAIT_THREAD_INFORMATION )lpParameter;

    ASSERT(pThreadInformation->dwWaitObjectCount <= MAXIMUM_WAIT_OBJECTS );
    ASSERT(pThreadInformation->pWaitHandles      != NULL );
    ASSERT(pThreadInformation->dwWaitMSeconds    != INFINITE );
    ASSERT(pThreadInformation->dwWaitMSeconds    >= 1000 );

    dwReturnValue = WaitForMultipleObjects(
                        pThreadInformation->dwWaitObjectCount,
                        pThreadInformation->pWaitHandles,
                        TRUE,
                        pThreadInformation->dwWaitMSeconds );

    return dwReturnValue;

}

Sample usage of this method will look like below.

//
// Allocate memory for wait handle array
//
pWaitHandles = new HANDLE[ dwWaitHandleCount ];

...

//
// Call WaitForAllObjects
//
WaitForAllObjects( dwWaitHandleCount,
                   pWaitHandles,
                   dwMaximumWait );

...

//
// Free memory held by pWaitHandles
//
if( pWaitHandles != NULL )
{
    delete [] pWaitHandles;
    pWaitHandles = NULL;
}

Hope this helps.
Kanwal