Performance Tuning PHP Apps on Windows with Wincache

A few weeks ago I wrote a post that showed how to improve the performance of PHP applications on Windows by using the IIS output caching module. Using the output caching module can have significant positive impact on application performance since pages are served from cache without executing any PHP code. However, this very strength can also be a drawback depending on how your application is built. Because entire pages are cached, using output caching may not be ideal for pages that have multiple data sources. The Wincache extension to PHP provides an alternative (and/or compliment) to output caching. In this post, I’ll look at what performance improvements you get for “free” just by enabling the Wincache extension, as well as how you can cache user objects to get finer caching granularity than output caching affords.

What Wincache Does for “Free”

By simply adding the php_wincache.dll file to your extension directory and adding extension=php_wincache.dll to your php.ini file, you get 3 performance improvements for “free” (I’m quoting from the Wincache documentation on php.net):

  • PHP opcode caching: PHP is a script processing engine, which reads an input stream of data that contains text and/or PHP instructions and produces another stream of data, most commonly in the HTML format. This means that on a web server the PHP engine reads, parses, compiles and executes a PHP script each time that it is requested by a Web client. The reading, parsing and compilation operations put additional load on the web server's CPU and file system and thus affect the overall performance of a PHP web application. The PHP bytecode (opcode) cache is used to store the compiled script bytecode in shared memory so that it can be re-used by PHP engine for subsequent executions of the same script. (You can get metadata about the cached opcodes by using the wincache_ocache_fileinfo and wincache_ocache_meminfo functions.)
  • File caching: Even with the PHP opcode cache enabled, the PHP engine has to accesses the script files on a file system. When PHP scripts are stored on a remote UNC file share, the file operations introduce a significant performance overhead. The Windows Cache Extension for PHP includes a file cache that is used to store the content of the PHP script files in shared memory, which reduces the amount of file system operations performed by PHP engine. (You can get metadata about cached files by using the wincache_fcache_fileinfo and wincache_fcache_meminfo functions.)
  • Resolve file path caching: PHP scripts very often include or operate with files by using relative file paths. Every file path has to be normalized to an absolute file path by the PHP engine. When a PHP application uses many PHP files and accesses them by relative paths, the operation of resolving the paths may negatively impact the application's performance. The Windows Cache Extension for PHP provides a Resolve File Path cache, which is used to store the mappings between relative and absolute file paths, thereby reducing the number of path resolutions that the PHP engine has to perform. (You can get metadata about cached paths by using the wincache_rplist_fileinfo and wincache_rplist_meminfo functions.)

I’ve seen large applications (such as WordPress and Drupal) handle up to 3 times as many page requests per second with Wincache enabled as without it. (I ran tests on my laptop. YMMV) Simply enabling Wincache should be part of any performance improvement strategy for PHP applications on Windows. And, if you are looking to squeeze more out of Wincache (and therefore your app), you can use the user cache functions that Wincache offers…

Caching User Objects

As I alluded to in the introduction, the Wincache extension allows you to cache “parts” of a multi-data-source page instead of an entire page (as the output caching module does). For example, if a database query was one data source on a page, Wincache can be used to cache the result set, which would save a round trip to the database. To see this in action, I modified the example application that is included in the SQL Server Driver for PHP documentation. The example application is very simple; it uses the AdventureWorks2008R2 database to allow a user search for products, see product reviews, and submit product reviews (my modified application is attached to this post). I’ve added logic that uses Wincache to cache database result sets based on user input. I’ll walk you through some of the code here.

The function below returns an array of products based on search terms provided by the user. The things that I think are worth pointing out are the following:

  • The wincache_ucache_get function attempts to get an object from cache by looking for a supplied key (the first parameter). If the object is found in the cache, the second parameter is set to true. Notice that the key is a concatenation of the string “products” and the user’s search terms. I’m attaching the “products” string to the key in order to distinguish the object from other cached objects that may not be a result set of products.
  • The $from_cache variable is not important for caching. I’m using it to determine if data was pulled from the cache or not (as you will see in the screen shots below).
  • The wincache_ucache_add function adds an object to the cache with the specified key only if the object is not already in the cache. Note that I’m setting the time-to-live (ttl) for this object to 300 seconds. You may want to expire objects from the cache much more quickly or slowly depending on your application.
 function GetProducts($search_terms, &$from_cache)
 {
     $results = array();
     // Get results from cache if possible. Otherwise, get results from database.
     $results = wincache_ucache_get("products".$search_terms, $success);
     $from_cache = "true";
     if(!$success)
     {
         $from_cache = "false";
         // Get results from the database.
         $conn = ConnectToDB();
         $tsql = "SELECT ProductID, Name, Color, Size, ListPrice 
                  FROM Production.Product 
                  WHERE Name LIKE '%' + ? + '%' AND ListPrice > 0.0";
         $params = str_replace(" ", "%", $search_terms);               
         $stmt = sqlsrv_query($conn, $tsql, array($params));
         if ( $stmt === false )
             die( FormatErrors( sqlsrv_errors() ) );
  
         if(sqlsrv_has_rows($stmt))
         {
             while( $row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_ASSOC))
             {
                 $results[] = $row;
             }
         }
                 
         // Add array of search results to user cache
         wincache_ucache_add("products".$search_terms, $results, 300);
     }
     return $results;
 }

Those two functions (wincache_ucache_get and wincache_ucache_add) are the bread and butter of the Wincache extension and will take you a long way toward improving performance. However, you need to think carefully about when you use them. For example, when a result set of product reviews (for a particular product ID) are returned from the database, I want to cache the result set in the same way I did for products. But what happens, when  a user submits a new product review? A typical scenario might be 1) insert the new review, then 2) show the new review in the context of other reviews for the selected product. If I’m caching product review result sets by product ID, then after inserting a new review I’ll retrieve reviews from cache, which won’t include the newly added review! So, I need to account for this in my application logic.

The function below returns an array of product reviews based on a product ID. The things worth pointing out are the following:

  • The wincache_ucache_exists function is used to simply determine if a particular key exists in the cache. This allows me to construct logic that only retrieves reviews from the cache if a new review has not been submitted. (When a new review is submitted, I’ll simply pass true for the value of $new_review.)
  • The $from_cache variable is not important for caching. I’m using it to determine if data was pulled from the cache or not (as you will see in the screen shots below).
  • The wincache_ucache_set function adds a new object to the cache if it doesn’t already exist. But, unlike wincache_ucache_add, it overwrites an object if it does already exist (which we want in the case where a new review was added). Again, note the ttl of 300 seconds (with the same caveat I mentioned above).
 function GetReviews($productID, &$from_cache, $new_review = false)
 {
     $reviews = array();
     // Get reviews from cache if a new review hasn't been submitted.
     if(wincache_ucache_exists("reviews".$productID) && !$new_review)
     {
         $reviews = wincache_ucache_get("reviews".$productID);
         $from_cache = "true";
     }
     else
     {
         // Get reviews from the database.
         $conn = ConnectToDB();
         $tsql = "SELECT ReviewerName, CONVERT(varchar(32), ReviewDate, 107) AS [ReviewDate], Rating, Comments 
                  FROM Production.ProductReview 
                  WHERE ProductID = ? 
                  ORDER BY ReviewDate DESC";
         $stmt = sqlsrv_query( $conn, $tsql, array($productID));
         if( $stmt === false )
             die( FormatErrors( sqlsrv_errors() ) );
         
         if(sqlsrv_has_rows($stmt))
         {
             while ( $row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_ASSOC ) )
             {
                 $reviews[] = $row;
             }
             // Add array of reviews to cache. Overwrite cached object if it already exists.
             wincache_ucache_set(array("reviews".$productID => $reviews), null, 300);
         }
     }
     return $reviews;
 }

With that work done, it’s time to take a look at how it improves performance of the application. I’ve added some function calls and flags to show when a result set is retrieved from cache. I’ve also used the Wincache metadata functions (wincache_ucache_info and wincache_ucache_meminfo) to surface cache information in the UI.

In this screen shot, I’ve searched for product with the key word “gloves”. Since this is the first search based on that keyword, the results are not retrieved from the cache. But, the results have been added to the cache (notice the key_name: productgloves in the cache metadata). You can see the page load time in the screen shot along with other information about the cache:

image

By searching for “gloves” again, the results are returned from the cache and my page load time has decreased dramatically:

image

You’ll see similar results when retrieving reviews for a product:

image

Refresh that page and the results will be retrieved from the cache:

image

However, if you submit a review for this product, you want the reviews to be retrieved from the database after the new review has been submitted. Because of the logic built into the GetReviews function, this is exactly what happens:

image

And, because our logic overwrites the cache entry for reviews when a new review is added, this result set will be served from the cache the next time reviews are requested for this product (if the request happens with in the specified ttl, 300 seconds in my case).

Note: If you are looking at that last screen shot carefully you may notice that the object with key productgloves appears to still be cached even though it has been in the cache longer than its ttl. A background process periodically removes expired objects from the cache. From a functional standpoint, this object does not exist in the cache. i.e. If that same key is requested again, it will not be served from cache and a new object (with the same key) will be added to the cache.

Other Wincache Performance Enhancements Options

In addition to being an object cache, with minimal work Wincache can also store session data in shared memory. Having session data in shared memory improves application performance by saving on the time it takes to read/write session data from/to files. To enable this feature, make the following modification to your php.ini file:

session.save_handler = wincache
session.save_path = C:\inetpub\temp\session\

For more information about caching session data with Wincache, see Wincache Session Handler in the official documentation.

Conclusion

The Wincache extension provides several ways to significantly improve PHP application performance on Windows. The largest performance gains can be made by simply enabling the Wincache extension (you get opcode, file, and resolved path caching for “free”). You can further enhance performance by leveraging the wincache_ucache_* functions to cache user objects (these functions are especially useful when IIS output caching does not provide the granularity you need for caching data). And finally, you can squeeze a few more drops of performance out of your application by enabling Wincache session handling.

Have fun performance tuning!

-Brian

Share this on Twitter

AdventureWorksDemo.zip

Comments