At the Intersection of PHP and Microsoft
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.
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):
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…
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:
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:
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; }
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);
else
// Get reviews from the database.
$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 )
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:
By searching for “gloves” again, the results are returned from the cache and my page load time has decreased dramatically:
You’ll see similar results when retrieving reviews for a product:
Refresh that page and the results will be retrieved from the cache:
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:
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.
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.
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